Sprint 23/24 — Tauri v2 앱 래핑 + salsa 0.16 증분 쿼리 백엔드
All checks were successful
Publish ParaWiki / build-and-deploy (push) Successful in 34s

Sprint 23: cimery-app을 Tauri v2 앱으로 전환.
- tauri.conf.json, capabilities/default.json, frontend/index.html 추가
- src/commands.rs: 7개 IPC 커맨드 (launch_viewer, 프로젝트 관리, USD/CSV 익스포트)
- 뷰어 사이드카: std::process::Command 방식 (PATH + exe-dir 탐색)
- release.yml: 3단계 멀티플랫폼 릴리스 워크플로로 교체

Sprint 24: cimery-incremental에 salsa 0.16 백엔드 추가.
- salsa_db.rs: BridgeQueryGroup + SalsaIncrementalDb<K>
- --features salsa-backend 로 활성화 (기본값: 수동 tracking, WASM 안전)
- IR 전 구조체 + Mesh + KernelError에 PartialEq/Eq 추가
- 테스트 20개 전부 통과 (수동 12 + salsa 8)
- cargo check --workspace 0 errors/warnings

기타: viewer/dsl 컴파일 경고 제거, wiki 실행 가이드 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-15 09:09:47 +09:00
parent 1f9ca3a00f
commit 824c18610b
24 changed files with 1743 additions and 138 deletions

View File

@@ -2,12 +2,15 @@
name = "cimery-app"
version.workspace = true
edition.workspace = true
description = "cimery desktop application (Tauri v2 + Leptos UI)"
description = "cimery desktop application (Tauri v2 + sidecar viewer)"
[features]
# Geometry backends
occt = ["cimery-kernel/occt"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
cimery-core = { workspace = true }
cimery-ir = { workspace = true }
@@ -22,6 +25,6 @@ serde_json = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }
# Tauri v2 (ADR-001: desktop packaging)
# Uncomment when setting up Tauri project:
# tauri = { version = "2", features = ["devtools"] }
# Tauri v2 (ADR-001: Tauri v2 desktop packaging)
tauri = { version = "2", features = ["devtools"] }
tauri-plugin-dialog = "2"

View File

@@ -0,0 +1,6 @@
// Sprint 23 — Tauri v2 build script.
// tauri_build::build() generates src-tauri metadata required by the Tauri runtime.
// Must be the only content in build.rs.
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://schema.tauri.app/config/2/capability",
"identifier": "default",
"description": "Default cimery app capabilities",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message"
]
}

View File

