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:
2026-05-30 12:46:53 +02:00
parent 7930705d01
commit 18d6d98e07
54 changed files with 5575 additions and 398 deletions
+7
View File
@@ -0,0 +1,7 @@
bin/
obj/
dist/
*.user
*.suo
.vs/
.idea/
+94
View File
@@ -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";
}
+47
View File
@@ -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";
}
+40
View File
@@ -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";
}
+65
View File
@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<AssemblyName>DOSSIER</AssemblyName>
<RootNamespace>DOSSIER</RootNamespace>
<Version>0.2.0</Version>
<Title>DOSSIER</Title>
<Company>Karim Gabriele Varano</Company>
<Description>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.</Description>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<!-- Rhino-Plugin-Output: .rhp statt .dll -->
<TargetExt>.rhp</TargetExt>
<NoWarn>NU1701;NETSDK1086</NoWarn>
<EnableDynamicLoading>true</EnableDynamicLoading>
<!-- Kein Konflikt mit Rhinos Eto/WPF -->
<UseWindowsForms>false</UseWindowsForms>
<UseWpf>false</UseWpf>
<!-- Plugin-Metadaten (sichtbar im _PluginManager) -->
<AssemblyTitle>DOSSIER</AssemblyTitle>
<Copyright>Copyright (C) 2026 Karim Gabriele Varano. AGPL-3.0-or-later.</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" Version="8.0.23304.9001" IncludeAssets="compile;build" />
<!-- Rhino-CPython3-Runtime — direkt aus dem App-Bundle linken.
Erlaubt RhinoCode-API ohne den Umweg ueber _-RunPythonScript-Command. -->
<Reference Include="Rhino.Runtime.Code">
<HintPath>/Applications/Rhino 8.app/Contents/Frameworks/RhCore.framework/Versions/A/Resources/Rhino.Runtime.Code.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<!-- Plugin-Guid als Assembly-Attribut (Rhino registriert Plugin via dieser ID) -->
<ItemGroup>
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
<_Parameter1>Rhino.PlugIns.DescriptionType.Address</_Parameter1>
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
<_Parameter2>-</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
<_Parameter1>Rhino.PlugIns.DescriptionType.Email</_Parameter1>
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
<_Parameter2>karim@gabrielevarano.ch</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
<_Parameter1>Rhino.PlugIns.DescriptionType.Organization</_Parameter1>
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
<_Parameter2>Karim Gabriele Varano</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
<_Parameter1>Rhino.PlugIns.DescriptionType.WebSite</_Parameter1>
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
<_Parameter2>https://github.com/karimgvarano/DOSSIER</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.InteropServices.GuidAttribute">
<_Parameter1>e8a4d2c1-6b3f-4e89-9c5a-1d2e3f4a5b6c</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
+61
View File
@@ -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;
/// <summary>
/// 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)
/// </summary>
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;
}
}
+52
View File
@@ -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;
/// <summary>
/// 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.
/// </summary>
public class DossierPlugin : PlugIn
{
public DossierPlugin() { Instance = this; }
public static DossierPlugin Instance { get; private set; } = null!;
/// <summary>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.</summary>
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;
}
}
+36
View File
@@ -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;
/// <summary>
/// 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 ...".
/// </summary>
public abstract class DossierPythonCommand : Command
{
/// <summary>z.B. "cmd/wand.py" oder "view/plan.py" — relativ zu rhino/aliases/.</summary>
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;
}
}
+79
View File
@@ -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;
/// <summary>
/// Geteilter Python-Script-Runner fuer Plugin-Startup (startup.py) und
/// Commands (cmd/*.py). Primaer ueber Rhino.Runtime.Code-API (CPython3),
/// Fallback ueber _-RunPythonScript-Command.
/// </summary>
internal static class PythonRunner
{
/// <summary>Fuehrt das Script aus. Versucht RhinoCode-API zuerst,
/// faellt auf _-RunPythonScript zurueck. Labels nur fuer Logs.</summary>
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;
}
}
/// <summary>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).</summary>
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;
}
}
}
+144
View File
@@ -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."
+65
View File
@@ -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/<guid>.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 '' '/<entry key="StartupCommands">.*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'"
+266 -2
View File
@@ -303,9 +303,17 @@ fn splash_owner_marker_path() -> PathBuf {
fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> { fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> {
let settings = load_settings(); let settings = load_settings();
// XML-Edit nur sinnvoll wenn Rhino nicht laeuft (sonst ueberschreibt's // Setup-Schritte nur wenn Rhino NICHT laeuft sonst ueberschreibt Rhino
// beim Beenden) UND der Eintrag fuer den naechsten Start eh schon greift. // unsere XML-Edits beim Beenden, und yak install kann die in-use .rhp
// nicht ersetzen.
if settings.auto_load_plugin && !is_rhino_running() { 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 let startup_path = settings.plugin_startup_path
.clone() .clone()
.unwrap_or_else(default_plugin_startup_path); .unwrap_or_else(default_plugin_startup_path);
@@ -466,11 +474,264 @@ fn ensure_rhino_startup_command(startup_path: &str) -> Result<(), String> {
Ok(()) 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] #[tauri::command]
fn open_rhino(app: tauri::AppHandle, path3dm: String) -> Result<(), String> { fn open_rhino(app: tauri::AppHandle, path3dm: String) -> Result<(), String> {
open_rhino_internal(&app, &path3dm) 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] #[tauri::command]
fn trigger_plugin_load_now() -> Result<(), String> { fn trigger_plugin_load_now() -> Result<(), String> {
// Schreibt den `_RunPythonScript <pfad>` Eintrag in Rhinos Startup-Command- // Schreibt den `_RunPythonScript <pfad>` Eintrag in Rhinos Startup-Command-
@@ -924,6 +1185,9 @@ pub fn run() {
read_project_config, read_project_config,
open_rhino, open_rhino,
trigger_plugin_load_now, trigger_plugin_load_now,
install_rhino_plugin,
dossier_init,
check_dossier_initialized,
get_default_plugin_startup_path, get_default_plugin_startup_path,
show_in_finder, show_in_finder,
is_rhino_running, is_rhino_running,
+3 -1
View File
@@ -56,7 +56,9 @@
}, },
"resources": { "resources": {
"../../dist": "dist", "../../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": { "plugins": {
+151
View File
@@ -126,6 +126,13 @@ export default function App() {
invoke('list_window_layouts').then(setLayouts).catch(() => {}) invoke('list_window_layouts').then(setLayouts).catch(() => {})
invoke('read_dossier_settings').then(ds => setActiveLayout(ds?.windowLayout || '')).catch(() => {}) invoke('read_dossier_settings').then(ds => setActiveLayout(ds?.windowLayout || '')).catch(() => {})
invoke('read_settings').then(s => setTags(s?.tags || [])).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 // File-Meta laden sobald recent sich aendert
@@ -750,6 +757,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
<header style={{ display: 'flex', alignItems: 'center', gap: 16 }}> <header style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span>Einstellungen</span> <span>Einstellungen</span>
<div style={{ display: 'flex', gap: 4, marginLeft: 'auto', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 4, marginLeft: 'auto', flexWrap: 'wrap' }}>
<TabBtn active={tab === 'setup'} onClick={() => setTab('setup')}>Setup</TabBtn>
<TabBtn active={tab === 'rhino'} onClick={() => setTab('rhino')}>Rhino</TabBtn> <TabBtn active={tab === 'rhino'} onClick={() => setTab('rhino')}>Rhino</TabBtn>
<TabBtn active={tab === 'view'} onClick={() => setTab('view')}>View</TabBtn> <TabBtn active={tab === 'view'} onClick={() => setTab('view')}>View</TabBtn>
<TabBtn active={tab === 'ebenen'} onClick={() => setTab('ebenen')}>Ebenen</TabBtn> <TabBtn active={tab === 'ebenen'} onClick={() => setTab('ebenen')}>Ebenen</TabBtn>
@@ -759,6 +767,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
</div> </div>
</header> </header>
<div className="body"> <div className="body">
{tab === 'setup' && <SetupSettings />}
{tab === 'rhino' && <RhinoSettings />} {tab === 'rhino' && <RhinoSettings />}
{tab === 'view' && <ViewSettings />} {tab === 'view' && <ViewSettings />}
{tab === 'ebenen' && <EbenenSchemaSettings />} {tab === 'ebenen' && <EbenenSchemaSettings />}
@@ -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) => (
<span style={{
display: 'inline-block', width: 8, height: 8, borderRadius: 4,
background: ok ? 'var(--accent)' : '#e87b6b', marginRight: 8,
}} />
)
return (
<div>
<h3 style={{ marginTop: 0 }}>DOSSIER einrichten</h3>
<p style={{ fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
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.
</p>
{/* Erkannte Konfiguration */}
<div style={{
marginTop: 14, padding: 10, background: 'rgba(255,255,255,0.04)',
border: '1px solid var(--border)', borderRadius: 6, fontSize: 11,
}}>
<div style={{ color: 'var(--text-muted)', marginBottom: 6 }}>Erkannte Konfiguration:</div>
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 4 }}>
<span style={{ color: 'var(--text-muted)' }}>Rhino-App:</span>
<span>{rhinoApp || '(nicht gesetzt)'}</span>
<span style={{ color: 'var(--text-muted)' }}>startup.py:</span>
<span style={{ wordBreak: 'break-all' }}>{startupPath || '(nicht gefunden)'}</span>
</div>
<div style={{ marginTop: 6, color: 'var(--text-muted)', fontSize: 10 }}>
(Aendern unter Settings Rhino)
</div>
</div>
{/* Aktueller Install-Status (live) */}
{status && (
<div style={{ marginTop: 12, fontSize: 11 }}>
<div style={{ color: 'var(--text-muted)', marginBottom: 4 }}>Status:</div>
<div>{dot(status.plugin_installed)}DOSSIER-Plugin (.rhp) installiert</div>
<div>{dot(status.startup_cmd_set)}Python-Bootstrap in Rhino-StartupCommands</div>
<div>{dot(status.layout_installed)}Window-Layout in Rhino-Workspaces</div>
</div>
)}
<p style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 14 }}>
Hinweis: Rhino muss waehrend des Setups <strong>geschlossen</strong> sein.
</p>
<div style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
<button
className="primary pill"
onClick={runInit}
disabled={running || rhinoBusy}
title={rhinoBusy ? 'Rhino laeuft — bitte beenden' : 'Setup starten'}
>
{running ? 'Setup laeuft…' : 'Setup starten'}
</button>
{rhinoBusy && (
<span style={{ fontSize: 11, color: '#e87b6b' }}>
Rhino laeuft bitte beenden.
</span>
)}
</div>
{result && (
<ul style={{ listStyle: 'none', padding: 0, marginTop: 20, borderTop: '1px solid var(--border)' }}>
{result.steps.map(s => (
<li key={s.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 10,
padding: '10px 0', borderBottom: '1px solid var(--border)' }}>
<span style={{ width: 16, fontSize: 14,
color: s.status === 'ok' ? 'var(--accent)' : '#e87b6b' }}>
{s.status === 'ok' ? '✓' : '✗'}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12 }}>{s.label}</div>
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2,
wordBreak: 'break-all' }}>
{s.detail}
</div>
</div>
</li>
))}
<li style={{ padding: '10px 0', fontSize: 11,
color: result.overall_ok ? 'var(--accent)' : '#e87b6b' }}>
{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.'}
</li>
</ul>
)}
{error && (
<p style={{ color: '#e87b6b', marginTop: 12, fontSize: 12 }}>{error}</p>
)}
</div>
)
}
function TabBtn({ active, onClick, children }) { function TabBtn({ active, onClick, children }) {
return ( return (
<button <button
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'aussparung'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("aussparung")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'dach'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("dach")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'decke'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("decke")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Alias 'dkeys': oeffnet DOSSIER Shortcuts-Cheatsheet
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import welcome
welcome.show_cheatsheet()
+8
View File
@@ -0,0 +1,8 @@
#! python3
# -*- coding: utf-8 -*-
# Alias 'dwelcome': zeigt DOSSIER Welcome-Screen manuell (force-mode,
# ignoriert version-marker + optout)
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import welcome
welcome._show_welcome_now()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'fenster'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("fenster")
+436
View File
@@ -0,0 +1,436 @@
#! python3
# -*- coding: utf-8 -*-
# Pipette / Einstellungen-übernehmen: User klickt ein Source-Objekt, dessen
# Attribute werden zur aktuellen Default-Einstellung gemacht — der naechste
# gezeichnete Curve/Rectangle/etc. erbt sie automatisch.
#
# Was uebernommen wird:
# 1. Layer → wird zum Current Layer
# 2. Color (wenn per-Object Override) → wird Current Object-Color
# 3. Linetype (per-Object) → Current
# 4. PlotWeight (per-Object) → Current
# 5. Fuer DOSSIER-Elemente (wand_axis, treppe_axis, etc.) → spezifische
# UserStrings (Dicke, Modus, Breite, Stufen etc.) werden in sticky
# gespeichert als _last_* → nachste Create-Wand/Treppe etc. nimmt sie.
# 6. Bei Hatch-Quelle → wechselt auf den Curve dahinter (Hatch hat selten
# direkt Sinn als Pipette-Quelle, eher der gefuellte Rahmen).
import scriptcontext as sc
import Rhino
import Rhino.Input.Custom as ric
import Rhino.DocObjects as rdoc
from Rhino.Input import GetResult
# Welche UserStrings pro DOSSIER-Type als sticky _last_* gespeichert werden,
# damit das naechste Create-Cmd sie als Default uebernimmt.
_DOSSIER_INHERIT = {
"wand_axis": [
("dossier_wand_dicke", "wand_dicke"),
("dossier_wand_referenz", "wand_referenz"),
("dossier_wand_modus", "wand_modus"),
],
"treppe_axis": [
("dossier_treppe_breite", "treppe_breite"),
("dossier_treppe_n", "treppe_n"),
("dossier_treppe_referenz", "treppe_referenz"),
("dossier_treppe_modus", "treppe_modus"),
("dossier_treppe_lauf_d", "treppe_lauf_d"),
("dossier_treppe_art", "treppe_art"),
],
"decke_outline": [
("dossier_decke_dicke", "decke_dicke"),
("dossier_decke_modus", "decke_modus"),
],
"dach_outline": [
("dossier_dach_dicke", "dach_dicke"),
("dossier_dach_neigung", "dach_neigung"),
],
"stuetze_point": [
("dossier_trag_profil", "stuetze_profil"),
("dossier_trag_b", "stuetze_b"),
("dossier_trag_h", "stuetze_h"),
],
"traeger_axis": [
("dossier_trag_profil", "traeger_profil"),
("dossier_trag_b", "traeger_b"),
("dossier_trag_h", "traeger_h"),
],
"oeffnung_point": [
("dossier_oeff_breite", "oeff_breite"),
("dossier_oeff_hoehe", "oeff_hoehe"),
],
}
def _save_sticky(key, value):
sc.sticky["elemente_last_" + key] = value
def _find_curve_behind_hatch(doc, hatch_obj):
"""Hatches haben in DOSSIER oft eine zugeordnete Source-Curve (gestaltung
speichert die Curve-ID auf der Hatch via 'ebenen_fill_owner')."""
try:
owner = hatch_obj.Attributes.GetUserString("ebenen_fill_owner") or ""
if owner:
import System
cid = System.Guid(owner)
cobj = doc.Objects.FindId(cid)
if cobj is not None and not cobj.IsDeleted: return cobj
except Exception: pass
return None
def _run():
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
go = ric.GetObject()
go.SetCommandPrompt("Pipette: Quell-Objekt picken (Attribute uebernehmen)")
go.GeometryFilter = (rdoc.ObjectType.Curve
| rdoc.ObjectType.Brep
| rdoc.ObjectType.Hatch
| rdoc.ObjectType.PointSet
| rdoc.ObjectType.Point
| rdoc.ObjectType.Annotation
| rdoc.ObjectType.TextDot)
go.SubObjectSelect = False
if go.Get() != GetResult.Object:
print("[PIPETTE] abgebrochen"); return
src = go.Object(0).Object()
if src is None: return
# Wenn Hatch gepickt, switch zur Source-Curve (gefuelltes Rechteck als
# Pipette-Quelle ist intuitiver als die Hatch selbst)
src_geom_type = type(src.Geometry).__name__
if src_geom_type == "Hatch":
cobj = _find_curve_behind_hatch(doc, src)
if cobj is not None:
src = cobj
print("[PIPETTE] Hatch → zugeordnete Curve verwendet")
sa = src.Attributes
msgs = []
# 1. Layer als Current setzen
try:
if doc.Layers.CurrentLayerIndex != sa.LayerIndex:
doc.Layers.SetCurrentLayerIndex(sa.LayerIndex, True)
try: lname = doc.Layers[sa.LayerIndex].FullPath
except Exception: lname = "idx=" + str(sa.LayerIndex)
msgs.append("Layer={}".format(lname))
except Exception as ex:
print("[PIPETTE] Layer-Set:", ex)
# 2. Color
try:
cs = Rhino.ApplicationSettings.AppearanceSettings
if sa.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromObject
cs.DefaultObjectColor = sa.ObjectColor
msgs.append("Color=obj")
else:
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromLayer
msgs.append("Color=byLayer")
except Exception as ex:
print("[PIPETTE] Color-Set:", ex)
# 3. Linetype + 4. PlotWeight — komplexer, weil Rhino keine direkten
# AppearanceSettings dafuer hat. Wir ueberspringen bewusst, weil der
# Layer-Wechsel die meisten Faelle abdeckt (Linetype + PlotWeight
# kommen typisch ByLayer).
# 5. DOSSIER-spezifische Attrs in sticky uebernehmen
try:
dtype = sa.GetUserString("dossier_element_type") or ""
if dtype in _DOSSIER_INHERIT:
inherited = []
for us_key, sticky_key in _DOSSIER_INHERIT[dtype]:
v = sa.GetUserString(us_key)
if v is None or v == "": continue
# Numerische Werte ggf. konvertieren
if any(k in sticky_key for k in ("dicke", "breite", "hoehe",
"neigung", "lauf_d", "_b", "_h")):
try: v = float(v)
except Exception: pass
elif "n" == sticky_key or sticky_key.endswith("_n"):
try: v = int(float(v))
except Exception: pass
_save_sticky(sticky_key, v)
inherited.append("{}={}".format(sticky_key, v))
if inherited:
msgs.append("DOSSIER " + dtype + ": " + ", ".join(inherited))
except Exception as ex:
print("[PIPETTE] DOSSIER-Inherit:", ex)
if msgs:
print("[PIPETTE] Uebernommen: " + " | ".join(msgs))
else:
print("[PIPETTE] Keine Aenderung (Source identisch zu Defaults)")
# 7. Per-Object Custom-Hatch / Custom-Attrs: speichern als "pending"
# + one-shot Listener auf AddRhinoObject — wenn naechster Curve
# gezeichnet ist, alle Custom-Attrs auf den uebertragen.
_setup_pending_apply(doc, src)
# 6. Auto-Chain: passendes Draw-Command starten basierend auf
# Source-Typ. So hat der User direkt "die richtige Tool in der Hand".
_auto_chain(doc, src)
def _capture_source_hatch_props(doc, src_obj):
"""Wenn Source einen per-Object Custom-Hatch hat, sample dessen
Properties (Pattern/Scale/Rotation/Color)."""
try:
sa = src_obj.Attributes
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
if not fill_hid: return None
import System
hid = System.Guid(fill_hid)
hobj = doc.Objects.FindId(hid)
if hobj is None or hobj.IsDeleted: return None
hg = hobj.Geometry
ha = hobj.Attributes
if not hasattr(hg, "PatternIndex"): return None
return {
"pattern_idx": int(hg.PatternIndex),
"scale": float(hg.PatternScale),
"rotation": float(hg.PatternRotation),
"layer_idx": int(ha.LayerIndex),
"color_source": int(ha.ColorSource),
"color_argb": int(ha.ObjectColor.ToArgb()),
"plot_color_source": int(ha.PlotColorSource),
"plot_color_argb": int(ha.PlotColor.ToArgb()),
"linetype_source": int(ha.LinetypeSource),
"linetype_idx": int(ha.LinetypeIndex),
}
except Exception as ex:
print("[PIPETTE] capture-hatch:", ex)
return None
def _setup_pending_apply(doc, src_obj):
"""Speichert Source-Custom-Attrs in sticky + registriert one-shot
AddRhinoObject-Listener der die Attrs (inkl. Hatch) auf den naechsten
neuen Curve uebertraegt. Nach Apply wird Listener wieder entfernt."""
sa = src_obj.Attributes
# Custom-User-Strings sammeln (DOSSIER-Element-Typen + andere). Skip
# die Fill-Tracking-Keys weil wir den Hatch neu erstellen mit neuer ID.
skip_keys = {
"ebenen_fill_hatch_id", # zeigt auf alte Source-Hatch-ID
"ebenen_fill_owner",
}
user_strings = {}
try:
for k in sa.GetUserStringKeys():
if k in skip_keys: continue
v = sa.GetUserString(k)
if v is not None: user_strings[k] = v
except Exception as ex:
print("[PIPETTE] user-strings:", ex)
# Source-Geometrie Closed-State erfassen — wenn Source closed war,
# erzwingen wir nach dem Add auch auf der Kopie ein Close (Polyline
# bleibt sonst standardmaessig offen, hatten User-Feedback dazu).
src_closed = False
try:
import Rhino.Geometry as _rg
sg = src_obj.Geometry
if isinstance(sg, _rg.Curve) and sg.IsClosed:
src_closed = True
except Exception: pass
pending = {
"linetype_source": int(sa.LinetypeSource),
"linetype_idx": int(sa.LinetypeIndex),
"plot_weight_source": int(sa.PlotWeightSource),
"plot_weight": float(sa.PlotWeight),
"user_strings": user_strings,
"hatch_props": _capture_source_hatch_props(doc, src_obj),
"src_closed": src_closed,
}
sc.sticky["dossier_pipette_pending"] = pending
# One-shot handler — applied beim naechsten AddRhinoObject + entfernt sich
def _on_add(sender, e):
try:
obj = e.TheObject
if obj is None or obj.IsDeleted: return
import Rhino.Geometry as rg2
if not isinstance(obj.Geometry, rg2.Curve): return
_apply_pending(doc, obj, pending)
except Exception as ex:
print("[PIPETTE] one-shot apply:", ex)
finally:
try: Rhino.RhinoDoc.AddRhinoObject -= _on_add
except Exception: pass
sc.sticky.pop("dossier_pipette_pending", None)
try:
Rhino.RhinoDoc.AddRhinoObject += _on_add
except Exception as ex:
print("[PIPETTE] listener-install:", ex)
def _force_close_curve(crv):
"""Schliesst eine offene Polyline durch Anhaengen des Startpunkts.
Generische Curves: MakeClosed (nur wenn Endpunkte nahe) oder Join mit
Lueckensegment. Returns geschlossene Curve oder None bei Fehler."""
import Rhino.Geometry as rg2
if crv is None or crv.IsClosed: return None
try:
if isinstance(crv, rg2.PolylineCurve):
ok, pl = crv.TryGetPolyline()
if ok and pl is not None and pl.Count >= 2:
if pl[0].DistanceTo(pl[pl.Count - 1]) > 1e-9:
pl.Add(pl[0])
return rg2.PolylineCurve(pl)
return None
# Generic: erst MakeClosed (closed wenn Endpunkte innerhalb tol)
try:
if crv.MakeClosed(1e-6): return crv
except Exception: pass
# Fallback: Lueckensegment einfuegen + joinen
line = rg2.LineCurve(crv.PointAtEnd, crv.PointAtStart)
joined = rg2.Curve.JoinCurves([crv, line], 1e-6)
if joined and len(joined) > 0 and joined[0].IsClosed:
return joined[0]
except Exception as ex:
print("[PIPETTE] force-close:", ex)
return None
def _apply_pending(doc, new_obj, pending):
"""Wendet pending state auf das neu erzeugte Objekt an."""
import Rhino.Geometry as rg2
import System
# Close-Erzwingen wenn Source geschlossen war — Polyline-Command erzeugt
# standardmaessig offene Curves; Pipette soll den Closed-State erhalten.
if pending.get("src_closed"):
try:
crv = new_obj.Geometry
if isinstance(crv, rg2.Curve) and not crv.IsClosed:
closed = _force_close_curve(crv)
if closed is not None:
if doc.Objects.Replace(new_obj.Id, closed):
ref = doc.Objects.FindId(new_obj.Id)
if ref is not None: new_obj = ref
print("[PIPETTE] Polyline auto-geschlossen (Source war closed)")
except Exception as ex:
print("[PIPETTE] close-replace:", ex)
# Linetype + PlotWeight overrides
try:
na = new_obj.Attributes.Duplicate()
if pending["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
na.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
na.LinetypeIndex = pending["linetype_idx"]
if pending["plot_weight_source"] == int(rdoc.ObjectPlotWeightSource.PlotWeightFromObject):
na.PlotWeightSource = rdoc.ObjectPlotWeightSource.PlotWeightFromObject
na.PlotWeight = pending["plot_weight"]
# UserStrings 1:1 kopieren
for k, v in pending["user_strings"].items():
try: na.SetUserString(k, v)
except Exception: pass
doc.Objects.ModifyAttributes(new_obj, na, True)
except Exception as ex:
print("[PIPETTE] apply-attrs:", ex)
# Per-Object Custom-Hatch: nachbauen wenn Source einen hatte UND
# der neue Curve closed ist
hp = pending.get("hatch_props")
if hp is None: return
try:
crv = new_obj.Geometry
if not isinstance(crv, rg2.Curve) or not crv.IsClosed: return
tol = doc.ModelAbsoluteTolerance
hatches = rg2.Hatch.Create(crv, hp["pattern_idx"],
hp["rotation"], hp["scale"], tol)
if not hatches or len(hatches) == 0: return
ha = rdoc.ObjectAttributes()
ha.LayerIndex = hp["layer_idx"]
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
try:
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
except Exception: pass
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
ha.LinetypeIndex = hp["linetype_idx"]
ha.SetUserString("ebenen_fill_source", "object")
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
new_hid = doc.Objects.AddHatch(hatches[0], ha)
if new_hid and new_hid != System.Guid.Empty:
# Cross-Link: Curve speichert Hatch-ID
ca = new_obj.Attributes.Duplicate()
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
ca.SetUserString("ebenen_fill_source", "object")
doc.Objects.ModifyAttributes(new_obj, ca, True)
print("[PIPETTE] Per-Object Hatch uebernommen (Pattern={}, Scale={})"
.format(hp["pattern_idx"], hp["scale"]))
except Exception as ex:
print("[PIPETTE] hatch-replicate:", ex)
def _auto_chain(doc, src):
"""Startet das passende Draw-Command basierend auf Source-Typ."""
sa = src.Attributes
dtype = sa.GetUserString("dossier_element_type") or ""
geom = src.Geometry
geom_type = type(geom).__name__
# DOSSIER-BIM: triggere den Dispatcher
_DOSSIER_DRAW = {
"wand_axis": "wand",
"treppe_axis": "treppe",
"decke_outline": "decke",
"dach_outline": "dach",
"stuetze_point": "stuetze",
"traeger_axis": "traeger",
"oeffnung_point": None, # braucht parent-Wand-Kontext → skip auto-chain
"raum_outline": "raum",
}
if dtype in _DOSSIER_DRAW:
action = _DOSSIER_DRAW[dtype]
if action:
import os
_here = os.path.dirname(os.path.abspath(__file__))
wrapper = os.path.join(_here, action + ".py")
if os.path.exists(wrapper):
Rhino.RhinoApp.RunScript(
'_-RunPythonScript "{}"'.format(wrapper), False)
print("[PIPETTE] → starte DOSSIER {}".format(action))
return
# Standard-Rhino-Curves: detect Typ → entsprechendes Draw-Cmd
cmd = None
if geom_type == "LineCurve":
cmd = "_Line"
elif geom_type == "ArcCurve":
# ArcCurve mit voller Sweep = Kreis
try:
if geom.IsClosed: cmd = "_Circle"
else: cmd = "_Arc"
except Exception:
cmd = "_Arc"
elif geom_type == "PolylineCurve":
try:
ok, pl = geom.TryGetPolyline()
if ok and pl is not None and pl.IsClosed and pl.Count == 5:
# Geschlossen + 4 Segmente → vermutlich Rectangle
cmd = "_Rectangle"
else:
cmd = "_Polyline"
except Exception:
cmd = "_Polyline"
elif geom_type == "NurbsCurve":
cmd = "_Curve"
elif geom_type == "TextEntity":
cmd = "_Text"
if cmd:
Rhino.RhinoApp.RunScript(cmd, False)
print("[PIPETTE] → starte {}".format(cmd))
_run()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'raum'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("raum")
+57
View File
@@ -0,0 +1,57 @@
#! python3
# -*- coding: utf-8 -*-
# Wrapper fuer dSection: interaktiver Schnitt-Pick (2 Punkte + Blickrichtung).
# Defaults kommen aus Project-Settings.defaults; nach erfolgreicher
# Erstellung wird der neue Schnitt als aktive Zeichnungs-Ebene gesetzt.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import Rhino
import scriptcontext as sc
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
print("[SECTION] kein aktives Dokument")
else:
try:
import schnitte
# Defaults aus Project-Settings; Fallback auf hartkodierte Werte.
defaults = {
"depthBack": 8.0, "heightMin": -1.0, "heightMax": 12.0,
"cutAtLine": True, "namePrefix": "S",
}
try:
import rhinopanel
ps = rhinopanel.load_project_settings(doc)
d = (ps or {}).get("defaults", {})
defaults["depthBack"] = float(d.get("schnittDepthBack", 8.0))
defaults["heightMin"] = float(d.get("schnittHeightMin", -1.0))
defaults["heightMax"] = float(d.get("schnittHeightMax", 12.0))
except Exception as ex:
print("[SECTION] defaults from project-settings:", ex)
sid = schnitte.pick_schnitt_interactive(doc, defaults=defaults)
if not sid:
print("[SECTION] abgebrochen")
else:
# Broadcast neue Zeichnungs-Ebene an Panels + auto-aktivieren
try:
eb = sc.sticky.get("ebenen_bridge")
if eb is not None:
eb._send_state()
except Exception as ex:
print("[SECTION] broadcast:", ex)
try:
import json
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
z_list = json.loads(zraw)
new_z = next((x for x in z_list
if isinstance(x, dict) and x.get("id") == sid), None)
if new_z is not None:
eb = sc.sticky.get("ebenen_bridge")
if eb is not None:
eb._set_active_zeichnungsebene(new_z)
print("[SECTION] erstellt: {}".format(sid))
except Exception as ex:
print("[SECTION] auto-activate:", ex)
except Exception as ex:
print("[SECTION] error:", ex)
+157
View File
@@ -0,0 +1,157 @@
#! python3
# -*- coding: utf-8 -*-
# Smart-Join: bei geschlossenen Curves → BooleanUnion (innere Linien weg),
# bei offenen Curves → normales _Join (Endpunkt-Verbindung).
# Sicherheits-Filter:
# A) Group by Layer + Object-Overrides (Color/Linetype/PlotWeight) + Fill —
# nur Curves mit IDENTISCHEN visuellen Attributen werden gemerged.
# C) Pre-Check Overlap — BooleanUnion liefert genauso viele Outputs wie
# Inputs wenn nichts overlapt → dann KEINE Aktion, Curves bleiben.
# Kombinierter Effekt: nur visuell zusammengehoerige UND tatsaechlich
# ueberlappende Curves werden zu einer Outline vereint.
import scriptcontext as sc
import Rhino
import Rhino.Geometry as rg
import Rhino.DocObjects as rdoc
def _attr_key(obj):
"""Tuple das definiert ob 2 Curves visuell identisch sind. Layer +
Per-Object-Overrides (alles was ByObject nicht ByLayer ist) + Fill-
State (Hatch-ID + No-Fill-Flag)."""
a = obj.Attributes
layer_idx = a.LayerIndex
# Color: nur Object-Override unterscheidend, ByLayer ist gleich.
col_key = ("layer",)
try:
if a.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
col_key = ("obj", a.ObjectColor.ToArgb())
except Exception: pass
# Linetype
lt_key = ("layer",)
try:
if a.LinetypeSource == rdoc.ObjectLinetypeSource.LinetypeFromObject:
lt_key = ("obj", a.LinetypeIndex)
except Exception: pass
# PlotWeight
pw_key = ("layer",)
try:
if a.PlotWeightSource == rdoc.ObjectPlotWeightSource.PlotWeightFromObject:
pw_key = ("obj", float(a.PlotWeight))
except Exception: pass
# Fill / Hatch via gestaltung-UserStrings
fill_hatch = ""
fill_source = ""
no_fill = ""
try:
fill_hatch = a.GetUserString("ebenen_fill_hatch_id") or ""
fill_source = a.GetUserString("ebenen_fill_source") or ""
no_fill = a.GetUserString("ebenen_no_fill") or ""
except Exception: pass
# Fuer Gruppierung zaehlt: "hatte Fill ja/nein" + Quelle + No-Fill-Flag.
fill_key = (bool(fill_hatch), fill_source, no_fill)
return (layer_idx, col_key, lt_key, pw_key, fill_key)
def _run():
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
sel = list(doc.Objects.GetSelectedObjects(False, False))
if not sel:
Rhino.RhinoApp.RunScript("_Join", False); return
# Curves nach Closed/Open trennen
closed_objs = []
has_non_closed = False
for obj in sel:
g = obj.Geometry
if isinstance(g, rg.Curve) and g.IsClosed:
closed_objs.append(obj)
else:
has_non_closed = True
# Wenn nicht ALLE closed sind → einfach Standard-Join
if has_non_closed or len(closed_objs) < 2:
Rhino.RhinoApp.RunScript("_Join", False); return
# Gruppieren nach (Layer + Attrs + Fill)
groups = {} # key → [obj, obj, ...]
for obj in closed_objs:
try:
k = _attr_key(obj)
except Exception:
k = ("ungroup", id(obj))
groups.setdefault(k, []).append(obj)
# gestaltung fuer Fill-Re-Apply
_g = None
try:
import gestaltung as _gmod; _g = _gmod
except Exception as iex:
print("[SMART-JOIN] gestaltung import:", iex)
tol = doc.ModelAbsoluteTolerance
ur = doc.BeginUndoRecord("DOSSIER Smart-Join (gruppiert)")
n_merged_total = 0
n_groups_ops = 0
try:
for key, objs in groups.items():
if len(objs) < 2: continue # einzelne Curve → nichts zu mergen
try:
curves = [o.Geometry for o in objs]
result = rg.Curve.CreateBooleanUnion(curves, tol)
except Exception as ex:
print("[SMART-JOIN] BooleanUnion in Gruppe fehlgeschlagen:", ex)
continue
if not result: continue
# C) Pre-Check Overlap: wenn result-Anzahl gleich input-Anzahl
# ist, gab's keinen tatsaechlichen Overlap → Gruppe nicht
# anfassen.
if len(result) >= len(objs):
continue
# Tatsaechlich gemerged → replace
attrs_template = objs[0].Attributes.Duplicate()
# Fill-Key clearen damit _apply_ebene_fill nicht "schon gefuellt"
# zurueckgibt
try:
attrs_template.SetUserString("ebenen_fill_hatch_id", "")
except Exception: pass
any_had_fill = bool(key[4][0]) # fill_key[0] = had-fill bool
new_ids = []
for crv in result:
nid = doc.Objects.AddCurve(crv, attrs_template)
if nid: new_ids.append(nid)
for o in objs:
try: doc.Objects.Delete(o.Id, True)
except Exception: pass
# Fill nachziehen wenn Inputs welche hatten
if any_had_fill and _g is not None:
for nid in new_ids:
try:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_g._apply_ebene_fill(doc, nobj)
except Exception as fex:
print("[SMART-JOIN] fill-apply:", fex)
n_merged_total += (len(objs) - len(result))
n_groups_ops += 1
finally:
doc.EndUndoRecord(ur)
if n_groups_ops == 0:
print("[SMART-JOIN] Nichts zu mergen — keine Curves overlappen "
"(oder verschiedene Attribute/Layer)")
else:
doc.Views.Redraw()
print("[SMART-JOIN] {} Gruppe(n) bearbeitet, {} Curve(s) zu Union vereint"
.format(n_groups_ops, n_merged_total))
_run()
+267
View File
@@ -0,0 +1,267 @@
#! python3
# -*- coding: utf-8 -*-
# Smart-Split: User zeichnet eine Splitlinie/Polylinie waehrend des Befehls
# (mehrere Klicks, Enter beendet die Eingabe). Alle Curves die die Linie
# schneidet werden gesplittet.
# - Offene Curves: bei den Schnittpunkten in offene Segmente.
# - GESCHLOSSENE Curves: in mehrere CLOSED Sub-Regionen via
# Curve.CreateBooleanRegions (funktioniert auch bei multi-segment
# Polylinien-Cuttern). Per-Object-Hatch wird auf alle Regionen repliziert.
# DOSSIER-Source-Typen (Wand-Achse etc.) bleiben geschuetzt.
import scriptcontext as sc
import Rhino
import Rhino.Input.Custom as ric
import Rhino.Geometry as rg
import Rhino.DocObjects as rdoc
from Rhino.Input import GetResult
# Was Smart-Split NIE anfasst:
# - oeffnung_point / stuetze_point: Punkte, nicht teilbar
# - schnitt_axis: Schnitt-Linien sollen bleiben, sonst kaputte Schnitte
# - treppe_axis: Treppen-State (Lauflinie, Schrittmass-Lock, Wendel-Sweep)
# waere bei einem Split inkonsistent
# Alles andere (wand/traeger/decke/dach/raum/aussparung) DARF gesplittet werden:
# der Add-Listener in elemente.py erkennt die Duplikat-IDs der neuen Stuecke
# und vergibt jedem Stueck ein frisches Element-ID + Regen → BIM-Volumen
# baut sich pro neuem Stueck neu auf.
_PROTECTED_TYPES = {
"treppe_axis",
"oeffnung_point", "stuetze_point", "schnitt_axis",
}
def _capture_hatch_props(doc, src_obj):
try:
sa = src_obj.Attributes
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
if not fill_hid: return None
import System
hid = System.Guid(fill_hid)
hobj = doc.Objects.FindId(hid)
if hobj is None or hobj.IsDeleted: return None
hg = hobj.Geometry
ha = hobj.Attributes
if not hasattr(hg, "PatternIndex"): return None
return {
"pattern_idx": int(hg.PatternIndex),
"scale": float(hg.PatternScale),
"rotation": float(hg.PatternRotation),
"layer_idx": int(ha.LayerIndex),
"color_source": int(ha.ColorSource),
"color_argb": int(ha.ObjectColor.ToArgb()),
"plot_color_source": int(ha.PlotColorSource),
"plot_color_argb": int(ha.PlotColor.ToArgb()),
"linetype_source": int(ha.LinetypeSource),
"linetype_idx": int(ha.LinetypeIndex),
"fill_source": sa.GetUserString("ebenen_fill_source") or "object",
}
except Exception as ex:
print("[SMART-SPLIT] capture-hatch:", ex)
return None
def _replicate_hatch(doc, new_obj, hp):
if hp is None: return
import System
try:
crv = new_obj.Geometry
if not isinstance(crv, rg.Curve) or not crv.IsClosed: return
tol = doc.ModelAbsoluteTolerance
hatches = rg.Hatch.Create(crv, hp["pattern_idx"], hp["rotation"],
hp["scale"], tol)
if not hatches or len(hatches) == 0: return
ha = rdoc.ObjectAttributes()
ha.LayerIndex = hp["layer_idx"]
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
try:
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
except Exception: pass
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
ha.LinetypeIndex = hp["linetype_idx"]
ha.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
new_hid = doc.Objects.AddHatch(hatches[0], ha)
if new_hid and new_hid != System.Guid.Empty:
ca = new_obj.Attributes.Duplicate()
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
ca.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
doc.Objects.ModifyAttributes(new_obj, ca, True)
except Exception as ex:
print("[SMART-SPLIT] hatch-replicate:", ex)
def _collect_polyline_cutter(prompt_first, prompt_more):
"""Sammelt n Punkte fuer den Cutter. Enter beendet (min. 2 Punkte).
ESC bricht ab. Returnt Polyline oder None."""
pts = []
while True:
gp = ric.GetPoint()
if not pts:
gp.SetCommandPrompt(prompt_first)
else:
gp.SetCommandPrompt(prompt_more + " (Enter zum Splitten, ESC = abbrechen)")
gp.SetBasePoint(pts[-1], True)
gp.DrawLineFromPoint(pts[-1], True)
gp.AcceptNothing(True)
res = gp.Get()
if res == GetResult.Nothing:
# Enter gedrueckt
if len(pts) >= 2: return rg.Polyline(pts)
print("[SMART-SPLIT] Mindestens 2 Punkte noetig"); return None
if res != GetResult.Point: return None
pts.append(gp.Point())
def _split_closed_with_cutter(closed_crv, cutter_crv, doc):
"""Splittet closed curve mit beliebigem cutter (Linie oder Polylinie) in
closed Sub-Regionen via Curve.CreateBooleanRegions."""
tol = doc.ModelAbsoluteTolerance
try:
# WorldXY-Plane als Default (DOSSIER ist 2D Plan-Workflow)
plane = rg.Plane.WorldXY
regions = rg.Curve.CreateBooleanRegions(
[closed_crv, cutter_crv], plane, False, tol)
if regions is None or regions.RegionCount == 0:
return None
out = []
for i in range(regions.RegionCount):
rcurves = list(regions.RegionCurves(i))
if not rcurves: continue
if len(rcurves) == 1:
if rcurves[0].IsClosed:
out.append(rcurves[0])
else:
# einzelne offene curve — sollte nicht passieren bei
# Boolean-Regions, aber defensiv
joined = rg.Curve.JoinCurves([rcurves[0]], tol)
if joined and len(joined) > 0 and joined[0].IsClosed:
out.append(joined[0])
else:
joined = rg.Curve.JoinCurves(rcurves, tol)
if joined:
for j in joined:
if j.IsClosed: out.append(j)
return out if out else None
except Exception as ex:
print("[SMART-SPLIT] closed-split:", ex)
return None
def _run():
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
# Polylinie als Cutter sammeln
poly = _collect_polyline_cutter(
"Splitlinie Startpunkt",
"Naechster Punkt")
if poly is None or poly.Count < 2:
return
cutter = rg.PolylineCurve(poly)
tol = doc.ModelAbsoluteTolerance
pre_sel = [o for o in doc.Objects.GetSelectedObjects(False, False)
if o is not None and not o.IsDeleted]
if pre_sel:
source = pre_sel
mode_label = "selektierte ({})".format(len(pre_sel))
else:
s = rdoc.ObjectEnumeratorSettings()
s.HiddenObjects = False; s.LockedObjects = False
source = list(doc.Objects.GetObjectList(s))
mode_label = "alle sichtbaren"
candidates_open = []
candidates_closed = []
for obj in source:
if obj is None or obj.IsDeleted: continue
try:
t = obj.Attributes.GetUserString("dossier_element_type") or ""
if t in _PROTECTED_TYPES: continue
except Exception: pass
g = obj.Geometry
if not isinstance(g, rg.Curve): continue
try:
ints = rg.Intersect.Intersection.CurveCurve(cutter, g, tol, tol)
except Exception:
continue
if not ints or ints.Count == 0: continue
if g.IsClosed:
candidates_closed.append((obj, g))
else:
params = []
for i in range(ints.Count):
ev = ints[i]
if ev.IsPoint:
params.append(ev.ParameterB)
else:
params.append(ev.ParameterB); params.append(ev.ParameterB2)
if params:
params = sorted(set(round(p, 6) for p in params))
candidates_open.append((obj, g, params))
if not candidates_open and not candidates_closed:
print("[SMART-SPLIT] Cutter schneidet nichts ({})".format(mode_label))
return
ur = doc.BeginUndoRecord("DOSSIER Smart-Split")
n_open = 0; n_closed = 0
try:
# Closed: Boolean-Regions → CLOSED Sub-Regionen + Fill replicate
for obj, crv in candidates_closed:
try:
regions = _split_closed_with_cutter(crv, cutter, doc)
if not regions or len(regions) <= 1: continue
hatch_props = _capture_hatch_props(doc, obj)
attrs = obj.Attributes.Duplicate()
try: attrs.SetUserString("ebenen_fill_hatch_id", "")
except Exception: pass
new_ids = []
for r in regions:
nid = doc.Objects.AddCurve(r, attrs)
if nid: new_ids.append(nid)
doc.Objects.Delete(obj.Id, True)
if hatch_props is not None:
for nid in new_ids:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_replicate_hatch(doc, nobj, hatch_props)
else:
try:
import gestaltung as _gmod
for nid in new_ids:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_gmod._apply_ebene_fill(doc, nobj)
except Exception: pass
n_closed += 1
except Exception as ex:
print("[SMART-SPLIT] closed-fail:", ex)
# Open: split bei Params
for obj, crv, params in candidates_open:
try:
pieces = crv.Split(params)
if not pieces or len(pieces) <= 1: continue
attrs = obj.Attributes.Duplicate()
for p in pieces:
doc.Objects.AddCurve(p, attrs)
doc.Objects.Delete(obj.Id, True)
n_open += 1
except Exception as ex:
print("[SMART-SPLIT] open-fail:", ex)
finally:
doc.EndUndoRecord(ur)
doc.Views.Redraw()
print("[SMART-SPLIT] {} closed-Regionen + {} offene Curves gesplittet "
"({} Cutter-Punkte, {})"
.format(n_closed, n_open, poly.Count, mode_label))
_run()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'stempel'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("stempel")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'stuetze'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("stuetze")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'symbol'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("symbol")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'traeger'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("traeger")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'treppe'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("treppe")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'tuer'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("tuer")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'wand'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("wand")
+97
View File
@@ -0,0 +1,97 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
dossier_dispatch.py
Universal-Wrapper fuer DOSSIER-Bridge-Commands via Rhino-Alias.
Aufruf vom Alias:
_-RunPythonScript "/.../dossier_dispatch.py" <action>
oder via Rhino.Input.RhinoGet — wir lesen den letzten String-Parameter
aus der Command-Line.
Aktionen mappen auf ElementeBridge._cmd_create_* via einer kleinen
Dispatch-Tabelle. Bridge-Referenz wird in sc.sticky vom panel_factory
abgelegt (siehe elemente.py _bridge_factory).
"""
import sys
import scriptcontext as sc
_ACTIONS = {
"wand": ("_cmd_create_wall", ()),
"tuer": ("_cmd_create_oeffnung", ("tuer",)),
"fenster": ("_cmd_create_oeffnung", ("fenster",)),
"decke": ("_cmd_create_decke", ()),
"aussparung":("_cmd_create_aussparung",()),
"dach": ("_cmd_create_dach", ()),
"treppe": ("_cmd_create_treppe", ()),
"stuetze": ("_cmd_create_stuetze", ()),
"traeger": ("_cmd_create_traeger", ()),
"raum": ("_cmd_create_raum", ()),
"stempel": ("_cmd_create_stempel", ()),
"symbol": ("_cmd_create_symbol", ()),
}
_PRETTY = {
"wand": "DOSSIER Wand",
"tuer": "DOSSIER Tuer",
"fenster": "DOSSIER Fenster",
"decke": "DOSSIER Decke",
"aussparung": "DOSSIER Aussparung",
"dach": "DOSSIER Dach",
"treppe": "DOSSIER Treppe",
"stuetze": "DOSSIER Stuetze",
"traeger": "DOSSIER Traeger",
"raum": "DOSSIER Raum",
"stempel": "DOSSIER Stempel",
"symbol": "DOSSIER Symbol",
}
def dispatch(action):
"""Public entry — von per-action Wrapper-Scripts aufgerufen."""
try:
import Rhino
Rhino.RhinoApp.SetCommandPrompt(_PRETTY.get(action, "DOSSIER " + action.capitalize()))
except Exception: pass
bridge = sc.sticky.get("dossier_bridge_elemente")
if bridge is None:
print("[DOSSIER-ALIAS] Elemente-Bridge nicht aktiv (Panel oeffnen)")
return
spec = _ACTIONS.get(action)
if spec is None:
print("[DOSSIER-ALIAS] Unbekannte Aktion:", action)
return
method_name, args = spec
method = getattr(bridge, method_name, None)
if method is None:
print("[DOSSIER-ALIAS] Bridge-Method fehlt:", method_name)
return
try:
method({}, *args)
except Exception as ex:
print("[DOSSIER-ALIAS]", action, "->", method_name, ":", ex)
# Backwards-Compat (alter Name).
_dispatch = dispatch
def _read_action_from_argv():
# sys.argv enthaelt bei _-RunPythonScript "path" arg1 arg2 ... die
# Args nach dem Skript-Pfad. argv[0] = Skript-Pfad.
if len(sys.argv) >= 2:
return str(sys.argv[1]).strip().lower()
return None
if __name__ == "__main__":
a = _read_action_from_argv()
if a:
_dispatch(a)
else:
print("[DOSSIER-ALIAS] Keine Aktion uebergeben. Erwartet:",
", ".join(sorted(_ACTIONS.keys())))
+72
View File
@@ -0,0 +1,72 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
dossier_view_mode.py
Setzt Display-Mode (+ optional Standard-Ansicht) im aktiven Viewport.
Aufruf:
_-RunPythonScript "/.../dossier_view_mode.py" <mode>
mode: plan | persp3d | material | raytracing
"""
import sys
import Rhino
_MODES = {
"plan": {"display": "Dossier Plan", "view": "Top", "label": "DOSSIER Plan-Mode"},
"persp3d": {"display": "Dossier 3D", "view": "Perspective","label": "DOSSIER 3D-Mode"},
"material": {"display": "Dossier Material", "view": None, "label": "DOSSIER Material-Mode"},
"raytracing": {"display": "Dossier Raytracing", "view": None, "label": "DOSSIER Raytracing"},
}
def _apply(mode_name):
spec = _MODES.get(mode_name)
if spec is None:
print("[VIEW-MODE] Unbekannt:", mode_name)
return
try: Rhino.RhinoApp.SetCommandPrompt(spec.get("label", "DOSSIER View"))
except Exception: pass
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
print("[VIEW-MODE] Kein aktives Doc")
return
view = doc.Views.ActiveView
if view is None:
print("[VIEW-MODE] Kein aktiver Viewport")
return
# Standard-View setzen (Top / Perspective) falls definiert
vw_name = spec["view"]
if vw_name:
try:
view.ActiveViewport.SetProjection(
Rhino.Display.DefinedViewportProjection.Top
if vw_name == "Top"
else Rhino.Display.DefinedViewportProjection.Perspective,
vw_name, True)
except Exception as ex:
print("[VIEW-MODE] view-set:", ex)
# Display-Mode setzen via Description-Lookup
dm_name = spec["display"]
try:
all_dm = Rhino.Display.DisplayModeDescription.GetDisplayModes()
target = None
for d in all_dm:
if d.EnglishName == dm_name or d.LocalName == dm_name:
target = d; break
if target is None:
print("[VIEW-MODE] Display-Mode nicht gefunden:", dm_name)
return
view.ActiveViewport.DisplayMode = target
view.Redraw()
except Exception as ex:
print("[VIEW-MODE] display-mode:", ex)
if __name__ == "__main__":
if len(sys.argv) >= 2:
_apply(str(sys.argv[1]).strip().lower())
else:
print("[VIEW-MODE] Erwartet Mode-Name:", ", ".join(_MODES.keys()))
+469
View File
@@ -0,0 +1,469 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
aliases/loader.py
Liest shortcuts_default.json + User-Overrides aus dossier_settings.json,
merged und wendet via Rhino.ApplicationSettings.CommandAliasList /
ShortcutKeySettings an. Wird einmal beim Rhino-Start aus startup.py
aufgerufen (idempotent — SetMacro ueberschreibt).
User-Override-Format in dossier_settings.json:
"shortcuts_user": {
"<action_id>": "<trigger_string>" // leer = Default
}
"""
import os
import json
import Rhino
_HERE = os.path.dirname(os.path.abspath(__file__))
_quit_xml_pairs = [] # gefuellt in apply_all(), genutzt vom Closing-Hook
_DEFAULTS_PATH = os.path.join(_HERE, "shortcuts_default.json")
_SETTINGS_PATHS = [
os.path.expanduser("~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json"),
os.path.expanduser("~/Library/Application Support/RhinoPanel/dossier_settings.json"), # legacy
]
def _read_defaults():
try:
with open(_DEFAULTS_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
out = {}
for k, v in data.items():
if k.startswith("_"): continue
if not isinstance(v, dict): continue
out[k] = v
return out
except Exception as ex:
print("[ALIAS-LOADER] Defaults lesen:", ex)
return {}
def _read_user_overrides():
"""Liest 'shortcuts_user' aus dossier_settings.json. Format:
{ action_id: trigger_string }. Leerer String / None = Default."""
for path in _SETTINGS_PATHS:
if not os.path.exists(path): continue
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
so = data.get("shortcuts_user")
if isinstance(so, dict): return so
except Exception as ex:
print("[ALIAS-LOADER] Settings lesen:", ex)
return {}
def _expand_macro(macro):
"""Platzhalter {ALIASDIR} → absoluter Pfad zum aliases/-Ordner."""
return macro.replace("{ALIASDIR}", _HERE)
# Sonderzeichen → Rhino-Enum-Namen (Mac XML + ShortcutKey-API)
_SPECIAL_KEY_NAMES = {
"-": "Minus", "+": "Plus", "=": "Equals",
"/": "Slash", "\\": "Backslash",
".": "Period", ",": "Comma",
";": "Semicolon", "'": "Quote", "`": "Backquote",
"[": "OpenBracket", "]": "CloseBracket",
}
def _normalize_key_part(key_part):
"""Mapped Sonderzeichen wie '-' auf Enum-Namen ('Minus'). Buchstaben/F-Keys
bleiben unveraendert (Case-preserved)."""
if key_part in _SPECIAL_KEY_NAMES:
return _SPECIAL_KEY_NAMES[key_part]
return key_part
def _xml_key_from_trigger(trigger):
"""'Cmd+Shift+F3''CommandShiftF3' (Mac Rhino XML-Schema).
Cmd/Ctrl → 'Command', Shift → 'Shift', Alt/Option → 'Option'.
Sonderzeichen ('-', '/', etc.) werden auf Enum-Namen gemapped."""
t = trigger.replace(" ", "")
parts = t.split("+") if "+" in t[1:] else [t]
# Edge-Case: trigger endet auf literal '+' oder '-' → letztes Element ist Key
# 'Cmd+-' → ['Cmd', '', '-'] via split. Fix: re-split last token wenn leer
parts = [p for p in parts if p != ""]
# Sonderfall trigger == 'Cmd+-' → split('+') = ['Cmd', '-'], OK
# Sonderfall trigger == 'Cmd++' → split('+') = ['Cmd', '', ''] → key = '+'
if "Cmd++" in trigger or "Ctrl++" in trigger or "Shift++" in trigger:
parts = trigger.replace(" ", "").rstrip("+").split("+") + ["+"]
if not parts: return None
key_part = _normalize_key_part(parts[-1])
mods = set(p.lower() for p in parts[:-1])
has_cmd = ("cmd" in mods) or ("ctrl" in mods) or ("command" in mods)
has_shift = "shift" in mods
has_alt = ("alt" in mods) or ("option" in mods) or ("opt" in mods)
prefix = ""
if has_cmd: prefix += "Command"
if has_shift: prefix += "Shift"
if has_alt: prefix += "Option"
return prefix + key_part
def _entry_in_xml(xml_key, expected_macro):
"""True wenn <entry key='<xml_key>'>expected_macro</entry> bereits im
Mac Rhino settings-XML existiert."""
import os
import re
paths = [
os.path.expanduser("~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"),
]
_esc = lambda s: s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
pat = re.compile(
r'<entry\s+key="' + re.escape(xml_key) + r'"\s*>([^<]*)</entry>')
for path in paths:
if not os.path.exists(path): continue
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
m = pat.search(content)
if m and m.group(1) == _esc(expected_macro):
return True
except Exception: pass
return False
def _xml_persist_shortcut(xml_key, macro, verbose=False):
"""Schreibt <entry key="<xml_key>"><macro></entry> direkt in Mac Rhino's
settings-Scheme__Default.xml unter <child key='ShortcutKeys'>. String-
basiert damit die Original-Formatierung 1:1 erhalten bleibt."""
import os
import re
paths = [
os.path.expanduser("~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"),
]
n_written = 0
_esc = lambda s: s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
for path in paths:
if not os.path.exists(path): continue
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
new_entry = '<entry key="{}">{}</entry>'.format(xml_key, _esc(macro))
# Existing entry? Loeschen (mit umgebendem Whitespace+Newline)
# und neu hinzufuegen mit sauberem Format. Vermeidet
# kaputt-formatierte Entries.
pat = re.compile(
r'<entry\s+key="' + re.escape(xml_key) + r'"\s*(/>|>[^<]*</entry>)')
m = pat.search(content)
if m:
# Check Line-Kontext: nur diese Entry auf Zeile + unveraendert?
line_start = content.rfind("\n", 0, m.start()) + 1
line_end = content.find("\n", m.end())
if line_end < 0: line_end = len(content)
line_trim = content[line_start:line_end].strip()
if line_trim == new_entry:
if verbose: print("[ALIAS-LOADER] XML '{}' unchanged".format(xml_key))
continue
# Sonst: loeschen inkl. preceding-newline+whitespace damit
# keine orphan-line uebrig bleibt
del_start = m.start()
while del_start > 0 and content[del_start-1] in " \t":
del_start -= 1
if del_start > 0 and content[del_start-1] == "\n":
del_start -= 1
content = content[:del_start] + content[m.end():]
if True:
# ShortcutKeys-Section finden
sec_start = content.find('<child key="ShortcutKeys">')
if sec_start < 0:
if verbose: print("[ALIAS-LOADER] ShortcutKeys-section fehlt")
continue
sec_end = content.find('</child>', sec_start)
if sec_end < 0:
if verbose: print("[ALIAS-LOADER] ShortcutKeys-close fehlt")
continue
# Indent vom letzten <entry> in der Section uebernehmen
section = content[sec_start:sec_end]
ms = list(re.finditer(r'\n([ \t]*)<entry\s', section))
entry_indent = ms[-1].group(1) if ms else " "
# Indent vor </child> (typisch 6 spaces)
close_match = re.search(r'\n([ \t]*)$', content[:sec_end])
close_indent = close_match.group(1) if close_match else " "
# Section neu zusammensetzen: alles vor </child> bereinigt
# + sauberer Insert
before = content[:sec_end].rstrip(" \t") + "\n"
content = (before + entry_indent + new_entry + "\n"
+ close_indent + content[sec_end:])
action = "added"
with open(path, "w", encoding="utf-8") as f:
f.write(content)
n_written += 1
if verbose: print("[ALIAS-LOADER] XML {} '{}'".format(action, xml_key))
except Exception as ex:
print("[ALIAS-LOADER] XML-Write {}: {}".format(path, ex))
return n_written
def _install_quit_xml_save(pairs):
"""Rhino's Closing-Event fired auf Mac NICHT zuverlaessig. Wir
installieren MEHRERE Hooks parallel:
1. Rhino.RhinoApp.Closing (Mac: meist No-op, Windows: ok)
2. Python atexit (laeuft wenn Interpreter terminiert)
3. AppDomain.ProcessExit (.NET-Level Hook)
4. Idle-Watcher: schreibt XML alle 30s wenn Aenderung erkannt
(Fallback fuer Rhino's runtime-flush)
Marker-Logging zur Verifikation welcher Hook wirklich feuert."""
import os as _os
import datetime as _dt
_marker = _os.path.expanduser("~/Library/Logs/dossier_quit_hook.log")
try:
_os.makedirs(_os.path.dirname(_marker), exist_ok=True)
except Exception: pass
def _log(msg):
try:
with open(_marker, "a") as f:
f.write("[{}] {}\n".format(_dt.datetime.now().isoformat(), msg))
except Exception: pass
def _write_all(source):
n_ok = 0
for xml_key, macro in pairs:
if _xml_persist_shortcut(xml_key, macro, verbose=False) > 0:
n_ok += 1
_log("{} FIRED — {}/{} ok".format(source, n_ok, len(pairs)))
return n_ok
n_hooks = 0
try:
import Rhino
def _on_closing(*_):
try: _write_all("RhinoClosing")
except Exception as ex: _log("RhinoClosing ERROR: {}".format(ex))
Rhino.RhinoApp.Closing += _on_closing
n_hooks += 1
except Exception as ex:
_log("RhinoClosing install err: {}".format(ex))
try:
import atexit
def _on_atexit():
try: _write_all("atexit")
except Exception as ex: _log("atexit ERROR: {}".format(ex))
atexit.register(_on_atexit)
n_hooks += 1
except Exception as ex:
_log("atexit install err: {}".format(ex))
try:
import System
def _on_process_exit(*_):
try: _write_all("ProcessExit")
except Exception as ex: _log("ProcessExit ERROR: {}".format(ex))
System.AppDomain.CurrentDomain.ProcessExit += _on_process_exit
n_hooks += 1
except Exception as ex:
_log("ProcessExit install err: {}".format(ex))
# Idle-Watcher: periodisch (alle ~30s) checken ob unsere XML-Entries
# noch da sind. Wenn nein → wieder reinschreiben. Ueberlebt Rhino-
# Runtime-Flushes auch ohne Close-Event.
try:
import Rhino
import time as _time
_state = {"last": 0.0}
def _idle_watcher(*_):
try:
now = _time.time()
if now - _state["last"] < 30.0: return
_state["last"] = now
# Pruefen ob entries fehlen — wenn ja, alle re-schreiben
_write_all("IdleWatch")
except Exception as ex:
_log("IdleWatch ERROR: {}".format(ex))
Rhino.RhinoApp.Idle += _idle_watcher
n_hooks += 1
_log("IdleWatch installed (30s interval)")
except Exception as ex:
_log("IdleWatch install err: {}".format(ex))
_log("Hooks INSTALLED ({} of 4) for {} shortcuts".format(n_hooks, len(pairs)))
# Initiale Schreibung im ersten Pass auch — falls Rhino sofort flusht
_write_all("InitialWrite")
return n_hooks > 0
def _resolve_fkey(trigger):
"""'F3' / 'Shift+F3' / 'Cmd+F3' / 'Cmd+Alt+F3' → ShortcutKey-Enum-Wert.
Enum-Naming-Konvention von Rhino: Ctrl → Shift → Alt → KeyName
(z.B. CtrlAltF3, CtrlShiftAltF3). Cmd auf Mac mappt auf Ctrl,
Option/Opt auf Alt. Sonderzeichen via _SPECIAL_KEY_NAMES."""
SK = Rhino.ApplicationSettings.ShortcutKey
t = trigger.replace(" ", "")
parts = t.split("+")
parts = [p for p in parts if p != ""]
if not parts: return None
raw_last = parts[-1]
if raw_last in _SPECIAL_KEY_NAMES:
key_part = _SPECIAL_KEY_NAMES[raw_last]
else:
key_part = raw_last.upper()
mods = set(p.lower() for p in parts[:-1])
has_ctrl = ("ctrl" in mods) or ("cmd" in mods) or ("command" in mods)
has_shift = "shift" in mods
has_alt = ("alt" in mods) or ("option" in mods) or ("opt" in mods)
prefix = ""
if has_ctrl: prefix += "Ctrl"
if has_shift: prefix += "Shift"
if has_alt: prefix += "Alt"
return getattr(SK, prefix + key_part, None)
def _resolve_cmd_letter(trigger):
"""'Cmd+W' / 'Cmd+Shift+W' → ShortcutKey-Enum (Ctrl* auf Rhino-Naming-
Konvention; Mac mappt Ctrl auf Cmd intern)."""
SK = Rhino.ApplicationSettings.ShortcutKey
t = trigger.replace(" ", "")
parts = t.split("+")
if len(parts) < 2: return None
letter = parts[-1].upper()
if not (len(letter) == 1 and letter.isalpha()): return None
mods = set(p.lower() for p in parts[:-1])
has_cmd = ("cmd" in mods) or ("ctrl" in mods)
if not has_cmd: return None
name = "Ctrl"
if "shift" in mods: name += "Shift"
if "alt" in mods: name += "Alt"
name += letter
return getattr(SK, name, None)
def apply_all():
"""Liest Defaults + Overrides, wendet alle Aliases + Shortcuts an.
Returnt (n_alias, n_fkey, n_cmd, n_skipped)."""
global _quit_xml_pairs
_quit_xml_pairs = []
defaults = _read_defaults()
overrides = _read_user_overrides()
aliases = Rhino.ApplicationSettings.CommandAliasList
skset = Rhino.ApplicationSettings.ShortcutKeySettings
n_alias = n_fkey = n_cmd = n_skipped = 0
seen_triggers = {} # trigger_normalized -> action_id (Konflikt-Erkennung)
for action_id, spec in defaults.items():
# User-Override hat Vorrang. Leerer String = Default, None/missing = Default.
user_trig = overrides.get(action_id)
if user_trig is not None and str(user_trig).strip() == "":
user_trig = None
trigger = user_trig if user_trig else spec.get("trigger", "")
if not trigger:
n_skipped += 1
continue
spec_type = spec.get("type", "alias")
macro = _expand_macro(spec.get("macro", ""))
if not macro:
n_skipped += 1; continue
# Konflikt-Check (gleicher Trigger → letzter gewinnt, Warning)
norm = (spec_type, str(trigger).lower())
if norm in seen_triggers:
print("[ALIAS-LOADER] Konflikt: '{}' fuer {} bereits von {} belegt"
.format(trigger, action_id, seen_triggers[norm]))
seen_triggers[norm] = action_id
try:
if spec_type == "alias":
tname = str(trigger)
try:
if aliases.IsAlias(tname):
aliases.Delete(tname)
except Exception: pass
added = False
try:
added = aliases.Add(tname, macro)
except Exception as _addex:
print("[ALIAS-LOADER] Add({}, ...) Exception: {}"
.format(tname, _addex))
if not added:
try: aliases.SetMacro(tname, macro)
except Exception: pass
# Verifizieren ob Alias wirklich registriert ist
try:
is_ok = aliases.IsAlias(tname)
if not is_ok:
print("[ALIAS-LOADER] WARN: '{}' (action={}) NICHT registriert "
"— Rhino lehnt Namen wahrscheinlich ab (z.B. reine Zahl)"
.format(tname, action_id))
n_skipped += 1
continue
except Exception: pass
n_alias += 1
elif spec_type == "fkey":
sk = _resolve_fkey(str(trigger))
xml_key = _xml_key_from_trigger(str(trigger))
api_ok = False
if sk is not None:
try:
skset.SetMacro(sk, macro)
got = skset.GetMacro(sk)
api_ok = (got == macro)
except Exception as _sex:
print("[ALIAS-LOADER] SetMacro({}): {}".format(trigger, _sex))
if not api_ok and xml_key:
# Enum-Wert fehlt → direkt ins XML (mit verbose-Log).
# n_xml=0 kann "schon korrekt" ODER "gescheitert" heissen
# — wir checken explizit ob Entry im XML existiert.
n_xml = _xml_persist_shortcut(xml_key, macro, verbose=True)
if n_xml > 0:
_quit_xml_pairs.append((xml_key, macro))
else:
# n_xml == 0 → entweder "unchanged" (= schon korrekt
# im XML) oder "missing path/section". Check via
# IsAliasInXml damit wir nicht falsch warnen.
if _entry_in_xml(xml_key, macro):
# Schon korrekt im XML → fuer Quit-Hook merken
# damit Rhino-Quit-Save sie nicht ueberschreibt
_quit_xml_pairs.append((xml_key, macro))
else:
print("[ALIAS-LOADER] WARN F-Key {} ({}) konnte weder "
"API noch XML gesetzt werden".format(trigger, action_id))
n_skipped += 1; continue
n_fkey += 1
elif spec_type == "cmd":
sk = _resolve_cmd_letter(str(trigger))
if sk is None:
# Fallback: Cmd+Letter API u.U. nicht im Enum → als Alias mit dem
# Letter (single-char) registrieren. User tippt dann Letter+Enter.
letter_only = str(trigger).split("+")[-1].lower()
if len(letter_only) == 1 and letter_only.isalpha():
aliases.SetMacro(letter_only, macro)
n_alias += 1
print("[ALIAS-LOADER] {} ({}): Cmd+Letter nicht im Enum, "
"fallback Alias '{}'".format(action_id, trigger, letter_only))
else:
n_skipped += 1
continue
skset.SetMacro(sk, macro)
n_cmd += 1
else:
print("[ALIAS-LOADER] Unbekannter Type:", spec_type); n_skipped += 1
except Exception as ex:
print("[ALIAS-LOADER] Apply", action_id, "->", trigger, ":", ex)
n_skipped += 1
# Quit-Hook installieren falls XML-only Shortcuts gesetzt wurden — diese
# ueberlebt sonst Rhino's Auto-Save beim Quit nicht.
if _quit_xml_pairs:
_install_quit_xml_save(list(_quit_xml_pairs))
print("[ALIAS-LOADER] {} XML-only Shortcuts werden bei Quit "
"re-persistiert (Closing-Hook installiert)"
.format(len(_quit_xml_pairs)))
return n_alias, n_fkey, n_cmd, n_skipped
if __name__ == "__main__":
a, f, c, s = apply_all()
print("[ALIAS-LOADER] OK: {} alias, {} fkey, {} cmd, {} skipped"
.format(a, f, c, s))
+66
View File
@@ -0,0 +1,66 @@
{
"_meta": {
"version": 2,
"description": "DOSSIER Default Shortcuts. Schema: F1-F12 = 2D-Werkzeuge (Single-Tastendruck). Shift+F* = Views/Panels. Cmd+F* = BIM-Objekte. F8/F9 bleiben Rhino-Default (Ortho/Snap). 2D-Tools auch als Alias n1-n0 (Fallback fuer typen). 2-Letter-Aliases (st/tg/ra/sy/sp/dh/au) fuer seltenere BIM. User-Overrides leben in dossier_settings.json unter 'shortcuts_user' = {action_id: trigger_string}. Macro-Platzhalter {ALIASDIR} wird zur Laufzeit ersetzt."
},
"wand": { "type": "fkey", "trigger": "Cmd+F1", "label": "DOSSIER Wand erstellen", "macro": "dWall" },
"tuer": { "type": "fkey", "trigger": "Cmd+F2", "label": "DOSSIER Tuer erstellen", "macro": "dDoor" },
"fenster": { "type": "fkey", "trigger": "Cmd+F3", "label": "DOSSIER Fenster erstellen", "macro": "dWindow" },
"decke": { "type": "fkey", "trigger": "Cmd+F4", "label": "DOSSIER Decke erstellen", "macro": "dSlab" },
"treppe": { "type": "fkey", "trigger": "Cmd+F5", "label": "DOSSIER Treppe erstellen", "macro": "dStair" },
"stuetze": { "type": "fkey", "trigger": "Cmd+F6", "label": "DOSSIER Stuetze erstellen", "macro": "dColumn" },
"traeger": { "type": "fkey", "trigger": "Cmd+F7", "label": "DOSSIER Traeger erstellen", "macro": "dBeam" },
"raum": { "type": "fkey", "trigger": "Cmd+F10", "label": "DOSSIER Raum erstellen", "macro": "dRoom" },
"symbol": { "type": "fkey", "trigger": "Cmd+F11", "label": "DOSSIER Symbol erstellen", "macro": "dSymbol" },
"stempel": { "type": "fkey", "trigger": "Cmd+F12", "label": "DOSSIER Stempel erstellen", "macro": "dTag" },
"dach": { "type": "alias", "trigger": "dh", "label": "DOSSIER Dach (Alias)", "macro": "dRoof" },
"aussparung": { "type": "alias", "trigger": "au", "label": "DOSSIER Aussparung (Alias)", "macro": "dVoid" },
"text": { "type": "fkey", "trigger": "F1", "label": "Text", "macro": "_Text" },
"line": { "type": "fkey", "trigger": "F2", "label": "Linie", "macro": "_Line" },
"arc": { "type": "fkey", "trigger": "F3", "label": "Kreisbogen", "macro": "_Arc" },
"rectangle": { "type": "fkey", "trigger": "F4", "label": "Rechteck", "macro": "_Rectangle" },
"polyline": { "type": "fkey", "trigger": "F5", "label": "Polylinie", "macro": "_Polyline" },
"curve": { "type": "fkey", "trigger": "F6", "label": "Spline / Kurve", "macro": "_Curve" },
"hatch": { "type": "fkey", "trigger": "F7", "label": "Schraffur", "macro": "_Hatch" },
"polygon": { "type": "fkey", "trigger": "F10", "label": "Polygon", "macro": "_Polygon" },
"ellipse": { "type": "fkey", "trigger": "F11", "label": "Ellipse", "macro": "_Ellipse" },
"circle": { "type": "fkey", "trigger": "F12", "label": "Kreis", "macro": "_Circle" },
"view_plan": { "type": "fkey", "trigger": "Cmd+K", "label": "Plan-Mode (Top + Dossier Plan)", "macro": "dPlan" },
"view_3d": { "type": "fkey", "trigger": "Cmd+L", "label": "3D-Mode (Perspective + Dossier 3D)", "macro": "d3D" },
"zoom_ext": { "type": "fkey", "trigger": "Cmd+U", "label": "Zoom Extents", "macro": "_Zoom _All _Extents" },
"zoom_sel": { "type": "fkey", "trigger": "Cmd+Shift+U", "label": "Zoom Selected", "macro": "_Zoom _Selected" },
"mod_group": { "type": "fkey", "trigger": "Cmd+G", "label": "Gruppieren (Group)", "macro": "_Group" },
"geschoss_up": { "type": "alias", "trigger": "gu", "label": "Geschoss hoch (Alias)", "macro": "dLevelUp" },
"geschoss_down": { "type": "fkey", "trigger": "Cmd+B", "label": "Geschoss tief", "macro": "dLevelDown" },
"view_material": { "type": "alias", "trigger": "ma", "label": "Material-Mode (Alias)", "macro": "dMaterial" },
"panel_layer": { "type": "alias", "trigger": "la", "label": "Layer-Panel (Alias)", "macro": "_Layer" },
"panel_elemente": { "type": "alias", "trigger": "el", "label": "DOSSIER Elemente-Panel (Alias)", "macro": "-_ShowPanel \"DOSSIER Elemente\"" },
"mod_mirror": { "type": "fkey", "trigger": "Cmd+I", "label": "Spiegeln (Mirror)", "macro": "_Mirror" },
"mod_copy": { "type": "fkey", "trigger": "Cmd+D", "label": "Kopieren (Copy = Duplicate)", "macro": "_Copy" },
"mod_rotate": { "type": "fkey", "trigger": "Cmd+R", "label": "Drehen (Rotate)", "macro": "_Rotate" },
"mod_trim": { "type": "fkey", "trigger": "Cmd+T", "label": "Trim (Schneiden)", "macro": "_Trim" },
"mod_join": { "type": "fkey", "trigger": "Cmd+J", "label": "Verbinden (Smart-Join: Regionen → Union, sonst Join)", "macro": "dJoin" },
"mod_explode": { "type": "fkey", "trigger": "Cmd+E", "label": "Trennen (Explode)", "macro": "_Explode" },
"mod_fillet": { "type": "fkey", "trigger": "Cmd+Shift+V", "label": "Verrunden (Fillet)", "macro": "_Fillet" },
"mod_move": { "type": "fkey", "trigger": "Cmd+M", "label": "Verschieben (Move)", "macro": "_Move" },
"mod_offset": { "type": "fkey", "trigger": "Cmd+Shift+P", "label": "Parallele (OffsetCrv)", "macro": "_OffsetCrv" },
"mod_split": { "type": "fkey", "trigger": "Cmd+X", "label": "Smart-Split (Splitlinie zeichnen — ueberschreibt Cut)", "macro": "dSplit" },
"mod_chamfer": { "type": "fkey", "trigger": "Cmd+Shift+C", "label": "Abfasen (Chamfer)", "macro": "_Chamfer" },
"mod_pipette": { "type": "fkey", "trigger": "Cmd+Y", "label": "Pipette (Einstellungen uebernehmen)", "macro": "dPipette" },
"cheatsheet": { "type": "fkey", "trigger": "Cmd+-", "label": "DOSSIER Shortcuts-Cheatsheet", "macro": "dKeys" },
"text_alias": { "type": "alias", "trigger": "n1", "label": "Text (Alias)", "macro": "_Text" },
"line_alias": { "type": "alias", "trigger": "n2", "label": "Linie (Alias)", "macro": "_Line" },
"arc_alias": { "type": "alias", "trigger": "n3", "label": "Kreisbogen (Alias)", "macro": "_Arc" },
"rectangle_alias": { "type": "alias", "trigger": "n4", "label": "Rechteck (Alias)", "macro": "_Rectangle" },
"polyline_alias": { "type": "alias", "trigger": "n5", "label": "Polylinie (Alias)", "macro": "_Polyline" },
"curve_alias": { "type": "alias", "trigger": "n6", "label": "Kurve (Alias)", "macro": "_Curve" },
"hatch_alias": { "type": "alias", "trigger": "n7", "label": "Schraffur (Alias)", "macro": "_Hatch" },
"polygon_alias": { "type": "alias", "trigger": "n8", "label": "Polygon (Alias)", "macro": "_Polygon" },
"ellipse_alias": { "type": "alias", "trigger": "n9", "label": "Ellipse (Alias)", "macro": "_Ellipse" },
"circle_alias": { "type": "alias", "trigger": "n0", "label": "Kreis (Alias)", "macro": "_Circle" }
}
+42
View File
@@ -0,0 +1,42 @@
#! python3
# -*- coding: utf-8 -*-
# Geschoss runter (zum naechsttieferen Eintrag in der Zeichnungsebenen-Liste)
import json
import scriptcontext as sc
import Rhino
def _go(delta):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
print("[GESCHOSS-NAV] kein Doc"); return
bridge = sc.sticky.get("ebenen_bridge_ref")
if bridge is None:
print("[GESCHOSS-NAV] Ebenen-Bridge nicht aktiv (Panel oeffnen)"); return
try:
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or ""
zs = json.loads(zraw) if zraw else []
if not isinstance(zs, list) or not zs:
print("[GESCHOSS-NAV] keine Zeichnungsebenen"); return
cur_id = doc.Strings.GetValue("dossier_active_id") or ""
idx = -1
for i, z in enumerate(zs):
if isinstance(z, dict) and z.get("id") == cur_id:
idx = i; break
if idx < 0:
idx = len(zs) # nichts aktiv → starten unten
new_idx = max(0, min(len(zs) - 1, idx + delta))
if new_idx == idx:
print("[GESCHOSS-NAV] schon am {}".format(
"untersten" if delta > 0 else "obersten")); return
target = zs[new_idx]
if not isinstance(target, dict) or not target.get("id"):
print("[GESCHOSS-NAV] Zielebene ungueltig"); return
print("[GESCHOSS-NAV] wechsle zu '{}'".format(target.get("name") or target["id"]))
bridge._set_active_zeichnungsebene(target)
except Exception as ex:
print("[GESCHOSS-NAV]", ex)
# delta=+1 = nach unten (naechster Eintrag in der Liste)
_go(+1)
+43
View File
@@ -0,0 +1,43 @@
#! python3
# -*- coding: utf-8 -*-
# Geschoss hoch (zum naechstoberen Eintrag in der Zeichnungsebenen-Liste)
import json
import scriptcontext as sc
import Rhino
def _go(delta):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
print("[GESCHOSS-NAV] kein Doc"); return
bridge = sc.sticky.get("ebenen_bridge_ref")
if bridge is None:
print("[GESCHOSS-NAV] Ebenen-Bridge nicht aktiv (Panel oeffnen)"); return
try:
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or ""
zs = json.loads(zraw) if zraw else []
if not isinstance(zs, list) or not zs:
print("[GESCHOSS-NAV] keine Zeichnungsebenen"); return
cur_id = doc.Strings.GetValue("dossier_active_id") or ""
idx = -1
for i, z in enumerate(zs):
if isinstance(z, dict) and z.get("id") == cur_id:
idx = i; break
if idx < 0:
idx = 0 # nichts aktiv → starten oben
new_idx = max(0, min(len(zs) - 1, idx + delta))
if new_idx == idx:
print("[GESCHOSS-NAV] schon am {}".format(
"obersten" if delta < 0 else "untersten")); return
target = zs[new_idx]
if not isinstance(target, dict) or not target.get("id"):
print("[GESCHOSS-NAV] Zielebene ungueltig"); return
print("[GESCHOSS-NAV] wechsle zu '{}'".format(target.get("name") or target["id"]))
bridge._set_active_zeichnungsebene(target)
except Exception as ex:
print("[GESCHOSS-NAV]", ex)
# delta=-1 = nach oben (vorheriger Eintrag in der Liste, weil Listen
# typischerweise oberste Ebene oben sind)
_go(-1)
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer View-Mode 'material'.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_view_mode
dossier_view_mode._apply("material")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer View-Mode 'persp3d'.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_view_mode
dossier_view_mode._apply("persp3d")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer View-Mode 'plan'.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_view_mode
dossier_view_mode._apply("plan")
+79
View File
@@ -0,0 +1,79 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
begin_cmd_hook.py
Hook auf Rhino.Commands.Command.BeginCommand. Wenn der User ein Drawing-
Command startet (Line, Polyline, Rectangle, Circle etc.), oeffnen wir
automatisch das DOSSIER-Gestaltung-Panel und bringen es in den Vordergrund.
Idempotent — Re-Install nach _reset_panels deregistriert alten Handler.
"""
import Rhino
import scriptcontext as sc
import System
# Commands bei denen wir Gestaltung-Panel fokussieren.
# CommandEnglishName ohne Underscore-Prefix.
_DRAWING_COMMANDS = {
"Line", "Polyline", "Curve", "InterpCrv",
"Arc", "Circle", "Ellipse",
"Rectangle", "Polygon",
"Hatch", "Text",
"Point", "Points",
"InfiniteLine",
}
_GESTALTUNG_PANEL_GUID = "4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829"
_HANDLER_KEY = "_dossier_begin_cmd_handler"
_VERBOSE_KEY = "_dossier_begin_cmd_verbose"
def _on_begin_command(sender, e):
try:
cmd = getattr(e, "CommandEnglishName", "") or ""
if sc.sticky.get(_VERBOSE_KEY):
print("[BEGIN-CMD] cmd='{}'".format(cmd))
if cmd not in _DRAWING_COMMANDS: return
try:
guid = System.Guid(_GESTALTUNG_PANEL_GUID)
Rhino.UI.Panels.OpenPanel(guid)
try:
Rhino.UI.Panels.FocusPanel(guid)
except Exception: pass
if sc.sticky.get(_VERBOSE_KEY):
print("[BEGIN-CMD] Gestaltung-Panel geoeffnet/fokussiert")
except Exception as ex:
print("[BEGIN-CMD] OpenPanel:", ex)
try:
Rhino.RhinoApp.RunScript(
'-_ShowPanel "DOSSIER Gestaltung"', False)
except Exception: pass
except Exception as ex:
print("[BEGIN-CMD] handler:", ex)
def install(verbose=False):
"""Einmalige Registrierung. Bei Re-Install (z.B. nach _reset_panels)
wird der alte Handler-Ref aus sc.sticky deregistriert."""
old = sc.sticky.get(_HANDLER_KEY)
if old is not None:
try: Rhino.Commands.Command.BeginCommand -= old
except Exception: pass
try:
Rhino.Commands.Command.BeginCommand += _on_begin_command
sc.sticky[_HANDLER_KEY] = _on_begin_command
sc.sticky[_VERBOSE_KEY] = bool(verbose)
print("[BEGIN-CMD] Hook installed (verbose={})".format(bool(verbose)))
except Exception as ex:
print("[BEGIN-CMD] install:", ex)
def set_verbose(flag):
sc.sticky[_VERBOSE_KEY] = bool(flag)
if __name__ == "__main__":
install(verbose=True)
+858 -311
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -1507,6 +1507,14 @@ class OberleisteBridge(panel_base.BaseBridge):
except Exception as ex: except Exception as ex:
print("[OBERLEISTE] open about:", ex) print("[OBERLEISTE] open about:", ex)
# --- Cheatsheet (Shortcuts-Uebersicht) --------------------------
elif t == "OPEN_CHEATSHEET":
try:
import welcome
welcome.show_cheatsheet()
except Exception as ex:
print("[OBERLEISTE] open cheatsheet:", ex)
# --- Text-Erstellung (Floating-Input) --------------------------- # --- Text-Erstellung (Floating-Input) ---------------------------
elif t == "CREATE_TEXT": elif t == "CREATE_TEXT":
try: try:
+7 -6
View File
@@ -684,8 +684,8 @@ def make_panel_icon(name_or_letter, bg_hex):
if icon_bmp is not None: chosen_path = png_path if icon_bmp is not None: chosen_path = png_path
else: print("[panel_base] PNG geladen aber Bitmap None:", else: print("[panel_base] PNG geladen aber Bitmap None:",
png_path) png_path)
else: # PNG-not-found ist normal: Fallback auf SVG dann Material-Font.
print("[panel_base] PNG nicht gefunden:", png_path) # Nur loggen wenn final ALLES failt (s.u.).
if icon_bmp is None: if icon_bmp is None:
svg_path = os.path.join(_PANEL_ICONS_SVG_DIR, svg_path = os.path.join(_PANEL_ICONS_SVG_DIR,
name_or_letter + ".svg") name_or_letter + ".svg")
@@ -713,10 +713,11 @@ def make_panel_icon(name_or_letter, bg_hex):
if font_family_name: if font_family_name:
try: try:
ff = drawing.FontFamily(font_family_name) ff = drawing.FontFamily(font_family_name)
# FontStyle.None: in Python3 nicht direkt zugreifbar # FontStyle.None: in Python3 ist None ein Keyword, deshalb
# (None ist Keyword) → getattr-Workaround, sonst 0 # via System.Enum.ToObject explizit konstruieren — Python.NET 3
try: fs = getattr(drawing.FontStyle, "None") # konvertiert int → Enum nicht mehr implizit.
except Exception: fs = 0 import System
fs = System.Enum.ToObject(drawing.FontStyle, 0)
font = drawing.Font(ff, 20, fs) font = drawing.Font(ff, 20, fs)
glyph = chr(mat_cp) glyph = chr(mat_cp)
_draw_glyph(g, size, font, glyph, _draw_glyph(g, size, font, glyph,
+92
View File
@@ -309,6 +309,47 @@ _PROJECT_SETTINGS_DEFAULTS = {
"unit": "meters", # "meters" | "millimeters" | "centimeters" "unit": "meters", # "meters" | "millimeters" | "centimeters"
}, },
"materials": [], "materials": [],
# Wand-Stile: Wand-Typ-Templates mit Prio fuer Joint-Dominanz.
#
# SOLID-Style (layered=False):
# material = Material-Identitaet (driver fuer Section-Hatch)
# dicke = DEFAULT bei Erstellung — User kann pro Wand ueberschreiben
# referenz = DEFAULT
#
# LAYERED-Style (layered=True):
# layers = fixe Schicht-Komposition mit Material+Dicke pro Layer
# dicke = ignoriert (kommt aus Summe der Layers)
#
# PRIO (1-999, 999=dominant): zwei Wand-Stile koennen das gleiche Material
# haben aber verschiedene Prios (z.B. Beton tragend prio=800,
# Beton innen prio=400). Bei Joints zwischen verschiedenen Stilen gewinnt
# der mit hoeherer Prio die Ecke (Phase 3, noch nicht implementiert).
#
# SECTION-MERGE (Phase 2, noch nicht implementiert): Hatches mergen visuell
# an Joints zwischen Waenden mit GLEICHEM Material — egal ob gleicher Stil.
# So sind „Beton tragend" und „Beton innen" im Schnitt verbunden.
"wand_styles": [
{"id": "style_beton_tragend", "name": "Beton tragend",
"prio": 800, "dicke": 0.25, "referenz": "mid",
"layered": False, "material": "Stahlbeton", "layers": []},
{"id": "style_beton_innen", "name": "Beton innen",
"prio": 400, "dicke": 0.15, "referenz": "mid",
"layered": False, "material": "Stahlbeton", "layers": []},
{"id": "style_gips", "name": "Gipswand",
"prio": 200, "dicke": 0.10, "referenz": "mid",
"layered": False, "material": "Putz", "layers": []},
{"id": "style_mauerwerk", "name": "Mauerwerk",
"prio": 300, "dicke": 0.12, "referenz": "mid",
"layered": False, "material": "Mauerwerk", "layers": []},
{"id": "style_aussen30", "name": "Aussenwand 30 cm gedaemmt",
"prio": 900, "dicke": 0.30, "referenz": "left",
"layered": True, "material": "",
"layers": [
{"material": "Stahlbeton", "dicke": 0.18},
{"material": "Daemmung", "dicke": 0.10},
{"material": "Putz", "dicke": 0.02},
]},
],
"project": { "project": {
"name": "", "name": "",
"number": "", "number": "",
@@ -379,6 +420,48 @@ def _normalize_project_meta(p):
} }
def _normalize_wand_style(s):
"""Garantiert Wand-Style-Schema. Stil = Bundle aus Geometrie + Prio.
Felder:
- id (str, kebab-case empfohlen), name (str)
- prio (int 1-999): bei Joints zwischen verschiedenen Stilen wins der hoehere
- dicke (float, Meter)
- referenz ('mid'|'left'|'right')
- layered (bool): wenn True, layers definieren die Schichten
- material (str): bei layered=False der Material-Name
- layers (list of {material, dicke}): bei layered=True"""
if not isinstance(s, dict): return None
sid = str(s.get("id") or "").strip()
if not sid: return None
try: prio = int(s.get("prio", 500))
except Exception: prio = 500
prio = max(1, min(999, prio))
try: dicke = float(s.get("dicke", 0.25))
except Exception: dicke = 0.25
ref = str(s.get("referenz") or "mid")
if ref not in ("mid", "left", "right"): ref = "mid"
layered = bool(s.get("layered"))
layers = []
if layered and isinstance(s.get("layers"), list):
for ly in s["layers"]:
if not isinstance(ly, dict): continue
try: ld = float(ly.get("dicke", 0))
except Exception: ld = 0.0
if ld <= 0: continue
layers.append({"material": str(ly.get("material") or ""),
"dicke": ld})
return {
"id": sid,
"name": str(s.get("name") or sid),
"prio": prio,
"dicke": dicke,
"referenz": ref,
"layered": layered,
"material": str(s.get("material") or ""),
"layers": layers,
}
def _normalize_material(m): def _normalize_material(m):
"""Garantiert Material-Schema. Material ist REIN 3D — Section-Hatch """Garantiert Material-Schema. Material ist REIN 3D — Section-Hatch
(2D-Schnitt) wird via Ebenen-Settings am Layer konfiguriert. (2D-Schnitt) wird via Ebenen-Settings am Layer konfiguriert.
@@ -442,6 +525,7 @@ def load_project_settings(doc):
out = { out = {
"defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]), "defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]),
"materials": list(_PROJECT_SETTINGS_DEFAULTS["materials"]), "materials": list(_PROJECT_SETTINGS_DEFAULTS["materials"]),
"wand_styles": [dict(s) for s in _PROJECT_SETTINGS_DEFAULTS["wand_styles"]],
"project": dict(_PROJECT_SETTINGS_DEFAULTS["project"]), "project": dict(_PROJECT_SETTINGS_DEFAULTS["project"]),
} }
if raw: if raw:
@@ -458,6 +542,12 @@ def load_project_settings(doc):
_normalize_material(x) for x in m _normalize_material(x) for x in m
if _normalize_material(x) is not None if _normalize_material(x) is not None
] ]
ws = data.get("wand_styles")
if isinstance(ws, list):
out["wand_styles"] = [
_normalize_wand_style(s) for s in ws
if _normalize_wand_style(s) is not None
]
pr = data.get("project") pr = data.get("project")
if isinstance(pr, dict): if isinstance(pr, dict):
out["project"] = _normalize_project_meta(pr) out["project"] = _normalize_project_meta(pr)
@@ -976,6 +1066,7 @@ class EbenenBridge(panel_base.BaseBridge):
"defaults": current.get("defaults", {}), "defaults": current.get("defaults", {}),
"project": current.get("project", {}), "project": current.get("project", {}),
"materials": current.get("materials", []), "materials": current.get("materials", []),
"wandStyles": current.get("wand_styles", []),
"builtinMaterials": built_in, "builtinMaterials": built_in,
"hatchPatterns": _hatch_pattern_names(doc), "hatchPatterns": _hatch_pattern_names(doc),
"hatchPatternsFull": _list_hatch_patterns_full(doc), "hatchPatternsFull": _list_hatch_patterns_full(doc),
@@ -992,6 +1083,7 @@ class EbenenBridge(panel_base.BaseBridge):
new_settings = { new_settings = {
"defaults": updated.get("defaults", {}), "defaults": updated.get("defaults", {}),
"materials": updated.get("materials", []), "materials": updated.get("materials", []),
"wand_styles": updated.get("wandStyles", []),
"project": updated.get("project", {}), "project": updated.get("project", {}),
} }
save_project_settings(doc2, new_settings) save_project_settings(doc2, new_settings)
+34
View File
@@ -309,6 +309,33 @@ def _load_all(sender, e):
_pb._t_mark("post_init", "unit_check", _t_uc) _pb._t_mark("post_init", "unit_check", _t_uc)
except Exception as ex: except Exception as ex:
print("[STARTUP] unit-check active doc:", ex) print("[STARTUP] unit-check active doc:", ex)
# Aliases + Shortcuts (Defaults aus rhino/aliases/shortcuts_default.json
# + User-Overrides aus dossier_settings.json) registrieren. Idempotent —
# SetMacro/SetMacro ueberschreibt vorhandene Eintraege. Wenn Bridge noch
# nicht in sticky liegt (elemente-Panel noch nicht geladen) ist das ok,
# die Aliases zeigen auf das Dispatch-Skript das die Bridge lazy aus
# sticky liest.
_t_al = _t.time()
try:
from aliases import loader as _alias_loader
_na, _nf, _nc, _ns = _alias_loader.apply_all()
print("[STARTUP] Aliases: {} alias, {} fkey, {} cmd, {} skipped"
.format(_na, _nf, _nc, _ns))
_pb._t_mark("post_init", "aliases", _t_al)
except Exception as ex:
print("[STARTUP] alias-loader:", ex)
# BeginCommand-Hook: Gestaltung-Panel oeffnen bei Drawing-Commands
try:
import begin_cmd_hook
begin_cmd_hook.install(verbose=True)
except Exception as ex:
print("[STARTUP] begin_cmd_hook:", ex)
# Welcome-Screen einmalig pro Version (markiert sich selbst)
try:
import welcome
welcome.show_welcome(force=False)
except Exception as ex:
print("[STARTUP] welcome:", ex)
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden # DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
_hint_dossier_ui() _hint_dossier_ui()
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle- # Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
@@ -341,5 +368,12 @@ def _load_all(sender, e):
print("[STARTUP] Fertig") print("[STARTUP] Fertig")
# Idempotency-Guard: wenn beide Pfade gleichzeitig feuern (C#-Plugin OnLoad
# UND legacy StartupCommands-XML), nur das erste registriert den Idle-Loader.
# Verhindert doppelte Panel-Registrierung + doppelte Listener.
if sc.sticky.get("_dossier_startup_scheduled"):
print("[STARTUP] schon geplant — skip (parallel-aufruf)")
else:
sc.sticky["_dossier_startup_scheduled"] = True
Rhino.RhinoApp.Idle += _load_all Rhino.RhinoApp.Idle += _load_all
print("[STARTUP] geplant - laedt sobald Rhino idle ist") print("[STARTUP] geplant - laedt sobald Rhino idle ist")
+24 -10
View File
@@ -4,19 +4,22 @@
# Copyright (C) 2026 Karim Gabriele Varano # Copyright (C) 2026 Karim Gabriele Varano
""" """
treppe_grips.py treppe_grips.py
Display-Conduit fuer gruene Endpunkt-Marker an Treppen-Achsen. Visuelle Display-Conduit fuer gruene Marker an Treppen-Achsen. Visuelle
Indikation wie bei Waenden, aber keine eigene Drag-Logik der normale Indikation wie bei Waenden, aber keine eigene Drag-Logik der normale
Partnership-Cascade (elemente._on_select_objects) + Pure-Transform-Pfad Partnership-Cascade (elemente._on_select_objects) + Pure-Transform-Pfad
verschieben die Treppe bereits sauber. verschieben die Treppe bereits sauber.
Endpunkt-Logik pro Treppen-Art: Marker-Logik pro Treppen-Art:
- gerade : PointAtStart, PointAtEnd der Linie - gerade : PointAtStart, PointAtEnd der Linie
- L : poly[0] (Start), poly[2] (Ende) poly[1] ist der Eck-Punkt - L (3-Pt): poly[0] (Start), poly[1] (Eck), poly[2] (Ende) alle 3
damit das Eck einzeln gegriffen werden kann
- L (4-Pt): alle 4 Punkte (Start, Lauf1-Ende, Lauf2-Anfang, Ende)
- Wendel : poly[1] (Start), poly[2] (Ende) poly[0] ist Rotations- - Wendel : poly[1] (Start), poly[2] (Ende) poly[0] ist Rotations-
zentrum, nicht der Treppen-Anfang zentrum, nicht der Treppen-Anfang
""" """
import Rhino import Rhino
import Rhino.Display as rd import Rhino.Display as rd
import Rhino.DocObjects as rdoc
import Rhino.Geometry as rg import Rhino.Geometry as rg
import scriptcontext as sc import scriptcontext as sc
import System.Drawing as SD import System.Drawing as SD
@@ -28,8 +31,7 @@ _MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
def _treppe_endpoints(axis_obj): def _treppe_endpoints(axis_obj):
"""Liefert Liste von Point3d fuer Treppen-Start + -Ende. Beachtet """Liefert Liste von Point3d. Beachtet treppe_art + Polyline-Punktzahl."""
treppe_art (Wendel hat anderes Polyline-Schema)."""
if axis_obj is None or axis_obj.IsDeleted: return [] if axis_obj is None or axis_obj.IsDeleted: return []
a = axis_obj.Attributes a = axis_obj.Attributes
if a.GetUserString("dossier_element_type") != "treppe_axis": return [] if a.GetUserString("dossier_element_type") != "treppe_axis": return []
@@ -41,14 +43,26 @@ def _treppe_endpoints(axis_obj):
ok, poly = geom.TryGetPolyline() ok, poly = geom.TryGetPolyline()
if not ok or poly is None or poly.Count != 3: return [] if not ok or poly is None or poly.Count != 3: return []
return [poly[1], poly[2]] return [poly[1], poly[2]]
# gerade + L → Start- und End-Punkt der Curve sind die Treppen-Enden if art == "l":
ok, poly = geom.TryGetPolyline()
if not ok or poly is None: return []
return [poly[i] for i in range(poly.Count)]
return [geom.PointAtStart, geom.PointAtEnd] return [geom.PointAtStart, geom.PointAtEnd]
except Exception: except Exception:
return [] return []
def _enumerator_all():
"""Iterator-Settings die hidden + locked Objekte mit einschliessen —
Mac-Default skipt sonst hidden-Layer-Objekte."""
s = rdoc.ObjectEnumeratorSettings()
s.HiddenObjects = True
s.LockedObjects = True
return s
class _TreppeEndpointConduit(rd.DisplayConduit): class _TreppeEndpointConduit(rd.DisplayConduit):
"""Zeichnet gruene Endpunkt-Marker an allen selektierten Treppen-Achsen.""" """Zeichnet gruene Marker an allen selektierten Treppen-Achsen."""
def DrawForeground(self, e): def DrawForeground(self, e):
try: try:
@@ -60,10 +74,10 @@ class _TreppeEndpointConduit(rd.DisplayConduit):
a = obj.Attributes a = obj.Attributes
eid = a.GetUserString("dossier_element_id") or "" eid = a.GetUserString("dossier_element_id") or ""
if not eid or eid in seen: continue if not eid or eid in seen: continue
# Source-Axis via element_id finden (kann anderer Obj sein # Source-Axis via element_id finden — auch wenn auf hidden
# wenn User nur Volume oder 2D-Symbol selektiert hat) # Layer (User hat z.B. nur 2D-Plansymbol selektiert).
axis = None axis = None
for o in doc.Objects: for o in doc.Objects.GetObjectList(_enumerator_all()):
if o is None or o.IsDeleted: continue if o is None or o.IsDeleted: continue
try: try:
a2 = o.Attributes a2 = o.Attributes
+562
View File
@@ -0,0 +1,562 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
welcome.py
Welcome-Screen + Shortcuts-Cheatsheet als WebView-Dialog im DOSSIER-Style
(passend zum Splashscreen Petrol-Gradient, Mono-Font).
Funktionen:
- show_welcome() erscheint NACH dem Splash (eigener Idle-Timer), einmal
pro Version. User kann "nicht mehr anzeigen" rechts unten anklicken.
- show_cheatsheet() DOSSIER-Shortcut-Liste, aufrufbar via dkeys-Alias.
Marker-Datei fuer "schon gesehen" wird in
~/Library/Application Support/ch.gabrielevarano.Dossier/welcome_shown abgelegt.
"""
import os
import json
import Rhino
DOSSIER_VERSION = "0.6.3"
DOSSIER_GITHUB = "https://github.com/karimgvarano/DOSSIER"
DOSSIER_SUPPORT_EMAIL = "karim@gabrielevarano.ch"
_WELCOME_DIR = os.path.expanduser(
"~/Library/Application Support/ch.gabrielevarano.Dossier")
_WELCOME_FLAG = os.path.join(_WELCOME_DIR, "welcome_shown.txt")
_WELCOME_OPTOUT = os.path.join(_WELCOME_DIR, "welcome_dontshow.txt")
_SPLASH_MIN_DELAY_SEC = 3.5
_HERE = os.path.dirname(os.path.abspath(__file__))
_SHORTCUTS_JSON = os.path.join(_HERE, "aliases", "shortcuts_default.json")
def _has_optout():
return os.path.exists(_WELCOME_OPTOUT)
def _has_seen_version(version):
try:
if not os.path.exists(_WELCOME_FLAG): return False
with open(_WELCOME_FLAG, "r") as f:
return f.read().strip() == version
except Exception:
return False
def _mark_seen(version):
try:
os.makedirs(_WELCOME_DIR, exist_ok=True)
with open(_WELCOME_FLAG, "w") as f:
f.write(version)
except Exception as ex:
print("[WELCOME] mark-seen err:", ex)
def _write_optout():
try:
os.makedirs(_WELCOME_DIR, exist_ok=True)
with open(_WELCOME_OPTOUT, "w") as f:
f.write("1")
except Exception as ex:
print("[WELCOME] optout-write err:", ex)
def _load_shortcuts():
try:
with open(_SHORTCUTS_JSON, "r", encoding="utf-8") as f:
data = json.load(f)
items = []
for k, v in data.items():
if k.startswith("_") or not isinstance(v, dict): continue
items.append({
"id": k,
"trigger": v.get("trigger", ""),
"label": v.get("label", k),
"type": v.get("type", ""),
})
return items
except Exception as ex:
print("[WELCOME] shortcuts-load err:", ex)
return []
# ---- HTML — DOSSIER-Style passend zum Splash ----------------------------
_WELCOME_HTML = """<!DOCTYPE html>
<html lang="de"><head><meta charset="utf-8"/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&family=Playfair+Display:wght@400;500&display=swap" rel="stylesheet"/>
<style>
:root {{
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
--paper: #fff; --paper-mute: rgba(255,255,255,0.78); --paper-faint: rgba(255,255,255,0.5);
--font-display: Krungthep, 'Archivo Black', sans-serif;
--font-serif: 'Playfair Display', serif;
--font-mono: 'DM Mono', 'Menlo', monospace;
}}
* {{ box-sizing:border-box; }}
html, body {{
margin:0; padding:0; width:100%; height:100%; background:transparent !important;
color:var(--paper); overflow:hidden; font-family:var(--font-mono); user-select:none;
-webkit-user-select:none;
}}
.frame {{
box-sizing:border-box; width:100%; height:100%; padding:28px 32px 24px;
display:flex; flex-direction:column;
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
border-radius:16px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
}}
.brand-row {{ display:flex; align-items:baseline; justify-content:space-between; gap:12px; }}
.brand {{
font-family:var(--font-display); font-size:32px; letter-spacing:-0.01em;
line-height:1; color:var(--paper);
}}
.brand-dot {{ color:var(--accent-deep); }}
.version {{
font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em;
color:var(--paper-mute); text-transform:uppercase;
}}
.title {{
font-family:var(--font-serif); font-size:22px; line-height:1.3;
color:var(--paper); margin-top:20px; font-weight:500;
}}
.intro {{
font-size:11px; line-height:1.65; color:var(--paper-mute); margin-top:10px;
letter-spacing:0.02em;
}}
.section-title {{
font-size:9px; letter-spacing:0.18em; text-transform:uppercase;
color:var(--paper-faint); margin:22px 0 10px;
}}
.links {{ display:flex; flex-direction:column; gap:8px; }}
a {{ color:inherit; text-decoration:none; }}
.link {{
display:flex; align-items:flex-start; gap:12px;
padding:10px 14px; border-radius:6px; cursor:pointer;
background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.12);
transition:background 0.15s;
color:var(--paper); text-decoration:none;
}}
.link:hover {{ background:rgba(255,255,255,0.16); }}
.link-icon {{
font-family:var(--font-display); font-size:14px; color:var(--accent-deep);
background:var(--paper); width:24px; height:24px; border-radius:50%;
display:flex; align-items:center; justify-content:center; flex-shrink:0;
margin-top:1px;
}}
.link-content {{ flex:1; min-width:0; }}
.link-title {{ font-size:12px; color:var(--paper); font-weight:500; }}
.link-desc {{ font-size:10px; color:var(--paper-mute); margin-top:2px; }}
kbd {{
background:rgba(0,0,0,0.18); padding:1px 6px; border-radius:3px;
font-family:var(--font-mono); font-size:10px; color:var(--paper);
border:1px solid rgba(255,255,255,0.15);
}}
.footer {{
margin-top:auto; display:flex; align-items:center; justify-content:space-between;
padding-top:18px; gap:12px;
}}
.footer-meta {{
font-size:9px; letter-spacing:0.14em; color:var(--paper-faint);
text-transform:uppercase;
}}
.optout {{
display:flex; align-items:center; gap:6px; cursor:pointer;
font-size:10px; color:var(--paper-mute); user-select:none;
}}
.optout:hover {{ color:var(--paper); }}
.optout input {{ accent-color:var(--paper); margin:0; }}
.win-ctrl {{
position:absolute; top:14px; right:16px; display:flex; gap:6px; z-index:20;
}}
.win-btn {{
width:22px; height:22px; border-radius:50%; cursor:pointer;
display:flex; align-items:center; justify-content:center;
background:rgba(0,0,0,0.18); border:1px solid rgba(255,255,255,0.18);
color:var(--paper); font-family:var(--font-mono); font-size:13px;
text-decoration:none; transition:background 0.12s;
line-height:1; user-select:none;
}}
.win-btn:hover {{ background:rgba(0,0,0,0.32); }}
</style></head><body>
<div class="frame">
<div class="win-ctrl">
<a class="win-btn" href="dossier:close" title="Schliessen">&times;</a>
</div>
<div class="brand-row">
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
<div class="version">Version {ver}</div>
</div>
<div class="title">Willkommen im Studio</div>
<div class="intro">
DOSSIER ist dein Architektur-Studio-Plugin fuer Rhino 8
Waende, Decken, Treppen, Fenster, Tueren, Raumstempel,
Layouts. Alles aus einer Hand, im selben Stil.
</div>
<div class="section-title">Einstieg</div>
<div class="links">
<a class="link" href="dossier:cheatsheet">
<div class="link-icon"></div>
<div class="link-content">
<div class="link-title">Shortcuts &amp; Cheatsheet</div>
<div class="link-desc">Tippe <kbd>dkeys</kbd> im Command-Prompt fuer die volle Liste</div>
</div>
</a>
<a class="link" href="{github}" target="_blank">
<div class="link-icon">i</div>
<div class="link-content">
<div class="link-title">Einfuehrung &amp; Doku</div>
<div class="link-desc">{github}</div>
</div>
</a>
<a class="link" href="{github}/releases" target="_blank">
<div class="link-icon">v</div>
<div class="link-content">
<div class="link-title">Changelog</div>
<div class="link-desc">Was ist neu in dieser Version</div>
</div>
</a>
<a class="link" href="mailto:{email}" target="_blank">
<div class="link-icon">?</div>
<div class="link-content">
<div class="link-title">Support &amp; Problem melden</div>
<div class="link-desc">{email} oder GitHub-Issues</div>
</div>
</a>
</div>
<div class="footer">
<div class="footer-meta">AGPL-3.0 &middot; Karim Gabriele Varano</div>
<label class="optout">
<input type="checkbox" id="optout" onchange="window.location='dossier:optout?'+this.checked"/>
Nicht mehr anzeigen
</label>
</div>
</div>
</body></html>"""
_CHEATSHEET_HTML = """<!DOCTYPE html>
<html><head><meta charset="utf-8"/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet"/>
<style>
:root {{
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
--paper: #fff; --paper-mute: rgba(255,255,255,0.78); --paper-faint: rgba(255,255,255,0.5);
--font-display: Krungthep, 'Archivo Black', sans-serif;
--font-mono: 'DM Mono', 'Menlo', monospace;
}}
* {{ box-sizing:border-box; }}
html, body {{
margin:0; padding:0; width:100%; height:100%; background:transparent !important;
color:var(--paper); overflow:auto; font-family:var(--font-mono);
}}
.frame {{
box-sizing:border-box; min-height:100%; padding:24px 28px;
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
border-radius:16px;
}}
.brand-row {{ display:flex; align-items:baseline; justify-content:space-between; gap:12px; }}
.brand {{ font-family:var(--font-display); font-size:24px; line-height:1; color:var(--paper); }}
.brand-dot {{ color:var(--accent-deep); }}
.version {{ font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em; color:var(--paper-mute); text-transform:uppercase; }}
h2 {{
font-size:10px; letter-spacing:0.18em; color:var(--paper); margin:18px 0 8px;
text-transform:uppercase; font-weight:500;
}}
table {{ width:100%; border-collapse:collapse; }}
td {{ padding:5px 8px; border-bottom:1px solid rgba(255,255,255,0.12); vertical-align:middle; }}
td:first-child {{ width:170px; }}
kbd {{
background:rgba(0,0,0,0.18); padding:2px 8px; border-radius:3px;
font-family:var(--font-mono); font-size:11px; color:var(--paper);
border:1px solid rgba(255,255,255,0.18);
}}
.lab {{ color:var(--paper); font-size:11px; }}
.badge {{
font-size:9px; padding:1px 5px; border-radius:2px; margin-left:6px;
background:rgba(255,255,255,0.12); color:var(--paper-mute);
font-family:var(--font-mono);
}}
.win-ctrl {{
position:fixed; top:14px; right:18px; display:flex; gap:6px; z-index:20;
}}
.win-btn {{
width:22px; height:22px; border-radius:50%; cursor:pointer;
display:flex; align-items:center; justify-content:center;
background:rgba(0,0,0,0.22); border:1px solid rgba(255,255,255,0.18);
color:var(--paper); font-family:var(--font-mono); font-size:13px;
text-decoration:none; transition:background 0.12s;
line-height:1; user-select:none;
}}
.win-btn:hover {{ background:rgba(0,0,0,0.38); }}
</style></head><body>
<div class="frame">
<div class="win-ctrl">
<a class="win-btn" href="dossier:back" title="Zurueck">&lsaquo;</a>
<a class="win-btn" href="dossier:close" title="Schliessen">&times;</a>
</div>
<div class="brand-row">
<div class="brand">DOSSIER<span class="brand-dot">.</span> Shortcuts</div>
<div class="version">v {ver}</div>
</div>
{sections}
</div></body></html>"""
def _build_cheatsheet_html():
items = _load_shortcuts()
groups = {
"DOSSIER BIM": [],
"2D-Werkzeuge": [],
"Views & Navigation": [],
"Modify-Tools": [],
"Sonstige Aliases": [],
}
bim_ids = {"wand", "tuer", "fenster", "decke", "treppe", "stuetze",
"traeger", "raum", "symbol", "stempel", "dach", "aussparung"}
view_ids = {"view_plan", "view_3d", "view_material", "zoom_ext",
"zoom_sel", "geschoss_up", "geschoss_down",
"panel_layer", "panel_elemente"}
twod_ids = {"text", "line", "arc", "rectangle", "polyline", "curve",
"hatch", "polygon", "ellipse", "circle"}
for it in items:
i = it["id"]
if i in bim_ids: groups["DOSSIER BIM"].append(it)
elif i in view_ids: groups["Views & Navigation"].append(it)
elif i.startswith("mod_"): groups["Modify-Tools"].append(it)
elif i in twod_ids or i.endswith("_alias"): groups["2D-Werkzeuge"].append(it)
else: groups["Sonstige Aliases"].append(it)
def _row(it):
trig = it["trigger"]
trig = trig.replace("Cmd+", "⌘+").replace("Shift+", "⇧+").replace("Alt+", "⌥+")
return ('<tr><td><kbd>{}</kbd></td>'
'<td class="lab">{}</td></tr>'
.format(trig, it["label"]))
sections = []
for gname, gitems in groups.items():
if not gitems: continue
rows = "".join(_row(it) for it in gitems)
sections.append('<h2>{}</h2><table>{}</table>'.format(gname, rows))
return _CHEATSHEET_HTML.format(ver=DOSSIER_VERSION, sections="".join(sections))
# ---- Dialog-Anzeige ------------------------------------------------------
def _try_borderless_mac(form):
"""Borderless NSWindow + transparenten Hintergrund (analog _startup_splash)."""
try:
import System
nsw = getattr(form, "ControlObject", None)
if nsw is None: return False
# StyleMask = 0 (Borderless)
try:
cur = nsw.StyleMask
nsw.StyleMask = System.Enum.ToObject(type(cur), 0)
except Exception as ex:
print("[WELCOME] StyleMask:", ex)
# Transparent background damit border-radius vom HTML sichtbar
for prop, val in [("TitlebarAppearsTransparent", True),
("IsOpaque", False), ("HasShadow", True),
("MovableByWindowBackground", True)]:
try: setattr(nsw, prop, val)
except Exception: pass
try:
tv_type = type(nsw.TitleVisibility)
nsw.TitleVisibility = System.Enum.ToObject(tv_type, 1)
except Exception: pass
try:
from AppKit import NSColor as _NSC
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
if clear is not None: nsw.BackgroundColor = clear
except Exception: pass
return True
except Exception as ex:
print("[WELCOME] borderless:", ex)
return False
def _webview_transparent(web):
"""WKWebView vollstaendig transparent — KVC drawsBackground=NO,
UnderPageBackgroundColor=Clear, Layer.BackgroundColor=CGColor.Clear."""
wk = getattr(web, "ControlObject", None)
if wk is None: return
try:
from Foundation import NSNumber, NSString
try: wk.SetValueForKey(NSNumber.FromBoolean(False), NSString("drawsBackground"))
except Exception as ex: print("[WELCOME] KVC drawsBackground:", ex)
except Exception as ex: print("[WELCOME] Foundation:", ex)
try:
from AppKit import NSColor as _NSC
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
if clear is not None:
try: wk.UnderPageBackgroundColor = clear
except Exception: pass
try:
layer = getattr(wk, "Layer", None)
if layer is not None:
layer.BackgroundColor = clear.CGColor
layer.Opaque = False
except Exception as ex: print("[WELCOME] Layer:", ex)
except Exception as ex: print("[WELCOME] NSColor:", ex)
def _show_html_form(title, html, width=620, height=720, on_navigating=None,
borderless=True):
"""Eto.Forms.Form mit WebView + Inline-HTML. Optional borderless +
Navigation-Hook fuer custom URL-Schemes."""
try:
import Eto.Forms as ef
import Eto.Drawing as ed
except Exception as ex:
print("[WELCOME] Eto.Forms nicht verfuegbar:", ex)
return None
try:
form = ef.Form()
form.Title = title
form.ClientSize = ed.Size(width, height)
form.Topmost = False
form.Resizable = False
if borderless:
try: form.WindowStyle = getattr(ef.WindowStyle, "None")
except Exception: pass
for attr, val in (("Minimizable", False), ("Maximizable", False),
("Closeable", False), ("ShowInTaskbar", False)):
try: setattr(form, attr, val)
except Exception: pass
try: form.BackgroundColor = ed.Colors.Transparent
except Exception: pass
web = ef.WebView()
web.Size = ed.Size(width, height)
if borderless:
try: web.BackgroundColor = ed.Colors.Transparent
except Exception: pass
if on_navigating is not None:
try: web.DocumentLoading += on_navigating
except Exception as ex: print("[WELCOME] nav-hook:", ex)
try: web.LoadHtml(html)
except Exception as e: print("[WELCOME] LoadHtml:", e)
form.Content = web
try: form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
except Exception: pass
form.Show()
if borderless:
_try_borderless_mac(form)
_webview_transparent(web)
try: ef.Application.Instance.RunIteration()
except Exception: pass
return form
except Exception as ex:
print("[WELCOME] form show:", ex)
return None
def show_welcome(force=False):
"""Zeigt Welcome NACH Splash. Erscheint bei jedem Start ausser der
User klickt 'Nicht mehr anzeigen' (= optout-File).
WICHTIG: UI muss auf Main-Thread laufen (Mac Cocoa) Rhino-Idle-Event
feuert dort, deshalb defern wir die Anzeige."""
if not force and _has_optout():
print("[WELCOME] optout aktiv ({}) — skip".format(_WELCOME_OPTOUT))
return
print("[WELCOME] geplant — Anzeige nach Splash (>{:.1f}s)".format(_SPLASH_MIN_DELAY_SEC))
import time
state = {"start": time.time(), "fired": False}
def _on_idle(sender, e):
if state["fired"]: return
if time.time() - state["start"] < _SPLASH_MIN_DELAY_SEC: return
state["fired"] = True
try:
Rhino.RhinoApp.Idle -= _on_idle
except Exception: pass
try:
print("[WELCOME] Anzeige starten")
_show_welcome_now()
except Exception as ex:
print("[WELCOME] show err:", ex)
try:
Rhino.RhinoApp.Idle += _on_idle
except Exception as ex:
print("[WELCOME] idle-hook err:", ex)
def _show_welcome_now():
html = _WELCOME_HTML.format(
ver=DOSSIER_VERSION, github=DOSSIER_GITHUB, email=DOSSIER_SUPPORT_EMAIL)
form_ref = [None]
def _on_nav(sender, e):
try:
url = e.Uri.ToString() if hasattr(e, "Uri") else str(getattr(e, "Url", ""))
except Exception:
url = ""
if not url: return
if url.startswith("dossier:optout"):
# Optout-Checkbox-Klick. URL-Form: dossier:optout?true/false
checked = url.endswith("true")
if checked: _write_optout()
else:
try:
if os.path.exists(_WELCOME_OPTOUT):
os.remove(_WELCOME_OPTOUT)
except Exception: pass
try: e.Cancel = True
except Exception: pass
elif url.startswith("dossier:cheatsheet"):
try: e.Cancel = True
except Exception: pass
show_cheatsheet()
try:
if form_ref[0] is not None: form_ref[0].Close()
except Exception: pass
elif url.startswith("dossier:close"):
try: e.Cancel = True
except Exception: pass
try:
if form_ref[0] is not None: form_ref[0].Close()
except Exception: pass
form_ref[0] = _show_html_form("Willkommen bei DOSSIER", html, 600, 620,
on_navigating=_on_nav)
def show_cheatsheet():
html = _build_cheatsheet_html()
form_ref = [None]
def _on_nav(sender, e):
try:
url = e.Uri.ToString() if hasattr(e, "Uri") else str(getattr(e, "Url", ""))
except Exception:
url = ""
if not url: return
if url.startswith("dossier:close"):
try: e.Cancel = True
except Exception: pass
try:
if form_ref[0] is not None: form_ref[0].Close()
except Exception: pass
elif url.startswith("dossier:back"):
try: e.Cancel = True
except Exception: pass
try:
if form_ref[0] is not None: form_ref[0].Close()
except Exception: pass
try: _show_welcome_now()
except Exception as ex: print("[WELCOME] back:", ex)
form_ref[0] = _show_html_form("DOSSIER Shortcuts", html, 640, 760,
on_navigating=_on_nav)
if __name__ == "__main__":
show_cheatsheet()
@@ -0,0 +1,561 @@
<?xml version="1.0" encoding="utf-8"?>
<!--This file was generated by Rhinoceros, DO NOT modify this file-->
<RhinoUI major_ver="1" minor_ver="0" created_on_platform="Mac" created_on_rhino_version="8.31.26126.13432" name="DOSSIERUIV0.2">
<!--Window Layout display name for this file-->
<name>
<locale_1033>DOSSIERUIV0.2</locale_1033>
</name>
<!--Tab panel collection definitions and dock bar placement information-->
<dock_bars major_ver="1" minor_ver="0">
<dock_bar guid="035c14a5-6fa6-4e54-9769-5db41f770a3c">
<placement dock_location="Top" recent_dock_location="Top" dock_band_size="-1,62" dock_band_item_size="1,-1" docked_placement="0,0" float_point="610,52" float_size="200,200" visible="True" />
<tabs name="Oberleiste" selected_item="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" display_style="Text">
<name>
<locale_1033>Oberleiste</locale_1033>
</name>
<panel guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Oberleiste" />
</tabs>
</dock_bar>
<dock_bar guid="044781f9-dc80-4398-8498-e14c27c44423">
<placement dock_band_size="230,-1" dock_band_item_size="-1,0.5" docked_placement="0,1" float_point="1235,903" float_size="200,200" visible="True" />
<tabs name="Ebenen" selected_item="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" display_style="Bitmap">
<name>
<locale_1033>Ebenen</locale_1033>
</name>
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Zeichnungsebenen" />
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Ebenen" />
<panel guid="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Ausschnitte" />
<panel guid="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Layouts" />
</tabs>
</dock_bar>
<dock_bar guid="171011a9-a956-41ee-853e-3ccc0c0db1d8" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="ac3566f9-fe75-4258-9210-b1e9c05a5881">
<placement dock_location="Floating" recent_dock_location="Top" dock_band_size="36,48" dock_band_item_size="1,-1" docked_placement="0,0" float_point="317,336" float_size="1000,75" />
<tabs name="Standard Toolbars" selected_item="4bb9c817-d19f-45fd-8af2-39e9805f3e9f" display_style="BitmapAndText">
<name>
<locale_1033>Standard Toolbars</locale_1033>
<locale_1029>Standardní palety nástrojů</locale_1029>
<locale_1031>Standard-Werkzeugleisten</locale_1031>
<locale_1034>Barras de herramientas estándar</locale_1034>
<locale_1036>Barres d'outils Standard</locale_1036>
<locale_1040>Barre degli strumenti standard</locale_1040>
<locale_1041>標準ツールバー</locale_1041>
<locale_1042>표준 도구모음</locale_1042>
<locale_1045>Standardowe paski narzędzi</locale_1045>
<locale_2070>Barras de Ferramentas Standard</locale_2070>
<locale_2052>标准工具列</locale_2052>
<locale_1028>標準工具列</locale_1028>
<locale_1049>Стандартные панели инструментов</locale_1049>
</name>
<tool_bar guid="4bb9c817-d19f-45fd-8af2-39e9805f3e9f" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="32318c40-46e9-4aa3-8f73-09371ec27a4d" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="2ed87e2b-d225-4625-aeda-b11a115c9a14" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="720d3154-34fc-4177-8e52-9f417d4b5af3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="79d0d952-85af-4fe3-8444-46596bbe22fd" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="9772529f-ea7a-4e2f-9dab-5beff3ce96e8" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="0608110b-184c-443f-97ec-0612d9d2b605" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="9f21066b-eb0f-46f5-adeb-d62f80e04fd7" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="d767ebe9-eebd-4e75-a217-03f7431f71bf" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar_colletion="4b37b788-3a60-4c54-a4be-67e756115945" />
<tool_bar guid="b977d038-c9b6-4a9b-b097-9592a4117052" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="c8064867-a611-4b55-b747-fc2aee5d9bf3" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="4cd9a071-9337-4389-aa40-2a20f570da3b" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="625e34b2-3b19-4aba-91c2-94f79e2e1d91" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="44619cf6-b73a-46ea-93f8-46f1fa333115" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="12428ca8-b9f4-4954-8f6e-8a139226b383" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="a5379863-ee51-4b77-ab0c-44ee93c92ca3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="fa6d5bcc-8cd8-416b-8701-f89bd697e94d" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="d0a817a1-dea9-4e03-89e3-0d63d99b5e51" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="54dcdb3a-f098-49a4-9947-d5605d675be3" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="90fd89fc-e41f-49cc-bcf0-29e0d58017a1" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar_colletion="4b37b788-3a60-4c54-a4be-67e756115945" />
<tool_bar guid="16770b13-f7fb-4060-a6e6-607f90dd8bb3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="26b86ed9-12ff-4198-b49e-83bd3dfa7480" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="12428ca8-b9f4-4954-8f6e-8a139226b383">
<placement dock_location="Floating" recent_dock_location="Left" float_point="-1431655808,0" float_size="266,255" />
<tabs name="SubD Sidebar" selected_item="aaa213c9-f422-477a-9782-a6ae60104171" display_style="BitmapAndText">
<name>
<locale_1033>SubD Sidebar</locale_1033>
<locale_1029>Postranní panel SubD</locale_1029>
<locale_1031>SubD-Seitenleiste</locale_1031>
<locale_1034>SubD (lateral)</locale_1034>
<locale_1036>Volet SubD</locale_1036>
<locale_1040>Barra laterale SubD</locale_1040>
<locale_1041>SubDサイドバー</locale_1041>
<locale_1042>SubD 사이드바</locale_1042>
<locale_1045>SubD - pasek boczny</locale_1045>
<locale_2070>Barra Lateral SubD</locale_2070>
<locale_2052>细分边栏</locale_2052>
<locale_1028>SubD 邊欄</locale_1028>
<locale_1049>SubD - боковая</locale_1049>
</name>
<tool_bar guid="aaa213c9-f422-477a-9782-a6ae60104171" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="2e388a71-8d04-4d88-b545-9c714b2cc497">
<placement dock_location="Floating" float_point="952,182" float_size="400,800" />
<tabs name="Layers" selected_item="3610bf83-047d-4f7f-93fd-163ea305b493" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Layers</locale_1033>
</name>
<panel guid="3610bf83-047d-4f7f-93fd-163ea305b493" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Layers" />
</tabs>
</dock_bar>
<dock_bar guid="303293c9-aad0-4419-994d-6765718a58ed" unmanaged="True" text="Command">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="260,200" dock_band_item_size="-1,0.33333334" docked_placement="0,0" float_point="1084,409" float_size="200,200" visible="True" />
<tabs name="Container" selected_item="971fdb61-b9c6-4080-b38f-d5c72ce7a577" display_style="Bitmap">
<name>
<locale_1033>Container</locale_1033>
</name>
<tool_bar guid="971fdb61-b9c6-4080-b38f-d5c72ce7a577" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="3443edbb-66f4-460f-97b8-1008a2a31bc5">
<placement dock_location="Floating" float_point="1059,182" float_size="400,800" />
<tabs name="Display" selected_item="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Display</locale_1033>
</name>
<panel guid="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" name="Display" />
</tabs>
</dock_bar>
<dock_bar guid="39716459-9788-492d-adfd-8d89a7585363">
<placement dock_band_size="494,-1" dock_band_item_size="-1,1" docked_placement="0,0" float_point="2113,787" float_size="396,796" />
<tabs name="Display &amp; Rendering" selected_item="d9ac0269-811b-47d1-aa33-777986b13715" can_be_empty="True" display_style="Bitmap">
<name>
<locale_1033>Display &amp; Rendering</locale_1033>
<locale_1029>Zobrazení a renderování</locale_1029>
<locale_1031>Anzeige und Rendering</locale_1031>
<locale_1034>Visualización y renderizado</locale_1034>
<locale_1036>Affichage et rendu</locale_1036>
<locale_1040>Visualizzazione e rendering</locale_1040>
<locale_1041>表示 &amp; レンダリング</locale_1041>
<locale_1042>표시와 렌더링</locale_1042>
<locale_1045>Wyświetlanie i rendering</locale_1045>
<locale_2070>Visualização e Renderização</locale_2070>
<locale_2052>显示 &amp; 渲染</locale_2052>
<locale_1028>顯示 &amp; 彩現</locale_1028>
<locale_1049>Отображение и визуализация</locale_1049>
</name>
<panel guid="d9ac0269-811b-47d1-aa33-777986b13715" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Rendering" />
<panel guid="6df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Materials" />
</tabs>
</dock_bar>
<dock_bar guid="4711e685-da6b-40e7-8da8-725c8f104065" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="625e34b2-3b19-4aba-91c2-94f79e2e1d91">
<placement dock_location="Floating" recent_dock_location="Top" float_point="-1431655808,0" float_size="276,216" />
<tabs name="Solids Sidebar" selected_item="f1c8658b-c36e-4835-9492-14fce56924db" display_style="BitmapAndText">
<name>
<locale_1033>Solids Sidebar</locale_1033>
<locale_1029>Postranní panel Tělesa</locale_1029>
<locale_1031>Volumenkörper-Seitenleiste</locale_1031>
<locale_1034>Sólidos (lateral)</locale_1034>
<locale_1036>Volet Solides</locale_1036>
<locale_1040>Barra laterale Solidi</locale_1040>
<locale_1041>ソリッドサイドバー</locale_1041>
<locale_1042>솔리드 사이드바</locale_1042>
<locale_1045>Bryły - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Sólidos</locale_2070>
<locale_2052>实体边栏</locale_2052>
<locale_1028>實體邊欄</locale_1028>
<locale_1049>Тела - боковая</locale_1049>
</name>
<tool_bar guid="f1c8658b-c36e-4835-9492-14fce56924db" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="4794a6f6-4985-4003-937c-3e6498131d0c">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Elemente" selected_item="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" display_style="BitmapAndText">
<name>
<locale_1033>Elemente</locale_1033>
</name>
<panel guid="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Elemente" />
</tabs>
</dock_bar>
<dock_bar guid="4b37b788-3a60-4c54-a4be-67e756115945">
<placement dock_location="Floating" float_point="-1400,688" float_size="200,200" />
<tabs name="Curve drawing sidebar" selected_item="1990cfb7-2241-4dd1-b01d-735fe3be65fb" can_be_empty="True" display_style="BitmapAndText">
<name>
<locale_1033>Curve drawing sidebar</locale_1033>
<locale_1029>Postranní panel Křivka</locale_1029>
<locale_1031>Kurvenzeichnung-Seitenleiste</locale_1031>
<locale_1034>Dibujo de curvas (lateral)</locale_1034>
<locale_1036>Volet Dessin de courbes</locale_1036>
<locale_1040>Barra laterale disegno curve</locale_1040>
<locale_1041>曲線作成サイドバー</locale_1041>
<locale_1042>커브 그리기 사이드바</locale_1042>
<locale_1045>Rysowanie krzywych - pasek boczny</locale_1045>
<locale_2070>Barra lateral de desenhar curva</locale_2070>
<locale_2052>曲线绘制边栏</locale_2052>
<locale_1028>繪製曲線邊欄</locale_1028>
<locale_1049>Кривые - боковая</locale_1049>
</name>
<tool_bar guid="1990cfb7-2241-4dd1-b01d-735fe3be65fb" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="50eb6c7b-0be0-4166-ad17-ea694c97a6f4" text="Drag Strength">
<placement dock_location="Floating" dock_band_size="222,69" float_point="500,400" float_size="230,77" />
</dock_bar>
<dock_bar guid="5558eff8-695e-4524-9474-ae4a8c8ad07a">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Oberleiste" selected_item="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" display_style="BitmapAndText">
<name>
<locale_1033>Oberleiste</locale_1033>
</name>
<panel guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Oberleiste" />
</tabs>
</dock_bar>
<dock_bar guid="56e27247-204f-410f-879e-defcfd414a05">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Werkzeuge" selected_item="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" display_style="BitmapAndText">
<name>
<locale_1033>Werkzeuge</locale_1033>
</name>
<panel guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Werkzeuge" />
</tabs>
</dock_bar>
<dock_bar guid="5a5b89a0-830f-c90d-5f60-35c2e907e18c">
<placement dock_band_size="230,200" dock_band_item_size="-1,0.5" docked_placement="0,0" float_point="1612,998" float_size="400,800" visible="True" />
<tabs name="Right Container" selected_item="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" display_style="Bitmap">
<name>
<locale_1033>Right Container</locale_1033>
<locale_1029>Pravý kontejner</locale_1029>
<locale_1031>Rechter Container</locale_1031>
<locale_1034>Contenedor derecho</locale_1034>
<locale_1036>Conteneur droit</locale_1036>
<locale_1040>Contenitore destro</locale_1040>
<locale_1041>右コンテナ</locale_1041>
<locale_1042>오른쪽 컨테이너</locale_1042>
<locale_1045>Prawy zbiornik</locale_1045>
<locale_2070>Contentor Direito</locale_2070>
<locale_2052>右侧容器</locale_2052>
<locale_1028>右側容器</locale_1028>
<locale_1049>Правый контейнер</locale_1049>
</name>
<removed_item Item1="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" Item2="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" />
<removed_item Item1="3610bf83-047d-4f7f-93fd-163ea305b493" Item2="3610bf83-047d-4f7f-93fd-163ea305b493" />
<removed_item Item1="34ffb674-c504-49d9-9fcd-99cc811dcda2" Item2="34ffb674-c504-49d9-9fcd-99cc811dcda2" />
<removed_item Item1="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" Item2="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" />
<panel guid="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Dimensionen" />
</tabs>
</dock_bar>
<dock_bar guid="5bea5d56-1152-426e-9dcc-b542508bc7e6">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Gestaltung" selected_item="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" display_style="BitmapAndText">
<name>
<locale_1033>Gestaltung</locale_1033>
</name>
<panel guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Gestaltung" />
</tabs>
</dock_bar>
<dock_bar guid="6c5bbb6d-2e64-426b-b956-dc4a8d1e90eb">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Zeichnungsebenen" selected_item="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" display_style="BitmapAndText">
<name>
<locale_1033>Zeichnungsebenen</locale_1033>
</name>
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Zeichnungsebenen" />
</tabs>
</dock_bar>
<dock_bar guid="78ce0f5a-73ce-4268-b6c9-7d815cfc9f04" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="54dcdb3a-f098-49a4-9947-d5605d675be3">
<placement dock_location="Floating" recent_dock_location="Top" float_point="500,215" float_size="276,258" />
<tabs name="Render Sidebar" selected_item="ee7a5f30-1d7c-4a0f-9ebc-138d9c096aeb" display_style="BitmapAndText">
<name>
<locale_1033>Render Sidebar</locale_1033>
<locale_1029>Postranní panel Render</locale_1029>
<locale_1031>Render-Seitenleiste</locale_1031>
<locale_1034>Renderizado (lateral)</locale_1034>
<locale_1036>Volet Rendu</locale_1036>
<locale_1040>Barra laterale Rendering</locale_1040>
<locale_1041>レンダリングサイドバー</locale_1041>
<locale_1042>렌더링 사이드바</locale_1042>
<locale_1045>Rendering - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Renderizar</locale_2070>
<locale_2052>渲染边栏</locale_2052>
<locale_1028>彩現邊欄</locale_1028>
<locale_1049>Визуализация - боковая</locale_1049>
</name>
<tool_bar guid="ee7a5f30-1d7c-4a0f-9ebc-138d9c096aeb" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="78dafcee-5369-4c51-8325-ce2334941ad3">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Ebenen" selected_item="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" display_style="BitmapAndText">
<name>
<locale_1033>Ebenen</locale_1033>
</name>
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Ebenen" />
</tabs>
</dock_bar>
<dock_bar guid="84ddf9ff-a9d5-42a3-ac2e-6a28f2cc2f11">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Ausschnitte" selected_item="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" display_style="BitmapAndText">
<name>
<locale_1033>Ausschnitte</locale_1033>
</name>
<panel guid="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Ausschnitte" />
</tabs>
</dock_bar>
<dock_bar guid="87c7528a-a0a6-4245-837a-753237bfe57d">
<placement dock_location="Floating" float_point="869,182" float_size="400,800" />
<tabs name="Help 01" selected_item="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Help 01</locale_1033>
</name>
<panel guid="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Help" />
</tabs>
</dock_bar>
<dock_bar guid="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c">
<placement dock_band_size="351,-1" dock_band_item_size="-1,0.5" docked_placement="1,1" float_point="2179,1370" float_size="396,796" />
<tabs name="Named Views" selected_item="7df2a957-f12d-42ea-9fa6-95d7920c1b76" torn_off="True" display_style="Bitmap">
<name>
<locale_1033>Named Views</locale_1033>
<locale_1029>Pojmenované pohledy</locale_1029>
<locale_1031>Benannte Ansichten</locale_1031>
<locale_1034>Vistas guardadas</locale_1034>
<locale_1036>Vues nommées</locale_1036>
<locale_1040>Viste con nome</locale_1040>
<locale_1041>名前の付いたビュー</locale_1041>
<locale_1042>명명된 뷰</locale_1042>
<locale_1045>Nazwane widoki</locale_1045>
<locale_2070>Vistas Com Nome</locale_2070>
<locale_2052>已命名视图</locale_2052>
<locale_1028>已命名視圖</locale_1028>
<locale_1049>Именованные виды</locale_1049>
</name>
<panel guid="77d33034-194d-4cd5-957c-730d9a9eac50" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Named Views" />
<panel guid="7df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Environments" />
<panel guid="987b1930-ecde-4e62-8282-97ab4ad325fe" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Ground Plane" />
<panel guid="1012681e-d276-49d3-9cd9-7de92dc2404a" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Sun" />
<panel guid="8df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Textures" />
<panel guid="f4424a46-8281-430a-b03d-911dc9b40294" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Snapshots" />
<panel guid="86777b3d-3d68-4965-84f8-9e019c402433" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Lights" />
<panel guid="b70a4973-99ca-40c0-b2b2-f03417a5ff1d" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Render Libraries" />
</tabs>
</dock_bar>
<dock_bar guid="950872d7-94f3-41cb-a58e-ad95d3b7bde9">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Dimensionen" selected_item="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" display_style="BitmapAndText">
<name>
<locale_1033>Dimensionen</locale_1033>
</name>
<panel guid="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Dimensionen" />
</tabs>
</dock_bar>
<dock_bar guid="96a58830-8ba7-4e11-aecc-86793727b1b5">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="260,-1" dock_band_item_size="-1,0.64855075" docked_placement="0,1" float_point="-115,931" float_size="200,200" visible="True" />
<tabs name="Gestaltung" selected_item="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" display_style="Bitmap">
<name>
<locale_1033>Gestaltung</locale_1033>
</name>
<panel guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Gestaltung" />
<panel guid="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Elemente" />
<panel guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Werkzeuge" />
</tabs>
</dock_bar>
<dock_bar guid="a4206be9-c9bf-4cbc-a80d-00369bbb5392" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="c8064867-a611-4b55-b747-fc2aee5d9bf3">
<placement dock_location="Floating" recent_dock_location="Top" float_point="-1431655808,0" float_size="276,258" />
<tabs name="Surface Sidebar" selected_item="f6534c6e-b451-4c7a-b8ec-8c3ff3596913" display_style="BitmapAndText">
<name>
<locale_1033>Surface Sidebar</locale_1033>
<locale_1029>Postranní panel Plocha</locale_1029>
<locale_1031>Flächen-Seitenleiste</locale_1031>
<locale_1034>Superficies (lateral)</locale_1034>
<locale_1036>Volet Surface</locale_1036>
<locale_1040>Barra laterale Superfici</locale_1040>
<locale_1041>サーフェスサイドバー</locale_1041>
<locale_1042>서피스 사이드바</locale_1042>
<locale_1045>Powierzchnia - pasek boczny</locale_1045>
<locale_2070>Superfícies</locale_2070>
<locale_2052>曲面边栏</locale_2052>
<locale_1028>曲面邊欄</locale_1028>
<locale_1049>Поверхности - боковая</locale_1049>
</name>
<tool_bar guid="f6534c6e-b451-4c7a-b8ec-8c3ff3596913" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="260,16" dock_band_item_size="1,0.018115938" docked_placement="0,2" float_point="56,924" float_size="200,200" visible="True" />
<tabs name="Command History" selected_item="1d3d1785-2332-428b-a838-b2fe39ec50f4" display_style="Bitmap">
<name>
<locale_1033>Command History</locale_1033>
<locale_1042>명령 히스토리</locale_1042>
<locale_1041>コマンドヒストリ</locale_1041>
<locale_1049>История команд</locale_1049>
<locale_1031>Befehlsverlauf</locale_1031>
<locale_2052>指令历史</locale_2052>
<locale_1034>Historial de comandos</locale_1034>
<locale_1036>Historique des commandes</locale_1036>
<locale_1028>指令歷史</locale_1028>
<locale_1045>Historia poleceń</locale_1045>
<locale_1040>Storico comandi</locale_1040>
<locale_1029>Historie příkazů</locale_1029>
<locale_2070>Histórico de Comandos</locale_2070>
</name>
<panel guid="1d3d1785-2332-428b-a838-b2fe39ec50f4" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Command History" />
</tabs>
</dock_bar>
<dock_bar guid="be2f31c8-f79d-4ca9-953e-8ab3b8cd10e6">
<placement dock_location="Floating" float_point="910,176" float_size="92,116" />
<tabs name="Help" selected_item="2337f242-b576-41a4-aace-4a74772bc72e" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Help</locale_1033>
<locale_1042>도움말</locale_1042>
<locale_1041>ヘルプ</locale_1041>
<locale_1049>Справка</locale_1049>
<locale_1031>Hilfe</locale_1031>
<locale_2052>说明</locale_2052>
<locale_1034>Ayuda</locale_1034>
<locale_1036>Aide</locale_1036>
<locale_1028>說明</locale_1028>
<locale_1045>Pomoc</locale_1045>
<locale_1040>Aiuti</locale_1040>
<locale_1029>Nápověda</locale_1029>
<locale_2070>Ajuda</locale_2070>
</name>
<tool_bar guid="2337f242-b576-41a4-aace-4a74772bc72e" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="cdf21a5a-de32-48b1-8a6a-148a3564719a">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Layouts" selected_item="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" display_style="BitmapAndText">
<name>
<locale_1033>Layouts</locale_1033>
</name>
<panel guid="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Layouts" />
</tabs>
</dock_bar>
<dock_bar guid="d1225ee0-cf16-4f05-b053-60aa46183ac0" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="42b36785-e767-4c66-8704-4d828bcb0225">
<placement dock_location="Left" recent_dock_location="Left" float_point="-1431655808,0" float_size="276,300" />
<tabs name="Main" selected_item="971fdb61-b9c6-4080-b38f-d5c72ce7a577" display_style="Bitmap">
<name>
<locale_1033>Main</locale_1033>
<locale_1029>Hlavní</locale_1029>
<locale_1031>Haupt</locale_1031>
<locale_1034>Principal</locale_1034>
<locale_1036>Principale</locale_1036>
<locale_1040>Principale</locale_1040>
<locale_1041>メイン</locale_1041>
<locale_1042>메인</locale_1042>
<locale_1045>Główne</locale_1045>
<locale_2070>Principal</locale_2070>
<locale_2052>主要</locale_2052>
<locale_1028>主要</locale_1028>
<locale_1049>Главная</locale_1049>
</name>
<tool_bar guid="971fdb61-b9c6-4080-b38f-d5c72ce7a577" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38">
<placement dock_location="Floating" recent_dock_location="Left" dock_band_size="126,22" dock_band_item_size="1,0.24908425" docked_placement="0,1" float_point="370,371" float_size="219,279" />
<tabs name="OSnap" selected_item="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" display_style="BitmapAndText">
<name>
<locale_1033>OSnap</locale_1033>
<locale_1029>Uchop</locale_1029>
<locale_1031>Ofang</locale_1031>
<locale_1034>RefObj</locale_1034>
<locale_1036>Accrochages</locale_1036>
<locale_1040>Osnap</locale_1040>
<locale_1041>OSnap</locale_1041>
<locale_1042>개체스냅</locale_1042>
<locale_1045>UchwytOb</locale_1045>
<locale_2070>OSnap</locale_2070>
<locale_2052>物件锁点</locale_2052>
<locale_1028>物件鎖點</locale_1028>
<locale_1049>Привязка</locale_1049>
</name>
<panel guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Osnap" />
<panel guid="918191ca-1105-43f9-a34a-dda4276883c1" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Selection Filters" />
</tabs>
</dock_bar>
<dock_bar guid="e6d78882-4e26-47c1-9e3e-4be7574dec59">
<placement dock_location="Floating" float_point="1065,182" float_size="400,800" />
<tabs name="Properties" selected_item="34ffb674-c504-49d9-9fcd-99cc811dcda2" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Properties</locale_1033>
</name>
<panel guid="34ffb674-c504-49d9-9fcd-99cc811dcda2" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Properties" />
</tabs>
</dock_bar>
<dock_bar guid="f8acd7e3-6464-4fe1-93ee-87c36d31880f" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="fa6d5bcc-8cd8-416b-8701-f89bd697e94d">
<placement dock_location="Floating" recent_dock_location="Top" float_point="128,256" float_size="234,174" />
<tabs name="Mesh Sidebar" selected_item="c3eab84e-9994-4c04-b2ef-aa4060f96168" display_style="BitmapAndText">
<name>
<locale_1033>Mesh Sidebar</locale_1033>
<locale_1029>Postranní panel Síť</locale_1029>
<locale_1031>Polygonnetz-Seitenleiste</locale_1031>
<locale_1034>Malla (lateral)</locale_1034>
<locale_1036>Volet Maillage</locale_1036>
<locale_1040>Barra laterale Mesh</locale_1040>
<locale_1041>メッシュサイドバー</locale_1041>
<locale_1042>메쉬 사이드바</locale_1042>
<locale_1045>Siatka - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Malha</locale_2070>
<locale_2052>网格边栏</locale_2052>
<locale_1028>網格邊欄</locale_1028>
<locale_1049>Сети - боковая</locale_1049>
</name>
<tool_bar guid="c3eab84e-9994-4c04-b2ef-aa4060f96168" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="c4f7e3af-09ff-4844-b6dc-8f7a65e2f908" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="8f714f50-afd9-4e90-88a8-1432cdcfb431" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="11b1e4f8-1e1d-4caa-9796-cee7fad3ee3b" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
</dock_bars>
<last_collection_panel_was_in>
<item guid="0dfcac10-303b-48a3-b541-88316b2a719c" dock_bar="71a6e2aa-d426-4fcf-aac6-0b5761762f1b" />
<item guid="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" dock_bar="87c7528a-a0a6-4245-837a-753237bfe57d" />
<item guid="1012681e-d276-49d3-9cd9-7de92dc2404a" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="1d3d1785-2332-428b-a838-b2fe39ec50f4" dock_bar="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd" />
<item guid="1d55d702-028c-4aab-99cc-acfdd441fe5f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="34ffb674-c504-49d9-9fcd-99cc811dcda2" dock_bar="e6d78882-4e26-47c1-9e3e-4be7574dec59" />
<item guid="3610bf83-047d-4f7f-93fd-163ea305b493" dock_bar="2e388a71-8d04-4d88-b545-9c714b2cc497" />
<item guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" dock_bar="044781f9-dc80-4398-8498-e14c27c44423" />
<item guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" dock_bar="044781f9-dc80-4398-8498-e14c27c44423" />
<item guid="3d1dfae0-8786-46a3-94dc-130c6a6e78bf" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="4a985c3a-ad29-4f8d-927f-6629dd8d355a" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" dock_bar="96a58830-8ba7-4e11-aecc-86793727b1b5" />
<item guid="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" dock_bar="044781f9-dc80-4398-8498-e14c27c44423" />
<item guid="52606b82-9bc4-493a-b2b4-d2073d995529" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="562bda2a-184f-4b22-9607-79d992f28557" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" dock_bar="96a58830-8ba7-4e11-aecc-86793727b1b5" />
<item guid="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" dock_bar="044781f9-dc80-4398-8498-e14c27c44423" />
<item guid="679af970-96d0-4c3a-831d-b4ff878e2884" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="6b6ffd64-c279-4b45-9959-e7e5a8eef806" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" dock_bar="96a58830-8ba7-4e11-aecc-86793727b1b5" />
<item guid="6df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="39716459-9788-492d-adfd-8d89a7585363" />
<item guid="6df55e69-e102-4a72-b181-11664046c93f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="77d33034-194d-4cd5-957c-730d9a9eac50" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="7df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" dock_bar="035c14a5-6fa6-4e54-9769-5db41f770a3c" />
<item guid="86777b3d-3d68-4965-84f8-9e019c402433" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="8df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="8f23551a-a05b-4a03-a8d5-3e2fc55e4d8a" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="8fa84eff-1da5-4788-a983-e1ec3785e6a8" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="91799cf0-059a-46f8-854c-cc1c1419e29f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="918191ca-1105-43f9-a34a-dda4276883c1" dock_bar="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" />
<item guid="987b1930-ecde-4e62-8282-97ab4ad325fe" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="9a0fa999-295d-4d77-b160-074fa2cd8e6d" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="a650e8dd-3896-43a8-9359-0e7ad8daf38e" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" dock_bar="3443edbb-66f4-460f-97b8-1008a2a31bc5" />
<item guid="b70a4973-99ca-40c0-b2b2-f03417a5ff1d" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" dock_bar="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" />
<item guid="d9ac0269-811b-47d1-aa33-777986b13715" dock_bar="39716459-9788-492d-adfd-8d89a7585363" />
<item guid="f4424a46-8281-430a-b03d-911dc9b40294" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
</last_collection_panel_was_in>
<!--Visible items in each dock site-->
<dock_sites major_ver="1" minor_ver="0">
<dock_site location="Left" auto_hide="False">
<band size="260">
<dock_bar guid="303293c9-aad0-4419-994d-6765718a58ed" size="0.3333333432674408" />
<dock_bar guid="96a58830-8ba7-4e11-aecc-86793727b1b5" size="0.6485507488250732" />
<dock_bar guid="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd" size="0.01811593770980835" />
</band>
</dock_site>
<dock_site location="Right" auto_hide="False">
<band size="230">
<dock_bar guid="5a5b89a0-830f-c90d-5f60-35c2e907e18c" size="0.5" />
<dock_bar guid="044781f9-dc80-4398-8498-e14c27c44423" size="0.5" />
</band>
</dock_site>
<dock_site location="Top" auto_hide="False">
<band size="62">
<dock_bar guid="035c14a5-6fa6-4e54-9769-5db41f770a3c" />
</band>
</dock_site>
<dock_site location="Bottom" auto_hide="False" />
</dock_sites>
</RhinoUI>
+4 -4
View File
@@ -14,7 +14,7 @@ import {
deleteLayerCombination, openLayerCombinationsDialog, deleteLayerCombination, openLayerCombinationsDialog,
openDossierSettings, openKameraPanel, openDossierSettings, openKameraPanel,
setMasseActive, openMasseSettings, setMasseActive, openMasseSettings,
openAbout, createText, setTextSettings, openAbout, openCheatsheet, createText, setTextSettings,
applyTextStyle, saveTextStyle, deleteTextStyle, applyTextStyle, saveTextStyle, deleteTextStyle,
setDarstellung, setDarstellung,
arrangeSelection, arrangeSelection,
@@ -255,10 +255,10 @@ export default function OberleisteApp() {
overflowX: 'auto', overflowY: 'hidden', overflowX: 'auto', overflowY: 'hidden',
flexShrink: 0, flexShrink: 0,
}}> }}>
{/* Logo: DOSSIER. + Version darunter (Klick = About-Fenster) */} {/* Logo: DOSSIER. + Version darunter (Klick = Cheatsheet, Shift+Klick = About) */}
<button <button
onClick={() => openAbout()} onClick={(e) => e.shiftKey ? openAbout() : openCheatsheet()}
title="Über Dossier" title="Shortcuts (Shift+Klick = Über Dossier)"
style={{ style={{
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
alignItems: 'flex-start', gap: 0, alignItems: 'flex-start', gap: 0,
+4 -7
View File
@@ -265,20 +265,17 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
minHeight: 24, minHeight: 24,
}} }}
> >
{/* Chevron sitzt visuell weiter rechts (marginLeft) marginRight {/* Chevron-Slot 12w identisch zu GeschossManager-Spacer, damit
kompensiert das, damit die nachfolgenden Elemente (Auge, Code, die Eye-Spalten beider Panels auf gleicher Position liegen. */}
Farbe, Name) nicht mitrutschen. Spacer fuer kinderlose Zeilen
spiegelt dasselbe Offset, sonst springt die Eye-Spalte zwischen
Parent- und Leaf-Zeilen. */}
{hasChildren ? ( {hasChildren ? (
<button <button
className="btn-icon-xs" className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }} onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }}
title={expanded ? 'Einklappen' : 'Aufklappen'} title={expanded ? 'Einklappen' : 'Aufklappen'}
style={{ width: 12, height: 12, marginLeft: 6, marginRight: -6 }} style={{ width: 12, height: 12, flexShrink: 0 }}
><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={11} /></button> ><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={11} /></button>
) : ( ) : (
<span style={{ width: 12, flexShrink: 0, marginLeft: 6, marginRight: -6 }} /> <span style={{ width: 12, flexShrink: 0 }} />
)} )}
<button <button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`} className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
+356 -8
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano // Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import Icon from './Icon' import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls' import { BarToggle, BarButton, BAR_H } from './BarControls'
import { import {
@@ -117,6 +117,57 @@ function TabBar({ tabs, active, onChange }) {
) )
} }
// Vertikale Sidebar mit Gruppen skaliert auf beliebig viele Kategorien.
// Aktiver Eintrag = abgerundete Pill (mit Margin links/rechts), nicht
// full-width Fläche passt zum DOSSIER-Pill-Stil.
function Sidebar({ groups, active, onChange }) {
return (
<div style={{
width: 180, flexShrink: 0,
borderRight: '1px solid var(--border)',
background: 'var(--bg-dialog)',
overflowY: 'auto',
padding: '8px 0',
}}>
{groups.map((grp) => (
<div key={grp.label}>
<div style={{
padding: '12px 14px 6px',
fontSize: 8, fontWeight: 600,
color: 'var(--text-muted)',
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}>{grp.label}</div>
{grp.items.map((it) => {
const isActive = active === it.key
return (
<div key={it.key}
style={{ padding: '1px 8px' }}>
<button
onClick={() => onChange(it.key)}
style={{
display: 'block', width: '100%',
textAlign: 'left',
padding: '6px 12px',
background: isActive ? 'var(--accent)' : 'transparent',
color: isActive ? '#fff' : 'var(--text-primary)',
border: 'none',
borderRadius: 999,
cursor: 'pointer',
fontSize: 11,
lineHeight: 1.4,
}}>
{it.label}
</button>
</div>
)
})}
</div>
))}
</div>
)
}
/* LinetypePreview SVG-Linie mit Strich-Segmenten. segments = [{length,type}] /* LinetypePreview SVG-Linie mit Strich-Segmenten. segments = [{length,type}]
type Line/Space (manchmal auch Continuous-Ableitungen). Width in px; type Line/Space (manchmal auch Continuous-Ableitungen). Width in px;
wir skalieren die Segmente damit das Gesamtmuster in width passt. */ wir skalieren die Segmente damit das Gesamtmuster in width passt. */
@@ -625,8 +676,11 @@ export default function ProjectSettingsDialog({
const [draft, setDraft] = useState(() => ({ const [draft, setDraft] = useState(() => ({
defaults: { ...(initial.defaults || {}) }, defaults: { ...(initial.defaults || {}) },
materials: [...(initial.materials || [])], materials: [...(initial.materials || [])],
wandStyles: [...(initial.wandStyles || [])],
project: { ...(initial.project || {}) }, project: { ...(initial.project || {}) },
})) }))
const [selWandStyleIdx, setSelWandStyleIdx] = useState(() =>
(initial.wandStyles && initial.wandStyles.length) ? 0 : null)
const setProject = (k, v) => const setProject = (k, v) =>
setDraft(d => ({ ...d, project: { ...(d.project || {}), [k]: v } })) setDraft(d => ({ ...d, project: { ...(d.project || {}), [k]: v } }))
const [selMat, setSelMat] = useState(() => { const [selMat, setSelMat] = useState(() => {
@@ -750,6 +804,42 @@ export default function ProjectSettingsDialog({
setSelMat({ kind: 'local', idx: draft.materials.length }) 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 ? { const wrapperStyle = embedded ? {
width: '100%', height: '100%', width: '100%', height: '100%',
background: 'var(--bg-dialog)', background: 'var(--bg-dialog)',
@@ -766,17 +856,30 @@ export default function ProjectSettingsDialog({
<div style={wrapperStyle}> <div style={wrapperStyle}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, <div style={{ display: 'flex', flexDirection: 'column', flex: 1,
minHeight: 0, overflow: 'hidden' }}> minHeight: 0, overflow: 'hidden' }}>
<TabBar tabs={[ {/* Body: Sidebar links + Inhalt rechts */}
{ key: 'defaults', label: 'Voreinstellungen' }, <div style={{ display: 'flex', flexDirection: 'row', flex: 1,
minHeight: 0, overflow: 'hidden' }}>
<Sidebar
active={tab}
onChange={setTab}
groups={[
{ label: 'Projekt', items: [
{ key: 'defaults', label: 'Projekt & Defaults' },
]},
{ label: 'Stile', items: [
{ key: 'wandstile', label: 'Wandstile' },
{ key: 'raumstile', label: 'Raumstile' },
{ key: 'stempelstile', label: 'Stempelstile' },
{ key: 'symbols', label: 'Symbole' },
]},
{ label: 'Bibliothek', items: [
{ key: 'materials', label: 'Materialien' }, { key: 'materials', label: 'Materialien' },
{ key: 'linetypes', label: 'Linientypen' }, { key: 'linetypes', label: 'Linientypen' },
{ key: 'hatches', label: 'Schraffuren' }, { key: 'hatches', label: 'Schraffuren' },
{ key: 'symbols', label: 'Symbole' }, ]},
{ key: 'raumstile', label: 'Raumstile' }, ]} />
{ key: 'stempelstile', label: 'Stempelstile' },
]} active={tab} onChange={setTab} />
{/* Body */} {/* Tab content */}
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', <div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
padding: '8px 14px' }}> padding: '8px 14px' }}>
{tab === 'defaults' && ( {tab === 'defaults' && (
@@ -873,6 +976,18 @@ export default function ProjectSettingsDialog({
</div> </div>
)} )}
{tab === 'wandstile' && (
<WandStileTab
styles={draft.wandStyles}
selectedIdx={selWandStyleIdx}
onSelect={setSelWandStyleIdx}
materials={[...(initial.builtinMaterials || []), ...(draft.materials || [])]}
onChange={setWandStyle}
onAdd={addWandStyle}
onDelete={delWandStyle}
onDuplicate={dupWandStyle} />
)}
{tab === 'materials' && ( {tab === 'materials' && (
<div style={{ display: 'flex', height: '100%', <div style={{ display: 'flex', height: '100%',
margin: '-8px -14px', /* Tab-Padding aufheben */ margin: '-8px -14px', /* Tab-Padding aufheben */
@@ -1582,6 +1697,7 @@ export default function ProjectSettingsDialog({
</div> </div>
)} )}
</div> </div>
</div>{/* ← schließt Body-Row (Sidebar + Tab-Content) */}
{/* Footer — Pill-Buttons */} {/* Footer — Pill-Buttons */}
<div style={{ <div style={{
@@ -1598,3 +1714,235 @@ export default function ProjectSettingsDialog({
</div> </div>
) )
} }
// ============================================================================
// 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 (
<div style={{ display: 'flex', height: '100%',
margin: '-8px -14px', minHeight: 0 }}>
{/* Links: Liste */}
<div style={{
width: 240, flexShrink: 0,
display: 'flex', flexDirection: 'column',
borderRight: '1px solid var(--border)',
background: 'var(--bg-dialog)',
}}>
<div style={{ flex: 1, overflowY: 'auto', padding: '4px 0' }}>
{sorted.length === 0 && (
<div style={{ padding: 20, textAlign: 'center',
color: 'var(--text-muted)', fontSize: 10 }}>
Noch keine Wandstile.<br/>+ klicken zum Anlegen.
</div>
)}
{sorted.map(({ s, i }) => {
const isActive = selectedIdx === i
return (
<button key={s.id || i}
onClick={() => onSelect(i)}
style={{
display: 'flex', alignItems: 'center', gap: 8,
width: '100%', textAlign: 'left',
padding: '8px 12px',
background: isActive ? 'var(--accent)' : 'transparent',
color: isActive ? '#fff' : 'var(--text-primary)',
border: 'none', cursor: 'pointer',
fontSize: 11,
}}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 500 }}>
{s.name || s.id || '(unnamed)'}
</div>
<div style={{ fontSize: 9, opacity: 0.7, marginTop: 2 }}>
{s.layered ? 'geschichtet' : (s.material || 'kein Material')}
{' · '}{s.layered
? ((s.layers || []).reduce((sum, l) => sum + (l.dicke || 0), 0)).toFixed(2)
: (s.dicke || 0).toFixed(2)} m
</div>
</div>
<div style={{
fontSize: 9, padding: '2px 6px',
background: isActive ? 'rgba(255,255,255,0.2)' : 'var(--bg-section)',
borderRadius: 3, minWidth: 32, textAlign: 'center',
}}>{s.prio || 500}</div>
</button>
)
})}
</div>
<div style={{ display: 'flex', gap: 4,
padding: '6px 8px',
borderTop: '1px solid var(--border-light)' }}>
<BarToggle icon="add" onClick={onAdd} title="Neuer Wandstil" />
{selectedIdx != null && (
<>
<BarToggle icon="content_copy"
onClick={() => onDuplicate(selectedIdx)}
title="Duplizieren" />
<BarToggle icon="delete"
onClick={() => onDelete(selectedIdx)}
title="Löschen" />
</>
)}
</div>
</div>
{/* Rechts: Editor */}
<div style={{ flex: 1, minWidth: 0, overflowY: 'auto', padding: '14px' }}>
{!sel && (
<div style={{ color: 'var(--text-muted)', fontSize: 11,
padding: 20, textAlign: 'center' }}>
Wandstil links auswählen oder + für neuen.
</div>
)}
{sel && (
<div style={{ maxWidth: 540 }}>
<DetailSection title="Identität">
<InlineTextField label="Name"
value={sel.name}
onChange={(v) => onChange(selectedIdx, { name: v })} />
<InlineTextField label="ID (intern)"
value={sel.id}
onChange={(v) => onChange(selectedIdx, { id: v })}
hint="Wird in der Wand als UserString gespeichert. Aenderung verlinkt bestehende Waende nicht automatisch um." />
<InlineNumberField label="Prio (1-999)"
value={sel.prio || 500}
step={50} min={1} max={999}
onChange={(v) => 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." />
</DetailSection>
<DetailSection title="Geometrie">
<div style={{ padding: '5px 0',
display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ flex: 1, fontSize: 11,
color: 'var(--text-primary)' }}>Aufbau</span>
<div style={{ display: 'flex', gap: 3 }}>
<BarToggle label="Massiv" active={!sel.layered}
onClick={() => onChange(selectedIdx, { layered: false })} />
<BarToggle label="Geschichtet" active={sel.layered}
onClick={() => onChange(selectedIdx, { layered: true })} />
</div>
</div>
{!sel.layered && (
<>
<div style={{ padding: '5px 0',
display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ flex: 1, fontSize: 11,
color: 'var(--text-primary)' }}>Material</span>
<select value={sel.material || ''}
onChange={(e) => onChange(selectedIdx, { material: e.target.value })}
style={{ fontSize: 11, padding: '3px 8px',
background: 'var(--bg-section)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
borderRadius: 3 }}>
<option value="">(keines)</option>
{matNames.map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
</div>
<InlineNumberField label="Default-Dicke"
value={sel.dicke || 0.25}
step={0.01} min={0.01} suffix="m"
onChange={(v) => onChange(selectedIdx, { dicke: v || 0.25 })}
hint="Wird beim Erstellen vorgeschlagen. User kann pro Wand überschreiben." />
<div style={{ padding: '5px 0',
display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ flex: 1, fontSize: 11,
color: 'var(--text-primary)' }}>Referenz</span>
<div style={{ display: 'flex', gap: 3 }}>
{[
{ code: 'mid', label: 'Mittig' },
{ code: 'left', label: 'Links' },
{ code: 'right', label: 'Rechts' },
].map(r => (
<BarToggle key={r.code} label={r.label}
active={(sel.referenz || 'mid') === r.code}
onClick={() => onChange(selectedIdx, { referenz: r.code })} />
))}
</div>
</div>
</>
)}
{sel.layered && (
<div style={{ padding: '5px 0' }}>
<div style={{ fontSize: 10, color: 'var(--text-muted)',
paddingBottom: 4 }}>
Schichten (von links nach rechts):
</div>
{(sel.layers || []).map((ly, li) => (
<div key={li} style={{
display: 'flex', gap: 6, alignItems: 'center',
padding: '4px 0',
}}>
<select value={ly.material || ''}
onChange={(e) => {
const newLayers = [...(sel.layers || [])]
newLayers[li] = { ...newLayers[li], material: e.target.value }
onChange(selectedIdx, { layers: newLayers })
}}
style={{ flex: 1, fontSize: 11, padding: '3px 6px',
background: 'var(--bg-section)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
borderRadius: 3 }}>
<option value="">(material)</option>
{matNames.map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
<input type="number" step="0.01" min="0.005"
value={ly.dicke || 0}
onChange={(e) => {
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 }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
<BarToggle icon="delete" onClick={() => {
const newLayers = (sel.layers || []).filter((_, idx) => idx !== li)
onChange(selectedIdx, { layers: newLayers })
}} title="Schicht löschen" />
</div>
))}
<BarToggle icon="add" label="Schicht hinzufügen"
onClick={() => {
const newLayers = [...(sel.layers || []),
{ material: '', dicke: 0.05 }]
onChange(selectedIdx, { layers: newLayers })
}} />
<div style={{ fontSize: 10, color: 'var(--text-muted)',
padding: '6px 0 0' }}>
Total: {((sel.layers || []).reduce((s, l) => s + (l.dicke || 0), 0)).toFixed(3)} m
</div>
</div>
)}
</DetailSection>
</div>
)}
</div>
</div>
)
}
+1
View File
@@ -280,6 +280,7 @@ export function setSectionStyle(enabled, source, color, pattern, scale, rotation
}) })
} }
export function openAbout() { send('OPEN_ABOUT', {}) } export function openAbout() { send('OPEN_ABOUT', {}) }
export function openCheatsheet() { send('OPEN_CHEATSHEET', {}) }
export function createText() { send('CREATE_TEXT', {}) } export function createText() { send('CREATE_TEXT', {}) }
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) } export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
export function applyTextStyle(id) { send('APPLY_TEXT_STYLE', { id }) } export function applyTextStyle(id) { send('APPLY_TEXT_STYLE', { id }) }