From 18d6d98e07fc1f17565c1241c9d6ab201ee10b08 Mon Sep 17 00:00:00 2001 From: karim Date: Sat, 30 May 2026 12:46:53 +0200 Subject: [PATCH] DOSSIER Multi-Phase: C#-Plugin + Yak + Wandstile + UX-Polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- csharp/DOSSIER/.gitignore | 7 + csharp/DOSSIER/Commands/BIM.cs | 94 ++ csharp/DOSSIER/Commands/Tools.cs | 47 + csharp/DOSSIER/Commands/Views.cs | 40 + csharp/DOSSIER/DOSSIER.csproj | 65 + csharp/DOSSIER/DossierPaths.cs | 61 + csharp/DOSSIER/DossierPlugin.cs | 52 + csharp/DOSSIER/DossierPythonCommand.cs | 36 + csharp/DOSSIER/PythonRunner.cs | 79 ++ csharp/DOSSIER/build.sh | 144 ++ launcher/scripts/clean-rhino.sh | 65 + launcher/src-tauri/src/lib.rs | 268 +++- launcher/src-tauri/tauri.conf.json | 4 +- launcher/src/App.jsx | 151 ++ rhino/aliases/cmd/aussparung.py | 7 + rhino/aliases/cmd/dach.py | 7 + rhino/aliases/cmd/decke.py | 7 + rhino/aliases/cmd/dkeys.py | 7 + rhino/aliases/cmd/dwelcome.py | 8 + rhino/aliases/cmd/fenster.py | 7 + rhino/aliases/cmd/pipette.py | 436 ++++++ rhino/aliases/cmd/raum.py | 7 + rhino/aliases/cmd/section.py | 57 + rhino/aliases/cmd/smart_join.py | 157 +++ rhino/aliases/cmd/smart_split.py | 267 ++++ rhino/aliases/cmd/stempel.py | 7 + rhino/aliases/cmd/stuetze.py | 7 + rhino/aliases/cmd/symbol.py | 7 + rhino/aliases/cmd/traeger.py | 7 + rhino/aliases/cmd/treppe.py | 7 + rhino/aliases/cmd/tuer.py | 7 + rhino/aliases/cmd/wand.py | 7 + rhino/aliases/dossier_dispatch.py | 97 ++ rhino/aliases/dossier_view_mode.py | 72 + rhino/aliases/loader.py | 469 +++++++ rhino/aliases/shortcuts_default.json | 66 + rhino/aliases/view/geschoss_down.py | 42 + rhino/aliases/view/geschoss_up.py | 43 + rhino/aliases/view/material.py | 7 + rhino/aliases/view/persp3d.py | 7 + rhino/aliases/view/plan.py | 7 + rhino/begin_cmd_hook.py | 79 ++ rhino/elemente.py | 1239 ++++++++++++----- rhino/oberleiste.py | 8 + rhino/panel_base.py | 13 +- rhino/rhinopanel.py | 104 +- rhino/startup.py | 38 +- rhino/treppe_grips.py | 34 +- rhino/welcome.py | 562 ++++++++ .../b6b68c03-3031-4899-bca2-fe6e425146fc.xml | 561 ++++++++ src/OberleisteApp.jsx | 8 +- src/components/EbenenManager.jsx | 11 +- src/components/ProjectSettingsDialog.jsx | 376 ++++- src/lib/rhinoBridge.js | 1 + 54 files changed, 5575 insertions(+), 398 deletions(-) create mode 100644 csharp/DOSSIER/.gitignore create mode 100644 csharp/DOSSIER/Commands/BIM.cs create mode 100644 csharp/DOSSIER/Commands/Tools.cs create mode 100644 csharp/DOSSIER/Commands/Views.cs create mode 100644 csharp/DOSSIER/DOSSIER.csproj create mode 100644 csharp/DOSSIER/DossierPaths.cs create mode 100644 csharp/DOSSIER/DossierPlugin.cs create mode 100644 csharp/DOSSIER/DossierPythonCommand.cs create mode 100644 csharp/DOSSIER/PythonRunner.cs create mode 100755 csharp/DOSSIER/build.sh create mode 100755 launcher/scripts/clean-rhino.sh create mode 100644 rhino/aliases/cmd/aussparung.py create mode 100644 rhino/aliases/cmd/dach.py create mode 100644 rhino/aliases/cmd/decke.py create mode 100644 rhino/aliases/cmd/dkeys.py create mode 100644 rhino/aliases/cmd/dwelcome.py create mode 100644 rhino/aliases/cmd/fenster.py create mode 100644 rhino/aliases/cmd/pipette.py create mode 100644 rhino/aliases/cmd/raum.py create mode 100644 rhino/aliases/cmd/section.py create mode 100644 rhino/aliases/cmd/smart_join.py create mode 100644 rhino/aliases/cmd/smart_split.py create mode 100644 rhino/aliases/cmd/stempel.py create mode 100644 rhino/aliases/cmd/stuetze.py create mode 100644 rhino/aliases/cmd/symbol.py create mode 100644 rhino/aliases/cmd/traeger.py create mode 100644 rhino/aliases/cmd/treppe.py create mode 100644 rhino/aliases/cmd/tuer.py create mode 100644 rhino/aliases/cmd/wand.py create mode 100644 rhino/aliases/dossier_dispatch.py create mode 100644 rhino/aliases/dossier_view_mode.py create mode 100644 rhino/aliases/loader.py create mode 100644 rhino/aliases/shortcuts_default.json create mode 100644 rhino/aliases/view/geschoss_down.py create mode 100644 rhino/aliases/view/geschoss_up.py create mode 100644 rhino/aliases/view/material.py create mode 100644 rhino/aliases/view/persp3d.py create mode 100644 rhino/aliases/view/plan.py create mode 100644 rhino/begin_cmd_hook.py create mode 100644 rhino/welcome.py create mode 100644 rhino/workspaces/b6b68c03-3031-4899-bca2-fe6e425146fc.xml diff --git a/csharp/DOSSIER/.gitignore b/csharp/DOSSIER/.gitignore new file mode 100644 index 0000000..aced833 --- /dev/null +++ b/csharp/DOSSIER/.gitignore @@ -0,0 +1,7 @@ +bin/ +obj/ +dist/ +*.user +*.suo +.vs/ +.idea/ diff --git a/csharp/DOSSIER/Commands/BIM.cs b/csharp/DOSSIER/Commands/BIM.cs new file mode 100644 index 0000000..5f11203 --- /dev/null +++ b/csharp/DOSSIER/Commands/BIM.cs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Karim Gabriele Varano +// +// BIM-Commands: jeweils Wrapper auf das Python-Script in rhino/aliases/cmd/. +// Naming-Convention: d-Prefix + englischer BIM-Begriff (VisualARQ-Stil). +// Klassen-Guids sind frei generiert (uuidgen) — wichtig nur dass sie +// stabil bleiben, damit Rhino sie ueber Sessions wiedererkennt. +using System.Runtime.InteropServices; + +namespace DOSSIER.Cmd; + +[Guid("9A87B609-719F-468B-AF2A-6E59A9B61062")] +public class DWall : DossierPythonCommand +{ + public override string EnglishName => "dWall"; + protected override string ScriptRelativePath => "cmd/wand.py"; +} + +[Guid("80278984-16B8-485B-8876-3D63806BCA58")] +public class DDoor : DossierPythonCommand +{ + public override string EnglishName => "dDoor"; + protected override string ScriptRelativePath => "cmd/tuer.py"; +} + +[Guid("20D22047-03FA-4CF3-ACF3-3424A109BD91")] +public class DWindow : DossierPythonCommand +{ + public override string EnglishName => "dWindow"; + protected override string ScriptRelativePath => "cmd/fenster.py"; +} + +[Guid("536641ED-93D4-4D49-A028-9F2C4EEE2A24")] +public class DSlab : DossierPythonCommand +{ + public override string EnglishName => "dSlab"; + protected override string ScriptRelativePath => "cmd/decke.py"; +} + +[Guid("CF196C6A-EEAE-478C-8EB0-C69B6F7B9942")] +public class DStair : DossierPythonCommand +{ + public override string EnglishName => "dStair"; + protected override string ScriptRelativePath => "cmd/treppe.py"; +} + +[Guid("B4A19B8B-4056-428B-BA2E-DF69A2A8DA9A")] +public class DColumn : DossierPythonCommand +{ + public override string EnglishName => "dColumn"; + protected override string ScriptRelativePath => "cmd/stuetze.py"; +} + +[Guid("CCDF2D03-1FBD-4BC3-A06E-6D3FEEE575AB")] +public class DBeam : DossierPythonCommand +{ + public override string EnglishName => "dBeam"; + protected override string ScriptRelativePath => "cmd/traeger.py"; +} + +[Guid("6ADF4344-0C05-48D1-BB2A-B330E3057CE4")] +public class DRoom : DossierPythonCommand +{ + public override string EnglishName => "dRoom"; + protected override string ScriptRelativePath => "cmd/raum.py"; +} + +[Guid("A9D471FD-CB75-4C4E-8236-3C8B9A491266")] +public class DSymbol : DossierPythonCommand +{ + public override string EnglishName => "dSymbol"; + protected override string ScriptRelativePath => "cmd/symbol.py"; +} + +[Guid("5388E3A7-B40E-40CE-B958-4A294B1E9F4F")] +public class DTag : DossierPythonCommand +{ + public override string EnglishName => "dTag"; + protected override string ScriptRelativePath => "cmd/stempel.py"; +} + +[Guid("F0A5E3B0-F77E-4316-B521-294979F1E9CA")] +public class DRoof : DossierPythonCommand +{ + public override string EnglishName => "dRoof"; + protected override string ScriptRelativePath => "cmd/dach.py"; +} + +[Guid("404E4389-F8BF-4BAE-A972-60EADB33941C")] +public class DVoid : DossierPythonCommand +{ + public override string EnglishName => "dVoid"; + protected override string ScriptRelativePath => "cmd/aussparung.py"; +} diff --git a/csharp/DOSSIER/Commands/Tools.cs b/csharp/DOSSIER/Commands/Tools.cs new file mode 100644 index 0000000..8657810 --- /dev/null +++ b/csharp/DOSSIER/Commands/Tools.cs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Karim Gabriele Varano +using System.Runtime.InteropServices; + +namespace DOSSIER.Cmd; + +[Guid("07F23908-EF40-4A98-A550-C8D8A1F80A7F")] +public class DJoin : DossierPythonCommand +{ + public override string EnglishName => "dJoin"; + protected override string ScriptRelativePath => "cmd/smart_join.py"; +} + +[Guid("69DBE84C-5E44-4155-84CB-D67329B64830")] +public class DSplit : DossierPythonCommand +{ + public override string EnglishName => "dSplit"; + protected override string ScriptRelativePath => "cmd/smart_split.py"; +} + +[Guid("38E80D26-5270-45C6-B5F3-2E2179545C47")] +public class DPipette : DossierPythonCommand +{ + public override string EnglishName => "dPipette"; + protected override string ScriptRelativePath => "cmd/pipette.py"; +} + +[Guid("F2C8B5A1-9D4E-4F73-B2C6-1A8E7D3F5C42")] +public class DSection : DossierPythonCommand +{ + public override string EnglishName => "dSection"; + protected override string ScriptRelativePath => "cmd/section.py"; +} + +[Guid("66647D04-F324-459F-82B9-0FD82307FA93")] +public class DKeys : DossierPythonCommand +{ + public override string EnglishName => "dKeys"; + protected override string ScriptRelativePath => "cmd/dkeys.py"; +} + +[Guid("93406D93-E9AC-424D-BFBD-3B7A542A85A7")] +public class DWelcome : DossierPythonCommand +{ + public override string EnglishName => "dWelcome"; + protected override string ScriptRelativePath => "cmd/dwelcome.py"; +} diff --git a/csharp/DOSSIER/Commands/Views.cs b/csharp/DOSSIER/Commands/Views.cs new file mode 100644 index 0000000..baa7010 --- /dev/null +++ b/csharp/DOSSIER/Commands/Views.cs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Karim Gabriele Varano +using System.Runtime.InteropServices; + +namespace DOSSIER.Cmd; + +[Guid("4498B184-E064-4049-8B43-873721ECEE71")] +public class DPlan : DossierPythonCommand +{ + public override string EnglishName => "dPlan"; + protected override string ScriptRelativePath => "view/plan.py"; +} + +[Guid("D6089B7C-C513-4A39-A62B-5A5E91764A18")] +public class D3D : DossierPythonCommand +{ + public override string EnglishName => "d3D"; + protected override string ScriptRelativePath => "view/persp3d.py"; +} + +[Guid("BA89B2DE-2301-4E0D-8542-3BDF393BF7A7")] +public class DMaterial : DossierPythonCommand +{ + public override string EnglishName => "dMaterial"; + protected override string ScriptRelativePath => "view/material.py"; +} + +[Guid("A802824C-BC9B-405B-88A4-77125AA7D5A9")] +public class DLevelUp : DossierPythonCommand +{ + public override string EnglishName => "dLevelUp"; + protected override string ScriptRelativePath => "view/geschoss_up.py"; +} + +[Guid("A034FF6F-0BCC-48D7-9AC9-8447D5718D32")] +public class DLevelDown : DossierPythonCommand +{ + public override string EnglishName => "dLevelDown"; + protected override string ScriptRelativePath => "view/geschoss_down.py"; +} diff --git a/csharp/DOSSIER/DOSSIER.csproj b/csharp/DOSSIER/DOSSIER.csproj new file mode 100644 index 0000000..ee7c072 --- /dev/null +++ b/csharp/DOSSIER/DOSSIER.csproj @@ -0,0 +1,65 @@ + + + + net7.0 + DOSSIER + DOSSIER + 0.2.0 + DOSSIER + Karim Gabriele Varano + DOSSIER — Architektur-Studio-Plugin fuer Rhino 8. Bootstrappt beim Plugin-Load die Python-Module (Panels, Aliases, View-Modes, Welcome) und registriert native Commands (dWall, dDoor, dStair, ...) als saubere Wrapper auf die jeweiligen Python-Scripts. + enable + latest + + + .rhp + NU1701;NETSDK1086 + true + + + false + false + + + DOSSIER + Copyright (C) 2026 Karim Gabriele Varano. AGPL-3.0-or-later. + + + + + + + /Applications/Rhino 8.app/Contents/Frameworks/RhCore.framework/Versions/A/Resources/Rhino.Runtime.Code.dll + false + + + + + + + <_Parameter1>Rhino.PlugIns.DescriptionType.Address + <_Parameter1_IsLiteral>true + <_Parameter2>- + + + <_Parameter1>Rhino.PlugIns.DescriptionType.Email + <_Parameter1_IsLiteral>true + <_Parameter2>karim@gabrielevarano.ch + + + <_Parameter1>Rhino.PlugIns.DescriptionType.Organization + <_Parameter1_IsLiteral>true + <_Parameter2>Karim Gabriele Varano + + + <_Parameter1>Rhino.PlugIns.DescriptionType.WebSite + <_Parameter1_IsLiteral>true + <_Parameter2>https://github.com/karimgvarano/DOSSIER + + + <_Parameter1>e8a4d2c1-6b3f-4e89-9c5a-1d2e3f4a5b6c + + + + diff --git a/csharp/DOSSIER/DossierPaths.cs b/csharp/DOSSIER/DossierPaths.cs new file mode 100644 index 0000000..348b673 --- /dev/null +++ b/csharp/DOSSIER/DossierPaths.cs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Karim Gabriele Varano +using System; +using System.IO; + +namespace DOSSIER; + +/// +/// Locator-Pfade fuer das DOSSIER-Repo. Reihenfolge: +/// 1. Env-Var DOSSIER_HOME +/// 2. File ~/.dossier_home (eine Zeile mit dem Pfad) +/// 3. Hardcoded Fallback /Users/karim/STUDIO/DOSSIER (Dev-Setup) +/// +internal static class DossierPaths +{ + private const string FallbackRoot = "/Users/karim/STUDIO/DOSSIER"; + private const string MarkerFile = ".dossier_home"; + + private static string? _cachedRoot; + + public static string? Root + { + get + { + if (_cachedRoot is not null) return _cachedRoot; + _cachedRoot = ResolveRoot(); + return _cachedRoot; + } + } + + public static string RhinoDir => Path.Combine(Root ?? FallbackRoot, "rhino"); + + public static string AliasDir => Path.Combine(RhinoDir, "aliases"); + + public static string CmdDir => Path.Combine(AliasDir, "cmd"); + + public static string ViewDir => Path.Combine(AliasDir, "view"); + + public static string StartupPy => Path.Combine(RhinoDir, "startup.py"); + + private static string? ResolveRoot() + { + var env = Environment.GetEnvironmentVariable("DOSSIER_HOME"); + if (!string.IsNullOrEmpty(env) && Directory.Exists(env)) return env; + + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var marker = Path.Combine(home, MarkerFile); + if (File.Exists(marker)) + { + try + { + var p = File.ReadAllText(marker).Trim(); + if (!string.IsNullOrEmpty(p) && Directory.Exists(p)) return p; + } + catch { /* ignore */ } + } + + if (Directory.Exists(FallbackRoot)) return FallbackRoot; + return null; + } +} diff --git a/csharp/DOSSIER/DossierPlugin.cs b/csharp/DOSSIER/DossierPlugin.cs new file mode 100644 index 0000000..bf910a0 --- /dev/null +++ b/csharp/DOSSIER/DossierPlugin.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Karim Gabriele Varano +using Rhino; +using Rhino.PlugIns; + +namespace DOSSIER; + +/// +/// DOSSIER-Plugin. Drei Aufgaben: +/// 1. Bootstrappt beim Plugin-Load die Python-Module: Panels, Aliases, +/// View-Modes, BeginCommand-Hook, Welcome-Screen (alles ueber rhino/startup.py). +/// 2. Registriert native Rhino-Commands (dWall, dDoor, dStair, ...) die +/// jeweils das passende Python-Script in rhino/aliases/cmd/ ausfuehren. +/// 3. Loest das Echo-/Autocomplete-Problem der frueheren Keyboard-Macros +/// (jetzt zeigt die History "dWall" statt "_-RunPythonScript ..."). +/// +/// Installation: Plugin via _PluginManager → Install... registrieren. Beim +/// naechsten Rhino-Start laeuft DOSSIER automatisch. Kein zusaetzlicher +/// StartupCommands-XML-Eintrag noetig. +/// +public class DossierPlugin : PlugIn +{ + public DossierPlugin() { Instance = this; } + + public static DossierPlugin Instance { get; private set; } = null!; + + /// Plugin bei jedem Rhino-Start automatisch laden — default ist + /// "WhenNeeded" (erst beim ersten Command-Aufruf). Wir brauchen aber + /// AtStartup, damit OnLoad → startup.py-Bootstrap immer feuert. + public override PlugInLoadTime LoadTime => PlugInLoadTime.AtStartup; + + protected override LoadReturnCode OnLoad(ref string errorMessage) + { + var root = DossierPaths.Root; + if (root is null) + { + errorMessage = + "DOSSIER Root nicht gefunden. Setze Env-Var DOSSIER_HOME " + + "auf den DOSSIER-Repo-Ordner (z.B. /Users/karim/STUDIO/DOSSIER) " + + "oder leg ein File ~/.dossier_home an."; + return LoadReturnCode.ErrorShowDialog; + } + RhinoApp.WriteLine($"[DOSSIER] Plugin geladen (root={root})"); + + // Python-Bootstrap deferred auf Idle — OnLoad feuert vor Eto-UI-Init, + // Panels brauchen aber MainWindow + Idle-Event. PythonRunner.RunDeferred + // wartet auf naechstes Idle und ruft dann startup.py auf. + PythonRunner.RunDeferred(DossierPaths.StartupPy, "startup"); + + return LoadReturnCode.Success; + } +} diff --git a/csharp/DOSSIER/DossierPythonCommand.cs b/csharp/DOSSIER/DossierPythonCommand.cs new file mode 100644 index 0000000..066f2a8 --- /dev/null +++ b/csharp/DOSSIER/DossierPythonCommand.cs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Karim Gabriele Varano +using System.IO; +using Rhino; +using Rhino.Commands; + +namespace DOSSIER; + +/// +/// Abstrakte Basis fuer alle DOSSIER-Commands die ein Python-Script +/// ausfuehren. Subklasse setzt nur EnglishName + ScriptRelativePath. +/// +/// Mechanik: Rhino erlaubt kein synchrones Command-in-Command-Nesting fuer +/// _-RunPythonScript. PythonRunner.RunDeferred wartet auf das naechste Idle- +/// Event und versucht dann zuerst die RhinoCode-API (keine Echo) und faellt +/// auf _-RunPythonScript zurueck. Der Outer-Command beendet sauber mit Success, +/// das Python-Script laeuft direkt danach. Ergebnis fuer den User: sauberer +/// Command-Name in der History (z.B. "dWall") statt "_-RunPythonScript ...". +/// +public abstract class DossierPythonCommand : Command +{ + /// z.B. "cmd/wand.py" oder "view/plan.py" — relativ zu rhino/aliases/. + protected abstract string ScriptRelativePath { get; } + + protected override Result RunCommand(RhinoDoc doc, RunMode mode) + { + var scriptPath = Path.Combine(DossierPaths.AliasDir, ScriptRelativePath); + if (!File.Exists(scriptPath)) + { + RhinoApp.WriteLine($"[DOSSIER] FEHLER: Script nicht gefunden: {scriptPath}"); + return Result.Failure; + } + PythonRunner.RunDeferred(scriptPath, EnglishName); + return Result.Success; + } +} diff --git a/csharp/DOSSIER/PythonRunner.cs b/csharp/DOSSIER/PythonRunner.cs new file mode 100644 index 0000000..ad881c1 --- /dev/null +++ b/csharp/DOSSIER/PythonRunner.cs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Karim Gabriele Varano +using System; +using System.IO; +using Rhino; + +namespace DOSSIER; + +/// +/// Geteilter Python-Script-Runner fuer Plugin-Startup (startup.py) und +/// Commands (cmd/*.py). Primaer ueber Rhino.Runtime.Code-API (CPython3), +/// Fallback ueber _-RunPythonScript-Command. +/// +internal static class PythonRunner +{ + /// Fuehrt das Script aus. Versucht RhinoCode-API zuerst, + /// faellt auf _-RunPythonScript zurueck. Labels nur fuer Logs. + public static bool Run(string scriptPath, string label) + { + if (!File.Exists(scriptPath)) + { + RhinoApp.WriteLine($"[DOSSIER] {label}: Script nicht gefunden: {scriptPath}"); + return false; + } + if (TryRunViaRhinoCode(scriptPath, label)) return true; + try + { + RhinoApp.RunScript($"_-RunPythonScript \"{scriptPath}\"", echo: false); + return true; + } + catch (Exception ex) + { + RhinoApp.WriteLine($"[DOSSIER] {label} RunScript: {ex.Message}"); + return false; + } + } + + /// Wie Run, aber defern auf das naechste Idle-Event. + /// Erlaubt safe Invocation aus Plugin-OnLoad oder Command-RunCommand + /// (Rhino mag kein direktes _-RunPythonScript aus diesen Kontexten). + public static void RunDeferred(string scriptPath, string label) + { + EventHandler? handler = null; + handler = (sender, e) => + { + RhinoApp.Idle -= handler; + Run(scriptPath, label); + }; + RhinoApp.Idle += handler; + } + + private static bool TryRunViaRhinoCode(string scriptPath, string label) + { + try + { + var spec = new Rhino.Runtime.Code.Languages.LanguageSpec("*.*.python", "3.*"); + var lang = Rhino.Runtime.Code.RhinoCode.Languages.QueryLatest(spec); + if (lang == null) return false; + + // RhinoCode.CreateCode(text) setzt __file__/sys.path NICHT automatisch + // — die DOSSIER-Scripts erwarten beides. Injizieren vorne rein. + var pathLit = scriptPath.Replace("\\", "/"); + var preamble = + "import sys, os\n" + + $"__file__ = r'{pathLit}'\n" + + "sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n" + + "sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n"; + var code = lang.CreateCode(preamble + File.ReadAllText(scriptPath)); + var ctx = new Rhino.Runtime.Code.Execution.RunContext(); + code.Run(ctx); + return true; + } + catch (Exception ex) + { + RhinoApp.WriteLine($"[DOSSIER] {label} RhinoCode: {ex.Message}"); + return false; + } + } +} diff --git a/csharp/DOSSIER/build.sh b/csharp/DOSSIER/build.sh new file mode 100755 index 0000000..859ad67 --- /dev/null +++ b/csharp/DOSSIER/build.sh @@ -0,0 +1,144 @@ +#!/bin/bash +# DOSSIER — Build-Skript +# Baut das C#-Plugin (.rhp) das beim Rhino-Start die Python-Module +# bootstrappt (Panels, Aliases, Welcome) und Native-Commands registriert +# (dWall, dDoor, dStair, dSlab, ...). +# +# === Voraussetzungen === +# 1. .NET 7 SDK installiert. Auf Mac: +# brew install dotnet@7 +# Oder direkt von Microsoft: +# https://dotnet.microsoft.com/download/dotnet/7.0 +# +# 2. RhinoCommon NuGet-Package wird beim ersten Build automatisch geladen. +# +# === Build === +# ./build.sh — Release-Build, output in bin/Release/net7.0/ +# ./build.sh debug — Debug-Build mit Symbols +# ./build.sh clean — bin/obj loeschen +# ./build.sh install — Build + ins Rhino Plug-In-Verzeichnis kopieren +# +# === Installation in Rhino (einmalig, auf Mac) === +# WICHTIG: Mac Rhino 8 unterstuetzt KEIN Drag-Drop fuer .rhp-Plugins +# (der Drag landet im Datei-Oeffnen-Handler, nicht im Plugin-Loader). +# +# Richtiger Weg: +# 1. Rhino 8 oeffnen +# 2. Command-Prompt: PluginManager +# (oder Tools-Menue → Options → Plug-Ins) +# 3. Button "Install..." → browse zur .rhp +# bin/Release/net7.0/DOSSIER.rhp +# 4. Open → Rhino registriert das Plugin +# 5. Rhino restart — DOSSIER bootstrappt (Panels/Aliases/Welcome) + +# Commands dWall/dDoor/... sind verfuegbar +# +# Pfad bleibt in Rhinos settings-XML registriert. Bei spaeteren Builds +# einfach in den gleichen Output-Pfad bauen — Rhino laedt den neuen Stand +# automatisch beim naechsten Start. + +set -e +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +# --- Param-Parsing --- +MODE="${1:-release}" + +case "$MODE" in + debug|Debug) + CONFIG="Debug" + ;; + release|Release|"") + CONFIG="Release" + ;; + clean) + echo "==> Loesche bin/ + obj/" + rm -rf bin obj + exit 0 + ;; + install) + CONFIG="Release" + DO_INSTALL=1 + ;; + *) + echo "Usage: $0 [release|debug|clean|install]" + exit 1 + ;; +esac + +# --- dotnet-Check --- +if ! command -v dotnet &>/dev/null; then + echo "FEHLER: dotnet nicht installiert." + echo "Install: brew install dotnet@7" + echo " oder https://dotnet.microsoft.com/download/dotnet/7.0" + exit 1 +fi + +# --- Build --- +echo "==> Build: $CONFIG" +dotnet build -c "$CONFIG" + +OUTPUT="$SCRIPT_DIR/bin/$CONFIG/net7.0/DOSSIER.rhp" +if [ ! -f "$OUTPUT" ]; then + echo "FEHLER: Build-Output nicht gefunden: $OUTPUT" + exit 1 +fi +echo "==> .rhp Output: $OUTPUT" + +# --- Yak-Paket bauen --- +# yak (Rhinos Package Manager) ist im Rhino-App-Bundle dabei. Wir packen .rhp + +# manifest.yml in ein .yak-Archiv das der Launcher bundlet + via "yak install" +# in den User-Plugin-Pfad legt. Dort wird's von Rhino aus dem trusted Yak- +# Verzeichnis geladen. +YAK="/Applications/Rhino 8.app/Contents/Resources/bin/yak" +DIST_DIR="$SCRIPT_DIR/dist" +mkdir -p "$DIST_DIR" +if [ -x "$YAK" ]; then + BUILD_DIR="$SCRIPT_DIR/bin/$CONFIG/net7.0" + pushd "$BUILD_DIR" >/dev/null + # yak spec failed mit Exit-1 wenn manifest.yml schon existiert — kein Fehler + "$YAK" spec --input DOSSIER.rhp >/dev/null 2>&1 || true + rm -f dossier-*.yak + YAK_OUT=$("$YAK" build 2>&1 | grep -oE '/.*\.yak$' | head -1) + popd >/dev/null + if [ -n "$YAK_OUT" ] && [ -f "$YAK_OUT" ]; then + # Versionierter Filename rein damit Launcher die Version vom Filename ablesen kann + YAK_NAME=$(basename "$YAK_OUT") + # Alte .yak im dist/ wegraeumen + rm -f "$DIST_DIR"/dossier-*.yak + cp -v "$YAK_OUT" "$DIST_DIR/$YAK_NAME" + # Stabilen Symlink fuer Launcher (immer 'dossier.yak') zusaetzlich + ln -sf "$YAK_NAME" "$DIST_DIR/dossier.yak" + # Version separat als Textdatei (extrahiert aus manifest.yml im .yak) + VERSION=$(grep '^version:' "$BUILD_DIR/manifest.yml" | awk '{print $2}') + echo -n "$VERSION" > "$DIST_DIR/dossier-version.txt" + echo "==> .yak Output: $DIST_DIR/$YAK_NAME (version=$VERSION)" + else + echo "WARN: yak build hat keinen Output produziert" + fi +else + echo "WARN: yak CLI nicht gefunden ($YAK) — kein .yak-Paket gebaut" +fi + +# --- Install: lokales Test-Install via yak --- +# Fuer Dev-Iteration: installiert das frische .yak direkt in den Rhino-User- +# Plugin-Pfad (~/Library/Application Support/McNeel/Rhinoceros/packages/8.0/). +# In Production macht der Launcher das automatisch beim ersten Rhino-Start. +if [ -n "$DO_INSTALL" ]; then + # Alte Manuell-Install-Standorte aufraeumen + OLD_MANUAL="$HOME/Library/Application Support/Dossier/Plugin" + for old in "/Applications/Rhino 8.app/Contents/PlugIns/DOSSIER.rhp" \ + "/Applications/Rhino 8.app/Contents/PlugIns/DossierCommands.rhp" \ + "$OLD_MANUAL/DOSSIER.rhp"; do + if [ -f "$old" ]; then rm -v "$old"; fi + done + if [ -x "$YAK" ] && [ -f "$DIST_DIR/dossier.yak" ]; then + # yak install nimmt Quelle als Verzeichnis (treats local dir as source server) + "$YAK" install dossier --source "$DIST_DIR" 2>&1 | sed 's/^/ /' + echo "==> Yak-Install fertig. Rhino restart noetig (Plugin laedt on-demand beim ersten Command)." + echo "==> StartupCommands-XML-Eintrag wird vom Launcher gesetzt — fuer Dev manuell pruefen:" + echo " Options → General → Run these commands every time a model is opened" + echo " soll enthalten: _-RunPythonScript \"$SCRIPT_DIR/../../rhino/startup.py\"" + fi +fi + +echo "OK." diff --git a/launcher/scripts/clean-rhino.sh b/launcher/scripts/clean-rhino.sh new file mode 100755 index 0000000..42dd19a --- /dev/null +++ b/launcher/scripts/clean-rhino.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# clean-rhino.sh — setzt DOSSIER in Rhino zurueck auf "frisch installiert" Zustand. +# Damit kann das Setup im Launcher (Settings → Setup tab) jederzeit von Null +# durchgespielt werden. +# +# Aufgaben: +# 1. yak uninstall dossier (Plugin raus) +# 2. Window-Layout-Datei loeschen (workspaces/.xml) +# 3. StartupCommands-XML-Eintrag entfernen (Python-Bootstrap-Trigger) +# +# Bleibt unangetastet: +# - dossier_settings.json (User-Praeferenzen, Tags, etc.) +# - launcher recent.json +# - alles ausserhalb DOSSIER + +set -e + +RHINO_APP="/Applications/Rhino 8.app" +YAK="$RHINO_APP/Contents/Resources/bin/yak" +SETTINGS_XML="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml" +WORKSPACES_DIR="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces" +LAYOUT_GUID="b6b68c03-3031-4899-bca2-fe6e425146fc" + +# --- Safety: Rhino muss zu sein --- +if pgrep -f "Rhino 8.app/Contents/MacOS/Rhinoceros$" >/dev/null; then + echo "FEHLER: Rhino laeuft. Bitte beenden und nochmal." + exit 1 +fi + +# --- 1. Yak uninstall (idempotent — meldet 'package not installed' wenn schon weg) --- +echo "==> 1. Yak uninstall dossier" +if [ -x "$YAK" ]; then + "$YAK" uninstall dossier 2>&1 | sed 's/^/ /' || true +else + echo " WARN: yak nicht gefunden — skip" +fi + +# --- 2. Window-Layout-Datei loeschen --- +echo "==> 2. Window-Layout-Datei loeschen" +LAYOUT_FILE="$WORKSPACES_DIR/$LAYOUT_GUID.xml" +if [ -f "$LAYOUT_FILE" ]; then + rm -v "$LAYOUT_FILE" | sed 's/^/ /' +else + echo " schon weg" +fi + +# --- 3. StartupCommands-Eintrag aus XML entfernen --- +echo "==> 3. StartupCommands-Eintrag entfernen" +if [ -f "$SETTINGS_XML" ]; then + # sed: matche genau unsere DOSSIER-Zeile und loesche + # (egal welcher Pfad — solange startup.py drin steht) + if grep -q 'StartupCommands.*startup.py' "$SETTINGS_XML"; then + # macOS sed braucht leeres Backup-Suffix + sed -i '' '/.*startup\.py.*<\/entry>/d' "$SETTINGS_XML" + echo " entfernt" + else + echo " schon weg" + fi +else + echo " WARN: Rhino-settings-XML nicht gefunden" +fi + +echo +echo "Clean fertig. Naechster Schritt:" +echo " → Launcher → Settings → Setup → 'Setup starten'" diff --git a/launcher/src-tauri/src/lib.rs b/launcher/src-tauri/src/lib.rs index 0c9f614..d1b3c36 100644 --- a/launcher/src-tauri/src/lib.rs +++ b/launcher/src-tauri/src/lib.rs @@ -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 { + 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 { + 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 { + 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 "" _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 { + 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, + 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 { + 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 ` 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, diff --git a/launcher/src-tauri/tauri.conf.json b/launcher/src-tauri/tauri.conf.json index 525d490..5fca9c0 100644 --- a/launcher/src-tauri/tauri.conf.json +++ b/launcher/src-tauri/tauri.conf.json @@ -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": { diff --git a/launcher/src/App.jsx b/launcher/src/App.jsx index 6105348..cab5cdb 100644 --- a/launcher/src/App.jsx +++ b/launcher/src/App.jsx @@ -126,6 +126,13 @@ export default function App() { invoke('list_window_layouts').then(setLayouts).catch(() => {}) invoke('read_dossier_settings').then(ds => setActiveLayout(ds?.windowLayout || '')).catch(() => {}) invoke('read_settings').then(s => setTags(s?.tags || [])).catch(() => {}) + // Auto-Open Setup-Dialog wenn DOSSIER nicht initialisiert ist (z.B. nach + // clean-rhino.sh oder auf einem neuen Mac). + invoke('check_dossier_initialized') + .then(st => { + if (!st?.initialized) { setSettingsTab('setup'); setSettingsOpen(true) } + }) + .catch(() => {}) }, []) // File-Meta laden sobald recent sich aendert @@ -750,6 +757,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
Einstellungen
+ setTab('setup')}>Setup setTab('rhino')}>Rhino setTab('view')}>View setTab('ebenen')}>Ebenen @@ -759,6 +767,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
+ {tab === 'setup' && } {tab === 'rhino' && } {tab === 'view' && } {tab === 'ebenen' && } @@ -774,6 +783,148 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) { ) } +function SetupSettings() { + const [running, setRunning] = useState(false) + const [result, setResult] = useState(null) // { steps, overall_ok } + const [error, setError] = useState(null) + const [rhinoBusy, setRhinoBusy] = useState(false) + const [status, setStatus] = useState(null) // { plugin_installed, startup_cmd_set, layout_installed, initialized } + const [rhinoApp, setRhinoApp] = useState('') + const [startupPath, setStartupPath] = useState('') + + // Live-Check: ob Rhino laeuft (Init kann nicht laufen wenn ja) + useEffect(() => { + let cancelled = false + const tick = () => { + invoke('is_rhino_running') + .then(v => { if (!cancelled) setRhinoBusy(!!v) }) + .catch(() => {}) + } + tick() + const id = setInterval(tick, 2000) + return () => { cancelled = true; clearInterval(id) } + }, []) + + // Initialer State-Check + erkannte Rhino-Konfig + const refreshStatus = useCallback(() => { + invoke('check_dossier_initialized').then(setStatus).catch(() => {}) + }, []) + useEffect(() => { + refreshStatus() + invoke('read_settings').then(s => setRhinoApp(s?.rhinoApp || 'Rhinoceros 8')).catch(() => {}) + invoke('get_default_plugin_startup_path').then(setStartupPath).catch(() => {}) + }, [refreshStatus]) + + const runInit = async () => { + setRunning(true); setError(null); setResult(null) + try { + const r = await invoke('dossier_init') + setResult(r) + refreshStatus() + } catch (e) { + setError(typeof e === 'string' ? e : (e?.message || String(e))) + } finally { + setRunning(false) + } + } + + const dot = (ok) => ( + + ) + + return ( +
+

DOSSIER einrichten

+

+ Setzt DOSSIER auf einem frischen Mac komplett auf: installiert das C#-Plugin in Rhino via Yak, + traegt den Python-Bootstrap als Startup-Command ein, und kopiert das DOSSIER-Window-Layout in Rhinos + Workspaces-Folder. Idempotent — kann mehrfach ausgefuehrt werden. +

+ + {/* Erkannte Konfiguration */} +
+
Erkannte Konfiguration:
+
+ Rhino-App: + {rhinoApp || '(nicht gesetzt)'} + startup.py: + {startupPath || '(nicht gefunden)'} +
+
+ (Aendern unter Settings → Rhino) +
+
+ + {/* Aktueller Install-Status (live) */} + {status && ( +
+
Status:
+
{dot(status.plugin_installed)}DOSSIER-Plugin (.rhp) installiert
+
{dot(status.startup_cmd_set)}Python-Bootstrap in Rhino-StartupCommands
+
{dot(status.layout_installed)}Window-Layout in Rhino-Workspaces
+
+ )} + +

+ Hinweis: Rhino muss waehrend des Setups geschlossen sein. +

+ +
+ + {rhinoBusy && ( + + Rhino laeuft — bitte beenden. + + )} +
+ + {result && ( +
    + {result.steps.map(s => ( +
  • + + {s.status === 'ok' ? '✓' : '✗'} + +
    +
    {s.label}
    +
    + {s.detail} +
    +
    +
  • + ))} +
  • + {result.overall_ok + ? '✓ Alle Schritte erfolgreich. Rhino oeffnen — Plugin laedt bei erstem dWall/dDoor/...-Aufruf, startup.py bootstrappt automatisch.' + : '✗ Mindestens ein Schritt ist fehlgeschlagen. Details oben.'} +
  • +
+ )} + + {error && ( +

{error}

+ )} +
+ ) +} + function TabBtn({ active, onClick, children }) { return ( ) : ( - + )} +
+ ) + })} + + ))} + + ) +} + /* LinetypePreview — SVG-Linie mit Strich-Segmenten. segments = [{length,type}] type ∈ Line/Space (manchmal auch Continuous-Ableitungen). Width in px; wir skalieren die Segmente damit das Gesamtmuster in width passt. */ @@ -623,10 +674,13 @@ export default function ProjectSettingsDialog({ }) { const [tab, setTab] = useState('defaults') const [draft, setDraft] = useState(() => ({ - defaults: { ...(initial.defaults || {}) }, - materials: [...(initial.materials || [])], - project: { ...(initial.project || {}) }, + defaults: { ...(initial.defaults || {}) }, + materials: [...(initial.materials || [])], + wandStyles: [...(initial.wandStyles || [])], + project: { ...(initial.project || {}) }, })) + const [selWandStyleIdx, setSelWandStyleIdx] = useState(() => + (initial.wandStyles && initial.wandStyles.length) ? 0 : null) const setProject = (k, v) => setDraft(d => ({ ...d, project: { ...(d.project || {}), [k]: v } })) const [selMat, setSelMat] = useState(() => { @@ -750,6 +804,42 @@ export default function ProjectSettingsDialog({ setSelMat({ kind: 'local', idx: draft.materials.length }) } + // Wand-Stile CRUD — gleiche Pattern wie Materialien + const setWandStyle = (i, patch) => setDraft(d => ({ + ...d, wandStyles: d.wandStyles.map((s, idx) => + idx === i ? { ...s, ...patch } : s), + })) + const delWandStyle = (i) => { + setDraft(d => ({ + ...d, wandStyles: d.wandStyles.filter((_, idx) => idx !== i), + })) + setSelWandStyleIdx(null) + } + const addWandStyle = () => { + const newStyle = { + id: 'style_' + Math.random().toString(36).slice(2, 10), + name: 'Neuer Stil', prio: 500, + dicke: 0.25, referenz: 'mid', + layered: false, material: '', layers: [], + } + setDraft(d => ({ + ...d, wandStyles: [...d.wandStyles, newStyle], + })) + setSelWandStyleIdx(draft.wandStyles.length) + } + const dupWandStyle = (i) => { + setDraft(d => { + const src = d.wandStyles[i] + if (!src) return d + const copy = { ...src, + id: 'style_' + Math.random().toString(36).slice(2, 10), + name: (src.name || 'Stil') + ' (Kopie)', + } + return { ...d, wandStyles: [...d.wandStyles, copy] } + }) + setSelWandStyleIdx(draft.wandStyles.length) + } + const wrapperStyle = embedded ? { width: '100%', height: '100%', background: 'var(--bg-dialog)', @@ -766,17 +856,30 @@ export default function ProjectSettingsDialog({
- + {/* Body: Sidebar links + Inhalt rechts */} +
+ - {/* Body */} + {/* Tab content */}
{tab === 'defaults' && ( @@ -873,6 +976,18 @@ export default function ProjectSettingsDialog({
)} + {tab === 'wandstile' && ( + + )} + {tab === 'materials' && (
)}
+
{/* ← schließt Body-Row (Sidebar + Tab-Content) */} {/* Footer — Pill-Buttons */}
) } + + +// ============================================================================ +// WandStileTab — Liste links + Editor rechts (Pattern wie MaterialDetail). +// styles: Array von Wand-Style-Dicts, sortiert nach prio absteigend angezeigt. +// onChange(idx, patch): partial update des Style-Eintrags an Index idx. +// ============================================================================ +function WandStileTab({ + styles, selectedIdx, onSelect, + materials, + onChange, onAdd, onDelete, onDuplicate, +}) { + const matNames = (materials || []).map(m => m.name).filter(Boolean) + // Sortier-Index fuer Anzeige (nach prio absteigend). Editieren bleibt auf + // dem Original-Index — wir mappen visuell-Index → realer-Index. + const sorted = [...(styles || [])].map((s, i) => ({ s, i })) + .sort((a, b) => (b.s.prio || 0) - (a.s.prio || 0)) + const sel = (selectedIdx != null && styles && styles[selectedIdx]) + ? styles[selectedIdx] : null + return ( +
+ {/* Links: Liste */} +
+
+ {sorted.length === 0 && ( +
+ Noch keine Wandstile.
+ klicken zum Anlegen. +
+ )} + {sorted.map(({ s, i }) => { + const isActive = selectedIdx === i + return ( + + ) + })} +
+
+ + {selectedIdx != null && ( + <> + onDuplicate(selectedIdx)} + title="Duplizieren" /> + onDelete(selectedIdx)} + title="Löschen" /> + + )} +
+
+ {/* Rechts: Editor */} +
+ {!sel && ( +
+ Wandstil links auswählen oder + für neuen. +
+ )} + {sel && ( +
+ + onChange(selectedIdx, { name: v })} /> + onChange(selectedIdx, { id: v })} + hint="Wird in der Wand als UserString gespeichert. Aenderung verlinkt bestehende Waende nicht automatisch um." /> + onChange(selectedIdx, { prio: Math.max(1, Math.min(999, v || 500)) })} + hint="Bei Joints zwischen verschiedenen Stilen gewinnt der mit höherer Prio die Eckverbindung." /> + + + +
+ Aufbau +
+ onChange(selectedIdx, { layered: false })} /> + onChange(selectedIdx, { layered: true })} /> +
+
+ + {!sel.layered && ( + <> +
+ Material + +
+ onChange(selectedIdx, { dicke: v || 0.25 })} + hint="Wird beim Erstellen vorgeschlagen. User kann pro Wand überschreiben." /> +
+ Referenz +
+ {[ + { code: 'mid', label: 'Mittig' }, + { code: 'left', label: 'Links' }, + { code: 'right', label: 'Rechts' }, + ].map(r => ( + onChange(selectedIdx, { referenz: r.code })} /> + ))} +
+
+ + )} + + {sel.layered && ( +
+
+ Schichten (von links nach rechts): +
+ {(sel.layers || []).map((ly, li) => ( +
+ + { + const newLayers = [...(sel.layers || [])] + newLayers[li] = { ...newLayers[li], dicke: parseFloat(e.target.value) || 0 } + onChange(selectedIdx, { layers: newLayers }) + }} + style={{ width: 60, fontSize: 11, padding: '3px 6px', + background: 'var(--bg-section)', + color: 'var(--text-primary)', + border: '1px solid var(--border)', + borderRadius: 3 }} /> + m + { + const newLayers = (sel.layers || []).filter((_, idx) => idx !== li) + onChange(selectedIdx, { layers: newLayers }) + }} title="Schicht löschen" /> +
+ ))} + { + const newLayers = [...(sel.layers || []), + { material: '', dicke: 0.05 }] + onChange(selectedIdx, { layers: newLayers }) + }} /> +
+ Total: {((sel.layers || []).reduce((s, l) => s + (l.dicke || 0), 0)).toFixed(3)} m +
+
+ )} +
+
+ )} +
+
+ ) +} diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index 1db0553..223d662 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -280,6 +280,7 @@ export function setSectionStyle(enabled, source, color, pattern, scale, rotation }) } export function openAbout() { send('OPEN_ABOUT', {}) } +export function openCheatsheet() { send('OPEN_CHEATSHEET', {}) } export function createText() { send('CREATE_TEXT', {}) } export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) } export function applyTextStyle(id) { send('APPLY_TEXT_STYLE', { id }) }