@@ -0,0 +1,340 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>cimery</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', 'Malgun Gothic', sans-serif;
background: #1a1d23;
color: #d4d8e0;
height: 100vh;
display: flex;
flex-direction: column;
}
/* ── Titlebar ─────────────────────────────────────────────── */
.titlebar {
background: #13161b;
border-bottom: 1px solid #2c3040;
padding: 10px 20px;
display: flex;
align-items: center;
gap: 12px;
user-select: none;
}
.titlebar .logo {
font-size: 18px;
font-weight: 700;
color: #5b9bd5;
letter-spacing: 1px;
}
.titlebar .subtitle {
font-size: 12px;
color: #6b7280;
}
/* ── Main layout ──────────────────────────────────────────── */
.main {
flex: 1;
display: flex;
overflow: hidden;
}
/* ── Sidebar ──────────────────────────────────────────────── */
.sidebar {
width: 220px;
background: #13161b;
border-right: 1px solid #2c3040;
padding: 16px 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.sidebar-item {
padding: 9px 20px;
font-size: 13px;
cursor: pointer;
border-left: 3px solid transparent;
transition: background 0.15s;
}
.sidebar-item:hover { background: #1f2330; }
.sidebar-item.active {
background: #1f2330;
border-left-color: #5b9bd5;
color: #fff;
}
.sidebar-section {
padding: 16px 20px 6px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
color: #4b5563;
}
/* ── Content ──────────────────────────────────────────────── */
.content {
flex: 1;
padding: 32px 40px;
overflow-y: auto;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #e5e7eb;
margin-bottom: 24px;
}
/* ── Card grid ────────────────────────────────────────────── */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.card {
background: #1f2330;
border: 1px solid #2c3040;
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: border-color 0.15s, transform 0.1s;
}
.card:hover {
border-color: #5b9bd5;
transform: translateY(-1px);
}
.card h3 { font-size: 14px; color: #e5e7eb; margin-bottom: 6px; }
.card p { font-size: 12px; color: #6b7280; line-height: 1.5; }
.card .icon { font-size: 24px; margin-bottom: 10px; }
/* ── Action buttons ───────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 18px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: none;
transition: background 0.15s;
}
.btn-primary { background: #2563eb; color: #fff; }
.btn-primary:hover { background: #1d4ed8; }
.btn-secondary { background: #1f2330; color: #d4d8e0; border: 1px solid #2c3040; }
.btn-secondary:hover { background: #2c3040; }
.btn-group { display: flex; gap: 10px; margin-bottom: 28px; }
/* ── Status bar ───────────────────────────────────────────── */
.statusbar {
background: #13161b;
border-top: 1px solid #2c3040;
padding: 6px 20px;
font-size: 11px;
color: #4b5563;
display: flex;
gap: 20px;
}
.status-ok { color: #22c55e; }
.status-warn { color: #f59e0b; }
/* ── Recent projects table ────────────────────────────────── */
.recent-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.recent-table th {
text-align: left;
padding: 8px 12px;
color: #6b7280;
font-weight: 400;
border-bottom: 1px solid #2c3040;
}
.recent-table td {
padding: 10px 12px;
border-bottom: 1px solid #1f2330;
color: #d4d8e0;
}
.recent-table tr:hover td { background: #1f2330; cursor: pointer; }
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
background: #1e3a5f;
color: #5b9bd5;
}
</style>
</head>
<body>
<!-- Titlebar -->
<div class="titlebar">
<span class="logo">cimery</span>
<span class="subtitle">Civil Parametric BIM v0.1.0</span>
</div>
<!-- Main layout -->
<div class="main">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-section">작업공간</div>
<div class="sidebar-item active" onclick="showPage('home')"></div>
<div class="sidebar-item" onclick="showPage('projects')">프로젝트</div>
<div class="sidebar-item" onclick="launchViewer()">3D 뷰어 열기</div>
<div class="sidebar-section">내보내기</div>
<div class="sidebar-item" onclick="exportUSD()">USD 익스포트</div>
<div class="sidebar-item" onclick="exportCSV()">CSV 템플릿</div>
<div class="sidebar-section">도움말</div>
<div class="sidebar-item" onclick="showPage('about')">정보</div>
</nav>
<!-- Content area -->
<div class="content" id="content">
<!-- Populated by JS showPage() -->
</div>
</div>
<!-- Status bar -->
<div class="statusbar">
<span id="status-kernel" class="status-ok">커널: PureRust</span>
<span id="status-version">v0.1.0</span>
<span id="status-msg"></span>
</div>
<script>
// ── Tauri IPC helper ─────────────────────────────────────────
async function invoke(cmd, args = {}) {
try {
const { invoke: tauriInvoke } = await import('https://unpkg.com/@tauri-apps/api@2/core');
return await tauriInvoke(cmd, args);
} catch (e) {
// Dev fallback when running outside Tauri (browser preview)
console.warn('Tauri IPC not available — dev fallback', cmd, args);
return { ok: false, error: 'dev_mode' };
}
}
async function launchViewer() {
setStatus('3D 뷰어 실행 중…');
const result = await invoke('launch_viewer');
if (result && result.ok) {
setStatus('뷰어 실행됨');
} else {
setStatus('뷰어 실행 실패: ' + (result?.error ?? '알 수 없는 오류'), true);
}
}
async function exportUSD() {
setStatus('USD 익스포트 중…');
const result = await invoke('export_usd_default');
setStatus(result?.ok ? 'USD 익스포트 완료' : ('USD 실패: ' + result?.error), !result?.ok);
}
async function exportCSV() {
setStatus('CSV 템플릿 생성 중…');
const result = await invoke('export_csv_template');
setStatus(result?.ok ? 'CSV 템플릿 생성 완료' : ('CSV 실패: ' + result?.error), !result?.ok);
}
function setStatus(msg, warn = false) {
const el = document.getElementById('status-msg');
el.textContent = msg;
el.className = warn ? 'status-warn' : 'status-ok';
}
// ── Pages ────────────────────────────────────────────────────
const pages = {
home: `
<div class="section-title">시작하기</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="launchViewer()">3D 뷰어 열기</button>
<button class="btn btn-secondary" onclick="newProject()">새 프로젝트</button>
<button class="btn btn-secondary" onclick="openProject()">프로젝트 열기</button>
</div>
<div class="card-grid">
<div class="card" onclick="launchViewer()">
<div class="icon">🏗️</div>
<h3>거더교 3D 뷰어</h3>
<p>egui+wgpu 기반 실시간 파라메트릭 뷰어</p>
</div>
<div class="card" onclick="exportUSD()">
<div class="icon">📦</div>
<h3>USD 익스포트</h3>
<p>교량 씬 전체를 USD 텍스트 포맷으로 저장</p>
</div>
<div class="card" onclick="exportCSV()">
<div class="icon">📋</div>
<h3>CSV 템플릿</h3>
<p>거더 파라미터 템플릿을 CSV로 생성</p>
</div>
<div class="card" onclick="showPage('about')">
<div class="icon"></div>
<h3>cimery 정보</h3>
<p>버전·기술 스택·라이선스</p>
</div>
</div>
`,
projects: `
<div class="section-title">프로젝트</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="newProject()">새 프로젝트</button>
<button class="btn btn-secondary" onclick="openProject()">열기</button>
</div>
<table class="recent-table">
<thead>
<tr><th>이름</th><th>마지막 수정</th><th>유형</th></tr>
</thead>
<tbody id="project-list">
<tr><td colspan="3" style="color:#4b5563;padding:20px 12px;">저장된 프로젝트 없음</td></tr>
</tbody>
</table>
`,
about: `
<div class="section-title">cimery 정보</div>
<div class="card" style="max-width:480px">
<h3 style="font-size:16px;margin-bottom:12px">cimery v0.1.0</h3>
<p style="margin-bottom:8px">Civil + BIM + -ery</p>
<p style="margin-bottom:16px;line-height:1.8">
토목 엔지니어링 특성을 반영한 파라메트릭 모델링 도구.<br>
MVP: 거더교 (PSC-I 거더, 교각, 교대, 받침, 교량받침).
</p>
<p style="font-size:11px;color:#4b5563">
Tauri v2 · egui 0.29 · wgpu 22 · Rust 2021<br>
License: MIT OR Apache-2.0
</p>
</div>
`,
};
function showPage(name) {
document.querySelectorAll('.sidebar-item').forEach(el => el.classList.remove('active'));
event?.target?.classList.add('active');
document.getElementById('content').innerHTML = pages[name] ?? pages.home;
}
async function newProject() {
const result = await invoke('new_project');
setStatus(result?.ok ? '새 프로젝트 생성됨' : '새 프로젝트 실패');
}
async function openProject() {
const result = await invoke('open_project_dialog');
if (result?.ok) setStatus('프로젝트 열림: ' + result.path);
}
// Init
showPage('home');
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,257 @@
//! Tauri IPC commands (Sprint 23).
//!
//! Each function tagged with `#[tauri::command]` becomes callable from the
//! webview frontend via `invoke('command_name', args)`.
//!
//! Design principle: commands are thin shims — validation & business logic
//! live in the domain crates (cimery-dsl, cimery-usd, etc.).
use serde::Serialize;
use tauri::AppHandle;
// ── Response envelope ─────────────────────────────────────────────────────────
/// Uniform JSON response for all IPC commands.
#[derive(Debug, Serialize)]
pub struct CmdResult {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl CmdResult {
fn ok() -> Self {
Self { ok: true, path: None, data: None, error: None }
}
fn ok_path(path: impl Into<String>) -> Self {
Self { ok: true, path: Some(path.into()), data: None, error: None }
}
fn ok_data(data: serde_json::Value) -> Self {
Self { ok: true, path: None, data: Some(data), error: None }
}
fn err(e: impl Into<String>) -> Self {
Self { ok: false, path: None, data: None, error: Some(e.into()) }
}
}
// ── App info ──────────────────────────────────────────────────────────────────
/// Returns app version and build info.
#[tauri::command]
pub fn get_version() -> CmdResult {
CmdResult::ok_data(serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"kernel": "PureRust",
"build": "debug"
}))
}
// ── Viewer launcher ───────────────────────────────────────────────────────────
/// Spawns `cimery-viewer` as a child process.
///
/// Resolution order:
/// 1. Same directory as the running `cimery-app` executable (release bundle).
/// 2. `CIMERY_VIEWER_PATH` env var (developer override).
/// 3. `cimery-viewer` on PATH (fallback for dev builds).
#[tauri::command]
pub async fn launch_viewer(_app: AppHandle) -> CmdResult {
let viewer_path = find_viewer_binary();
log::info!("launching viewer: {viewer_path:?}");
match std::process::Command::new(&viewer_path).spawn() {
Ok(_child) => {
log::info!("cimery-viewer spawned: {viewer_path:?}");
CmdResult::ok()
}
Err(e) => {
log::error!("viewer spawn failed: {e}");
CmdResult::err(format!("{}: {e}", viewer_path.display()))
}
}
}
/// Resolves the cimery-viewer binary path.
fn find_viewer_binary() -> std::path::PathBuf {
// 1. Developer override
if let Ok(p) = std::env::var("CIMERY_VIEWER_PATH") {
return std::path::PathBuf::from(p);
}
// 2. Same directory as the running executable
let viewer_name = if cfg!(windows) { "cimery-viewer.exe" } else { "cimery-viewer" };
if let Ok(exe) = std::env::current_exe() {
let candidate = exe.parent().unwrap_or(&exe).join(viewer_name);
if candidate.exists() {
return candidate;
}
}
// 3. PATH fallback
std::path::PathBuf::from(viewer_name)
}
// ── Project management ────────────────────────────────────────────────────────
/// Creates a new empty project (in-memory; no file written yet).
#[tauri::command]
pub fn new_project() -> CmdResult {
// Sprint 23: stub — full project file I/O in a later sprint.
log::info!("new_project requested");
CmdResult::ok_data(serde_json::json!({ "name": "Untitled", "features": [] }))
}
/// Opens a save dialog and loads a `.cimery` project file.
#[tauri::command]
pub async fn open_project_dialog(app: AppHandle) -> CmdResult {
use tauri_plugin_dialog::DialogExt;
let path = app
.dialog()
.file()
.add_filter("cimery project", &["cimery", "json"])
.blocking_pick_file();
match path {
Some(p) => {
let path_str = p.to_string();
match std::fs::read_to_string(&path_str) {
Ok(contents) => match serde_json::from_str::<serde_json::Value>(&contents) {
Ok(data) => {
log::info!("project loaded: {path_str}");
let mut result = CmdResult::ok_data(data);
result.path = Some(path_str);
result
}
Err(e) => CmdResult::err(format!("JSON parse error: {e}")),
},
Err(e) => CmdResult::err(format!("file read error: {e}")),
}
}
None => CmdResult::err("cancelled"),
}
}
/// Saves project JSON to a user-selected path.
#[tauri::command]
pub async fn save_project_dialog(
app: AppHandle,
#[allow(unused)] data: serde_json::Value,
) -> CmdResult {
use tauri_plugin_dialog::DialogExt;
let path = app
.dialog()
.file()
.add_filter("cimery project", &["cimery"])
.set_file_name("project.cimery")
.blocking_save_file();
match path {
Some(p) => {
let path_str = p.to_string();
match serde_json::to_string_pretty(&data) {
Ok(json) => match std::fs::write(&path_str, &json) {
Ok(_) => {
log::info!("project saved: {path_str}");
CmdResult::ok_path(path_str)
}
Err(e) => CmdResult::err(format!("write error: {e}")),
},
Err(e) => CmdResult::err(format!("serialize error: {e}")),
}
}
None => CmdResult::err("cancelled"),
}
}
// ── USD export ────────────────────────────────────────────────────────────────
/// Exports the default bridge scene to a USD file.
/// The user picks the output path via a save dialog.
#[tauri::command]
pub async fn export_usd_default(app: AppHandle) -> CmdResult {
use tauri_plugin_dialog::DialogExt;
use cimery_dsl::Girder;
use cimery_core::UnitExt;
use cimery_usd::BridgeExporter;
let path = app
.dialog()
.file()
.add_filter("Universal Scene Description", &["usda", "usd"])
.set_file_name("bridge_scene.usda")
.blocking_save_file();
let Some(p) = path else {
return CmdResult::err("cancelled");
};
let path_str = p.to_string();
// Build the default girder bridge and export
let result = (|| -> Result<(), String> {
use cimery_kernel::PureRustKernel;
let girder = Girder::builder()
.station_start(0.0.m())
.station_end(40.0.m())
.section_psc_i_default()
.count(5)
.spacing(2500.0.mm())
.build()
.map_err(|e| e.to_string())?;
let mut exporter = BridgeExporter::new("BR-001");
let kernel = PureRustKernel;
let usda = exporter
.export_scene(&kernel, &[girder.ir], &[], &[], &[], &[])
.map_err(|e| format!("export error: {e}"))?;
std::fs::write(&path_str, usda)
.map_err(|e| format!("write error: {e}"))?;
Ok(())
})();
match result {
Ok(_) => {
log::info!("USD exported: {path_str}");
CmdResult::ok_path(path_str)
}
Err(e) => CmdResult::err(e),
}
}
// ── CSV template ──────────────────────────────────────────────────────────────
/// Generates a CSV parameter template for girder design.
#[tauri::command]
pub async fn export_csv_template(app: AppHandle) -> CmdResult {
use tauri_plugin_dialog::DialogExt;
use cimery_dsl::csv_template::girder_to_csv_template;
let path = app
.dialog()
.file()
.add_filter("CSV", &["csv"])
.set_file_name("girder_template.csv")
.blocking_save_file();
let Some(p) = path else {
return CmdResult::err("cancelled");
};
let path_str = p.to_string();
let csv = girder_to_csv_template();
match std::fs::write(&path_str, csv) {
Ok(_) => {
log::info!("CSV template exported: {path_str}");
CmdResult::ok_path(path_str)
}
Err(e) => CmdResult::err(format!("write error: {e}")),
}
}

View File

@@ -1,61 +1,55 @@
//! cimery-app — Tauri v2 desktop application skeleton.
//! cimery-app — Tauri v2 desktop application (Sprint 23).
//!
//! ADR-001: Tauri v2 (desktop) + PWA (web) dual-target.
//! ADR-003 A3: Gitea CI → GitHub Actions for Win/macOS release builds.
//! # 아키텍처
//! - Tauri v2 webview window: `frontend/index.html` (런처 UI)
//! - 3D 뷰어: `cimery-viewer` 사이드카 바이너리 (egui+wgpu, 별도 네이티브 창)
//! - IPC: `src/commands.rs`의 `#[tauri::command]` 함수들
//!
//! # Sprint 13 (this file): application shell scaffold
//! - Tauri integration commented out (requires `tauri init` + frontend setup)
//! - Core domain logic wired and accessible
//! # 빌드
//! ```bash
//! # 개발 실행 (Tauri dev — webview)
//! cargo tauri dev -p cimery-app
//!
//! # Sprint 14: Leptos UI frontend
//! - Leptos component tree for ribbon/panel/viewport layout
//! - wgpu viewport embedded as a <canvas> element
//! - Property panel connected to cimery-dsl builders
//! # 릴리스 설치 파일 생성
//! cargo tauri build -p cimery-app
//! # → src-tauri/target/release/bundle/
//! # Windows: .msi / .exe (NSIS)
//! # macOS: .dmg / .app
//! # Linux: .deb / .AppImage
//! ```
//!
//! # Tauri setup checklist (run once):
//! 1. `cargo tauri init` in this directory
//! 2. Edit `tauri.conf.json`: app name, window size, icons
//! 3. Implement Tauri commands (IPC bridge) in `src/commands.rs`
//! 4. Set up Leptos frontend in `src/ui/`
//! # 사이드카 뷰어
//! tauri.conf.json `bundle.externalBin`에 선언된 `cimery-viewer`는
//! 릴리스 빌드 시 앱 번들에 포함된다.
//! 개발 시: `cargo build -p cimery-viewer --release` 후 실행.
//!
//! ADR-001: Tauri v2 desktop + PWA 듀얼 타겟.
//! ADR-003 A3: CI/CD — GitHub Actions release.yml.
use cimery_dsl::Girder;
use cimery_core::UnitExt;
use cimery_kernel::{GeomKernel, PureRustKernel};
// Tauri v2 requires this on Windows.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod commands;
fn main() {
env_logger::init();
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("info")
).init();
// ── Quick sanity check: build a test girder ──────────────────────────────
let girder = Girder::builder()
.station_start(0.0.m())
.station_end(40.0.m())
.section_psc_i_default()
.count(5)
.spacing(2500.0.mm())
.build()
.expect("valid girder");
let mesh = PureRustKernel.girder_mesh(&girder.ir)
.expect("girder mesh");
log::info!(
"cimery-app startup OK — test girder: span={:.0}m, triangles={}",
girder.ir.span_m(), mesh.triangle_count()
);
// ── Tauri entry point (activate when Tauri is set up) ──────────────────
// Uncomment after `cargo tauri init`:
//
// tauri::Builder::default()
// .invoke_handler(tauri::generate_handler![
// commands::get_scene_params,
// commands::set_scene_params,
// commands::save_project,
// commands::load_project,
// ])
// .run(tauri::generate_context!())
// .expect("Tauri runtime error");
println!("cimery-app v{}", env!("CARGO_PKG_VERSION"));
println!("Tauri integration: pending (Sprint 14 — run `cargo tauri init`)");
tauri::Builder::default()
// ── Plugins ─────────────────────────────────────────────────────────
.plugin(tauri_plugin_dialog::init())
// ── IPC handlers ────────────────────────────────────────────────────
.invoke_handler(tauri::generate_handler![
commands::get_version,
commands::launch_viewer,
commands::new_project,
commands::open_project_dialog,
commands::save_project_dialog,
commands::export_usd_default,
commands::export_csv_template,
])
// ── Run ─────────────────────────────────────────────────────────────
.run(tauri::generate_context!())
.expect("Tauri application error");
}

View File

@@ -0,0 +1,53 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "cimery",
"version": "0.1.0",
"identifier": "com.cimery.app",
"build": {
"frontendDist": "frontend"
},
"app": {
"windows": [
{
"label": "main",
"title": "cimery — Civil Parametric BIM",
"width": 1280,
"height": 800,
"minWidth": 960,
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"center": true
}
],
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'"
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/icon.ico"
],
"resources": {},
"windows": {
"wix": {
"language": "ko-KR"
},
"nsis": {
"languages": ["Korean"],
"installMode": "perMachine"
}
},
"macOS": {
"minimumSystemVersion": "11.0"
},
"linux": {
"deb": {
"depends": []
}
}
},
"plugins": {}
}