DOSSIER Multi-Phase: C#-Plugin + Yak + Wandstile + UX-Polish
- C#-Plugin "DOSSIER" mit 23 nativen Commands (dWall, dDoor, ..., dSection)
- Native Command-Namen + Autocomplete + saubere History
- Idle-Defer + RhinoCode-API → kein _-RunPythonScript-Echo
- Yak-Paket via build.sh, Install in ~/Library/.../packages/8.0/
- Launcher (Tauri):
- dossier_init Tauri-Command + Setup-Tab in Settings
- Yak-Install + StartupCommands-XML + Window-Layout in einem Schritt
- clean-rhino.sh fuer reproduzierbare Resets
- check_dossier_initialized triggert Auto-Open-Setup beim ersten Start
- Wand-Architektur:
- Chain-Logik DEAKTIVIERT → jede Wand baut eigenes Volume (individuell
anwaehlbar, einzeln loeschbar)
- Polyline-Wand: jedes Segment = eigene Wand
- Smart-Split fuer wand_axis/decke/dach/raum/aussparung/traeger
- Auto-Group axis+volume → kein ChooseOne-Dialog, Delete loescht beides
- Stale-Mitre-Fix: Joint-Cache wird vor jedem Wand-Regen invalidiert
- T-Junction-Tolerance auf 1mm (war 1cm, lieferte falsche T-Mitres)
- Wand-Stile:
- Schema in dossier_project_settings.wand_styles (Material + Prio +
Default-Dicke + Referenz, oder Layered mit Schichten)
- dWall-Command Stil-Picker
- ProjectSettingsDialog: Sidebar-Layout (Pill-Selection) +
Wandstile-Tab mit Liste/Editor
- _wand_chain_compat benutzt style_id
- Prio-Dominanz: hoehere Prio gewinnt Eckverbindung, niedrigere wird
T-mitered (siehe _resolve_corner_miter)
- Cmd+G fuer Group (Geschoss-Up auf Alias 'gu')
- Welcome + Cheatsheet borderless mit X/Back-Buttons
- BeginCommand-Hook fuer Gestaltung-Panel-Auto-Open
- panel_base: Python.NET-Enum-Fix fuer Material-Render
This commit is contained in:
@@ -303,9 +303,17 @@ fn splash_owner_marker_path() -> PathBuf {
|
||||
|
||||
fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> {
|
||||
let settings = load_settings();
|
||||
// XML-Edit nur sinnvoll wenn Rhino nicht laeuft (sonst ueberschreibt's
|
||||
// beim Beenden) UND der Eintrag fuer den naechsten Start eh schon greift.
|
||||
// Setup-Schritte nur wenn Rhino NICHT laeuft — sonst ueberschreibt Rhino
|
||||
// unsere XML-Edits beim Beenden, und yak install kann die in-use .rhp
|
||||
// nicht ersetzen.
|
||||
if settings.auto_load_plugin && !is_rhino_running() {
|
||||
// Schritt 1: .rhp via yak installieren/aktualisieren. Soft-Fail wenn
|
||||
// .yak nicht gebundelt ist (Dev-Setup ohne build.sh ausgefuehrt) —
|
||||
// Bootstrap via XML reicht alleine, Commands fehlen nur.
|
||||
if let Err(e) = ensure_rhino_plugin_installed() {
|
||||
eprintln!("[DOSSIER] Plugin-Install skip: {e}");
|
||||
}
|
||||
// Schritt 2: StartupCommands-XML fuer Python-Bootstrap setzen.
|
||||
let startup_path = settings.plugin_startup_path
|
||||
.clone()
|
||||
.unwrap_or_else(default_plugin_startup_path);
|
||||
@@ -466,11 +474,264 @@ fn ensure_rhino_startup_command(startup_path: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// ===== Rhino-Plugin Install via Yak =====
|
||||
// Mac Rhino lehnt Auto-Load fuer dritte Plugins ab (egal welche Methode).
|
||||
// Wir installieren `.yak` aber trotzdem in den User-Plugin-Pfad — beim ersten
|
||||
// dWall/dDoor/... laedt Rhino das Plugin on-demand und cached es danach.
|
||||
// Der Python-Bootstrap (Panels/Aliases) laeuft parallel via StartupCommands-
|
||||
// XML (ensure_rhino_startup_command). Beide Pfade zusammen = full setup.
|
||||
|
||||
fn yak_binary_path() -> PathBuf {
|
||||
PathBuf::from("/Applications/Rhino 8.app/Contents/Resources/bin/yak")
|
||||
}
|
||||
|
||||
fn rhino_packages_dir() -> PathBuf {
|
||||
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
|
||||
home.join("Library/Application Support/McNeel/Rhinoceros/packages/8.0")
|
||||
}
|
||||
|
||||
fn installed_dossier_version() -> Option<String> {
|
||||
let manifest = rhino_packages_dir().join("DOSSIER/manifest.txt");
|
||||
fs::read_to_string(&manifest).ok().map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
// .yak + version.txt im App-Bundle (Production) oder Repo-dist/ (Dev).
|
||||
fn bundled_plugin_paths() -> (PathBuf, PathBuf) {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
|
||||
let yak = contents_dir.join("Resources/plugin/dossier.yak");
|
||||
let ver = contents_dir.join("Resources/plugin/dossier-version.txt");
|
||||
if yak.is_file() && ver.is_file() {
|
||||
return (yak, ver);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dev-Fallback: Repo-Pfad
|
||||
let repo = PathBuf::from("/Users/karim/STUDIO/DOSSIER/csharp/DOSSIER/dist");
|
||||
(repo.join("dossier.yak"), repo.join("dossier-version.txt"))
|
||||
}
|
||||
|
||||
fn bundled_plugin_version() -> Option<String> {
|
||||
let (_, ver_path) = bundled_plugin_paths();
|
||||
fs::read_to_string(&ver_path).ok().map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
// Installiert/aktualisiert das DOSSIER-Plugin via yak. Idempotent — skip wenn
|
||||
// installierte Version == gebundelte Version. Soft-Fail wenn .yak fehlt oder
|
||||
// yak-CLI nicht vorhanden (Logging, kein Error — Bootstrap via XML laeuft eh).
|
||||
fn ensure_rhino_plugin_installed() -> Result<(), String> {
|
||||
let yak = yak_binary_path();
|
||||
if !yak.is_file() {
|
||||
return Err(format!("yak CLI nicht gefunden: {}", yak.display()));
|
||||
}
|
||||
let (yak_pkg, _) = bundled_plugin_paths();
|
||||
if !yak_pkg.is_file() {
|
||||
return Err(format!(
|
||||
"DOSSIER .yak-Paket nicht gefunden: {} (build.sh in csharp/DOSSIER ausfuehren)",
|
||||
yak_pkg.display()
|
||||
));
|
||||
}
|
||||
let bundled_ver = bundled_plugin_version();
|
||||
let installed_ver = installed_dossier_version();
|
||||
if let (Some(b), Some(i)) = (&bundled_ver, &installed_ver) {
|
||||
if b == i {
|
||||
return Ok(()); // schon aktuell — kein Re-Install
|
||||
}
|
||||
}
|
||||
if is_rhino_running() {
|
||||
return Err(
|
||||
"Rhino laeuft — Plugin-Update kann erst nach Rhino-Quit installiert werden."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let pkg_dir = yak_pkg.parent().ok_or_else(|| "Plugin-Pfad ohne Parent".to_string())?;
|
||||
let output = Command::new(&yak)
|
||||
.arg("install")
|
||||
.arg("dossier")
|
||||
.arg("--source")
|
||||
.arg(pkg_dir)
|
||||
.output()
|
||||
.map_err(|e| format!("yak install: {e}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"yak install fehlgeschlagen: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
eprintln!(
|
||||
"[DOSSIER] Plugin via yak installiert: {} → {}",
|
||||
installed_ver.unwrap_or_else(|| "(nicht installiert)".into()),
|
||||
bundled_ver.unwrap_or_else(|| "(unbekannt)".into())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
fn open_rhino(app: tauri::AppHandle, path3dm: String) -> Result<(), String> {
|
||||
open_rhino_internal(&app, &path3dm)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn install_rhino_plugin() -> Result<String, String> {
|
||||
ensure_rhino_plugin_installed()?;
|
||||
Ok(installed_dossier_version().unwrap_or_else(|| "(version unbekannt)".into()))
|
||||
}
|
||||
|
||||
|
||||
// ===== Window-Layout Installation =====
|
||||
// Rhino-Workspaces sind XML-Dateien im User-Pfad benannt nach Layout-GUID.
|
||||
// Wir bundlen DOSSIERs Master-Layout(s) im Repo unter rhino/workspaces/ und
|
||||
// kopieren sie beim Init in Rhinos Workspaces-Folder, damit startup.py das
|
||||
// Layout per `_-WindowLayout "<name>" _Enter` direkt anwenden kann.
|
||||
|
||||
fn rhino_workspaces_dir() -> PathBuf {
|
||||
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
|
||||
home.join("Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces")
|
||||
}
|
||||
|
||||
fn bundled_workspaces_dir() -> PathBuf {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
|
||||
let bundled = contents_dir.join("Resources/rhino/workspaces");
|
||||
if bundled.is_dir() {
|
||||
return bundled;
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from("/Users/karim/STUDIO/DOSSIER/rhino/workspaces")
|
||||
}
|
||||
|
||||
// Kopiert alle Workspace-XMLs aus dem bundle in Rhinos Workspaces-Folder.
|
||||
// Vorhandene Files werden ueberschrieben (User-Customizations gehen verloren —
|
||||
// dafuer ist's reproduzierbar). Returns Anzahl kopierter Files.
|
||||
fn ensure_window_layout_installed() -> Result<usize, String> {
|
||||
let src = bundled_workspaces_dir();
|
||||
if !src.is_dir() {
|
||||
return Err(format!("Workspace-Quelle fehlt: {}", src.display()));
|
||||
}
|
||||
let dst = rhino_workspaces_dir();
|
||||
fs::create_dir_all(&dst)
|
||||
.map_err(|e| format!("Workspace-Zielordner erstellen: {e}"))?;
|
||||
if is_rhino_running() {
|
||||
return Err("Rhino laeuft — bitte erst beenden (Layout-Datei wird sonst ueberschrieben).".into());
|
||||
}
|
||||
let mut count = 0;
|
||||
for entry in fs::read_dir(&src).map_err(|e| format!("Workspace-Quelle lesen: {e}"))? {
|
||||
let entry = entry.map_err(|e| format!("DirEntry: {e}"))?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("xml")).unwrap_or(false) {
|
||||
let file_name = path.file_name().ok_or("FileName")?;
|
||||
let dst_path = dst.join(file_name);
|
||||
fs::copy(&path, &dst_path)
|
||||
.map_err(|e| format!("Workspace kopieren ({}): {e}", file_name.to_string_lossy()))?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
|
||||
// ===== DOSSIER INIT =====
|
||||
// Komplettes Setup auf einem neuen PC: Plugin via Yak + StartupCommands-XML +
|
||||
// Window-Layout-Files. Returns Status pro Schritt fuer das Frontend-Dialog.
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
struct InitStep {
|
||||
id: String,
|
||||
label: String,
|
||||
status: String, // "ok" | "error" | "skipped"
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
struct InitResult {
|
||||
steps: Vec<InitStep>,
|
||||
overall_ok: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
struct InitStatus {
|
||||
plugin_installed: bool,
|
||||
startup_cmd_set: bool,
|
||||
layout_installed: bool,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn check_dossier_initialized() -> InitStatus {
|
||||
let plugin_installed = installed_dossier_version().is_some();
|
||||
let startup_cmd_set = fs::read_to_string(rhino_settings_xml_path())
|
||||
.map(|s| s.contains("StartupCommands") && s.contains("startup.py"))
|
||||
.unwrap_or(false);
|
||||
let layout_installed = rhino_workspaces_dir()
|
||||
.join("b6b68c03-3031-4899-bca2-fe6e425146fc.xml")
|
||||
.is_file();
|
||||
InitStatus {
|
||||
plugin_installed,
|
||||
startup_cmd_set,
|
||||
layout_installed,
|
||||
// initialized = ALLE drei vorhanden. So zeigt der Dialog auch nach
|
||||
// teilweisem Clean (z.B. nur layout geloescht) noch an.
|
||||
initialized: plugin_installed && startup_cmd_set && layout_installed,
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn dossier_init() -> Result<InitResult, String> {
|
||||
if is_rhino_running() {
|
||||
return Err("Rhino laeuft — bitte erst beenden, dann Init nochmal starten.".into());
|
||||
}
|
||||
|
||||
let mut steps = Vec::new();
|
||||
let mut overall_ok = true;
|
||||
|
||||
// Schritt 1: Plugin via Yak installieren/aktualisieren
|
||||
let (status, detail) = match ensure_rhino_plugin_installed() {
|
||||
Ok(()) => {
|
||||
let v = installed_dossier_version().unwrap_or_else(|| "(?)".into());
|
||||
("ok".into(), format!("Version {v}"))
|
||||
}
|
||||
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||
};
|
||||
steps.push(InitStep {
|
||||
id: "plugin".into(),
|
||||
label: "DOSSIER-Plugin via Yak installieren".into(),
|
||||
status, detail,
|
||||
});
|
||||
|
||||
// Schritt 2: StartupCommands-XML eintragen (Python-Bootstrap)
|
||||
let startup_path = default_plugin_startup_path();
|
||||
let (status, detail) = if !Path::new(&startup_path).is_file() {
|
||||
overall_ok = false;
|
||||
("error".into(), format!("startup.py nicht gefunden: {startup_path}"))
|
||||
} else {
|
||||
match ensure_rhino_startup_command(&startup_path) {
|
||||
Ok(()) => ("ok".into(), startup_path.clone()),
|
||||
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||
}
|
||||
};
|
||||
steps.push(InitStep {
|
||||
id: "startup".into(),
|
||||
label: "Python-Bootstrap (StartupCommands-XML)".into(),
|
||||
status, detail,
|
||||
});
|
||||
|
||||
// Schritt 3: Window-Layout-Files in Rhinos Workspaces-Folder kopieren
|
||||
let (status, detail) = match ensure_window_layout_installed() {
|
||||
Ok(n) => ("ok".into(), format!("{n} Layout-Datei(en) installiert")),
|
||||
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||
};
|
||||
steps.push(InitStep {
|
||||
id: "layout".into(),
|
||||
label: "Window-Layout in Rhino kopieren".into(),
|
||||
status, detail,
|
||||
});
|
||||
|
||||
Ok(InitResult { steps, overall_ok })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn trigger_plugin_load_now() -> Result<(), String> {
|
||||
// Schreibt den `_RunPythonScript <pfad>` Eintrag in Rhinos Startup-Command-
|
||||
@@ -924,6 +1185,9 @@ pub fn run() {
|
||||
read_project_config,
|
||||
open_rhino,
|
||||
trigger_plugin_load_now,
|
||||
install_rhino_plugin,
|
||||
dossier_init,
|
||||
check_dossier_initialized,
|
||||
get_default_plugin_startup_path,
|
||||
show_in_finder,
|
||||
is_rhino_running,
|
||||
|
||||
@@ -56,7 +56,9 @@
|
||||
},
|
||||
"resources": {
|
||||
"../../dist": "dist",
|
||||
"../../rhino": "rhino"
|
||||
"../../rhino": "rhino",
|
||||
"../../csharp/DOSSIER/dist/dossier.yak": "plugin/dossier.yak",
|
||||
"../../csharp/DOSSIER/dist/dossier-version.txt": "plugin/dossier-version.txt"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
Reference in New Issue
Block a user