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:
@@ -0,0 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
dist/
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
.idea/
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+144
@@ -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."
|
||||
Executable
+65
@@ -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'"
|
||||
@@ -303,9 +303,17 @@ fn splash_owner_marker_path() -> PathBuf {
|
||||
|
||||
fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> {
|
||||
let settings = load_settings();
|
||||
// XML-Edit nur sinnvoll wenn Rhino nicht laeuft (sonst ueberschreibt's
|
||||
// beim Beenden) UND der Eintrag fuer den naechsten Start eh schon greift.
|
||||
// Setup-Schritte nur wenn Rhino NICHT laeuft — sonst ueberschreibt Rhino
|
||||
// unsere XML-Edits beim Beenden, und yak install kann die in-use .rhp
|
||||
// nicht ersetzen.
|
||||
if settings.auto_load_plugin && !is_rhino_running() {
|
||||
// Schritt 1: .rhp via yak installieren/aktualisieren. Soft-Fail wenn
|
||||
// .yak nicht gebundelt ist (Dev-Setup ohne build.sh ausgefuehrt) —
|
||||
// Bootstrap via XML reicht alleine, Commands fehlen nur.
|
||||
if let Err(e) = ensure_rhino_plugin_installed() {
|
||||
eprintln!("[DOSSIER] Plugin-Install skip: {e}");
|
||||
}
|
||||
// Schritt 2: StartupCommands-XML fuer Python-Bootstrap setzen.
|
||||
let startup_path = settings.plugin_startup_path
|
||||
.clone()
|
||||
.unwrap_or_else(default_plugin_startup_path);
|
||||
@@ -466,11 +474,264 @@ fn ensure_rhino_startup_command(startup_path: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// ===== Rhino-Plugin Install via Yak =====
|
||||
// Mac Rhino lehnt Auto-Load fuer dritte Plugins ab (egal welche Methode).
|
||||
// Wir installieren `.yak` aber trotzdem in den User-Plugin-Pfad — beim ersten
|
||||
// dWall/dDoor/... laedt Rhino das Plugin on-demand und cached es danach.
|
||||
// Der Python-Bootstrap (Panels/Aliases) laeuft parallel via StartupCommands-
|
||||
// XML (ensure_rhino_startup_command). Beide Pfade zusammen = full setup.
|
||||
|
||||
fn yak_binary_path() -> PathBuf {
|
||||
PathBuf::from("/Applications/Rhino 8.app/Contents/Resources/bin/yak")
|
||||
}
|
||||
|
||||
fn rhino_packages_dir() -> PathBuf {
|
||||
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
|
||||
home.join("Library/Application Support/McNeel/Rhinoceros/packages/8.0")
|
||||
}
|
||||
|
||||
fn installed_dossier_version() -> Option<String> {
|
||||
let manifest = rhino_packages_dir().join("DOSSIER/manifest.txt");
|
||||
fs::read_to_string(&manifest).ok().map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
// .yak + version.txt im App-Bundle (Production) oder Repo-dist/ (Dev).
|
||||
fn bundled_plugin_paths() -> (PathBuf, PathBuf) {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
|
||||
let yak = contents_dir.join("Resources/plugin/dossier.yak");
|
||||
let ver = contents_dir.join("Resources/plugin/dossier-version.txt");
|
||||
if yak.is_file() && ver.is_file() {
|
||||
return (yak, ver);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dev-Fallback: Repo-Pfad
|
||||
let repo = PathBuf::from("/Users/karim/STUDIO/DOSSIER/csharp/DOSSIER/dist");
|
||||
(repo.join("dossier.yak"), repo.join("dossier-version.txt"))
|
||||
}
|
||||
|
||||
fn bundled_plugin_version() -> Option<String> {
|
||||
let (_, ver_path) = bundled_plugin_paths();
|
||||
fs::read_to_string(&ver_path).ok().map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
// Installiert/aktualisiert das DOSSIER-Plugin via yak. Idempotent — skip wenn
|
||||
// installierte Version == gebundelte Version. Soft-Fail wenn .yak fehlt oder
|
||||
// yak-CLI nicht vorhanden (Logging, kein Error — Bootstrap via XML laeuft eh).
|
||||
fn ensure_rhino_plugin_installed() -> Result<(), String> {
|
||||
let yak = yak_binary_path();
|
||||
if !yak.is_file() {
|
||||
return Err(format!("yak CLI nicht gefunden: {}", yak.display()));
|
||||
}
|
||||
let (yak_pkg, _) = bundled_plugin_paths();
|
||||
if !yak_pkg.is_file() {
|
||||
return Err(format!(
|
||||
"DOSSIER .yak-Paket nicht gefunden: {} (build.sh in csharp/DOSSIER ausfuehren)",
|
||||
yak_pkg.display()
|
||||
));
|
||||
}
|
||||
let bundled_ver = bundled_plugin_version();
|
||||
let installed_ver = installed_dossier_version();
|
||||
if let (Some(b), Some(i)) = (&bundled_ver, &installed_ver) {
|
||||
if b == i {
|
||||
return Ok(()); // schon aktuell — kein Re-Install
|
||||
}
|
||||
}
|
||||
if is_rhino_running() {
|
||||
return Err(
|
||||
"Rhino laeuft — Plugin-Update kann erst nach Rhino-Quit installiert werden."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let pkg_dir = yak_pkg.parent().ok_or_else(|| "Plugin-Pfad ohne Parent".to_string())?;
|
||||
let output = Command::new(&yak)
|
||||
.arg("install")
|
||||
.arg("dossier")
|
||||
.arg("--source")
|
||||
.arg(pkg_dir)
|
||||
.output()
|
||||
.map_err(|e| format!("yak install: {e}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"yak install fehlgeschlagen: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
eprintln!(
|
||||
"[DOSSIER] Plugin via yak installiert: {} → {}",
|
||||
installed_ver.unwrap_or_else(|| "(nicht installiert)".into()),
|
||||
bundled_ver.unwrap_or_else(|| "(unbekannt)".into())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
fn open_rhino(app: tauri::AppHandle, path3dm: String) -> Result<(), String> {
|
||||
open_rhino_internal(&app, &path3dm)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn install_rhino_plugin() -> Result<String, String> {
|
||||
ensure_rhino_plugin_installed()?;
|
||||
Ok(installed_dossier_version().unwrap_or_else(|| "(version unbekannt)".into()))
|
||||
}
|
||||
|
||||
|
||||
// ===== Window-Layout Installation =====
|
||||
// Rhino-Workspaces sind XML-Dateien im User-Pfad benannt nach Layout-GUID.
|
||||
// Wir bundlen DOSSIERs Master-Layout(s) im Repo unter rhino/workspaces/ und
|
||||
// kopieren sie beim Init in Rhinos Workspaces-Folder, damit startup.py das
|
||||
// Layout per `_-WindowLayout "<name>" _Enter` direkt anwenden kann.
|
||||
|
||||
fn rhino_workspaces_dir() -> PathBuf {
|
||||
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
|
||||
home.join("Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces")
|
||||
}
|
||||
|
||||
fn bundled_workspaces_dir() -> PathBuf {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
|
||||
let bundled = contents_dir.join("Resources/rhino/workspaces");
|
||||
if bundled.is_dir() {
|
||||
return bundled;
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from("/Users/karim/STUDIO/DOSSIER/rhino/workspaces")
|
||||
}
|
||||
|
||||
// Kopiert alle Workspace-XMLs aus dem bundle in Rhinos Workspaces-Folder.
|
||||
// Vorhandene Files werden ueberschrieben (User-Customizations gehen verloren —
|
||||
// dafuer ist's reproduzierbar). Returns Anzahl kopierter Files.
|
||||
fn ensure_window_layout_installed() -> Result<usize, String> {
|
||||
let src = bundled_workspaces_dir();
|
||||
if !src.is_dir() {
|
||||
return Err(format!("Workspace-Quelle fehlt: {}", src.display()));
|
||||
}
|
||||
let dst = rhino_workspaces_dir();
|
||||
fs::create_dir_all(&dst)
|
||||
.map_err(|e| format!("Workspace-Zielordner erstellen: {e}"))?;
|
||||
if is_rhino_running() {
|
||||
return Err("Rhino laeuft — bitte erst beenden (Layout-Datei wird sonst ueberschrieben).".into());
|
||||
}
|
||||
let mut count = 0;
|
||||
for entry in fs::read_dir(&src).map_err(|e| format!("Workspace-Quelle lesen: {e}"))? {
|
||||
let entry = entry.map_err(|e| format!("DirEntry: {e}"))?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("xml")).unwrap_or(false) {
|
||||
let file_name = path.file_name().ok_or("FileName")?;
|
||||
let dst_path = dst.join(file_name);
|
||||
fs::copy(&path, &dst_path)
|
||||
.map_err(|e| format!("Workspace kopieren ({}): {e}", file_name.to_string_lossy()))?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
|
||||
// ===== DOSSIER INIT =====
|
||||
// Komplettes Setup auf einem neuen PC: Plugin via Yak + StartupCommands-XML +
|
||||
// Window-Layout-Files. Returns Status pro Schritt fuer das Frontend-Dialog.
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
struct InitStep {
|
||||
id: String,
|
||||
label: String,
|
||||
status: String, // "ok" | "error" | "skipped"
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
struct InitResult {
|
||||
steps: Vec<InitStep>,
|
||||
overall_ok: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
struct InitStatus {
|
||||
plugin_installed: bool,
|
||||
startup_cmd_set: bool,
|
||||
layout_installed: bool,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn check_dossier_initialized() -> InitStatus {
|
||||
let plugin_installed = installed_dossier_version().is_some();
|
||||
let startup_cmd_set = fs::read_to_string(rhino_settings_xml_path())
|
||||
.map(|s| s.contains("StartupCommands") && s.contains("startup.py"))
|
||||
.unwrap_or(false);
|
||||
let layout_installed = rhino_workspaces_dir()
|
||||
.join("b6b68c03-3031-4899-bca2-fe6e425146fc.xml")
|
||||
.is_file();
|
||||
InitStatus {
|
||||
plugin_installed,
|
||||
startup_cmd_set,
|
||||
layout_installed,
|
||||
// initialized = ALLE drei vorhanden. So zeigt der Dialog auch nach
|
||||
// teilweisem Clean (z.B. nur layout geloescht) noch an.
|
||||
initialized: plugin_installed && startup_cmd_set && layout_installed,
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn dossier_init() -> Result<InitResult, String> {
|
||||
if is_rhino_running() {
|
||||
return Err("Rhino laeuft — bitte erst beenden, dann Init nochmal starten.".into());
|
||||
}
|
||||
|
||||
let mut steps = Vec::new();
|
||||
let mut overall_ok = true;
|
||||
|
||||
// Schritt 1: Plugin via Yak installieren/aktualisieren
|
||||
let (status, detail) = match ensure_rhino_plugin_installed() {
|
||||
Ok(()) => {
|
||||
let v = installed_dossier_version().unwrap_or_else(|| "(?)".into());
|
||||
("ok".into(), format!("Version {v}"))
|
||||
}
|
||||
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||
};
|
||||
steps.push(InitStep {
|
||||
id: "plugin".into(),
|
||||
label: "DOSSIER-Plugin via Yak installieren".into(),
|
||||
status, detail,
|
||||
});
|
||||
|
||||
// Schritt 2: StartupCommands-XML eintragen (Python-Bootstrap)
|
||||
let startup_path = default_plugin_startup_path();
|
||||
let (status, detail) = if !Path::new(&startup_path).is_file() {
|
||||
overall_ok = false;
|
||||
("error".into(), format!("startup.py nicht gefunden: {startup_path}"))
|
||||
} else {
|
||||
match ensure_rhino_startup_command(&startup_path) {
|
||||
Ok(()) => ("ok".into(), startup_path.clone()),
|
||||
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||
}
|
||||
};
|
||||
steps.push(InitStep {
|
||||
id: "startup".into(),
|
||||
label: "Python-Bootstrap (StartupCommands-XML)".into(),
|
||||
status, detail,
|
||||
});
|
||||
|
||||
// Schritt 3: Window-Layout-Files in Rhinos Workspaces-Folder kopieren
|
||||
let (status, detail) = match ensure_window_layout_installed() {
|
||||
Ok(n) => ("ok".into(), format!("{n} Layout-Datei(en) installiert")),
|
||||
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||
};
|
||||
steps.push(InitStep {
|
||||
id: "layout".into(),
|
||||
label: "Window-Layout in Rhino kopieren".into(),
|
||||
status, detail,
|
||||
});
|
||||
|
||||
Ok(InitResult { steps, overall_ok })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn trigger_plugin_load_now() -> Result<(), String> {
|
||||
// Schreibt den `_RunPythonScript <pfad>` Eintrag in Rhinos Startup-Command-
|
||||
@@ -924,6 +1185,9 @@ pub fn run() {
|
||||
read_project_config,
|
||||
open_rhino,
|
||||
trigger_plugin_load_now,
|
||||
install_rhino_plugin,
|
||||
dossier_init,
|
||||
check_dossier_initialized,
|
||||
get_default_plugin_startup_path,
|
||||
show_in_finder,
|
||||
is_rhino_running,
|
||||
|
||||
@@ -56,7 +56,9 @@
|
||||
},
|
||||
"resources": {
|
||||
"../../dist": "dist",
|
||||
"../../rhino": "rhino"
|
||||
"../../rhino": "rhino",
|
||||
"../../csharp/DOSSIER/dist/dossier.yak": "plugin/dossier.yak",
|
||||
"../../csharp/DOSSIER/dist/dossier-version.txt": "plugin/dossier-version.txt"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -126,6 +126,13 @@ export default function App() {
|
||||
invoke('list_window_layouts').then(setLayouts).catch(() => {})
|
||||
invoke('read_dossier_settings').then(ds => setActiveLayout(ds?.windowLayout || '')).catch(() => {})
|
||||
invoke('read_settings').then(s => setTags(s?.tags || [])).catch(() => {})
|
||||
// Auto-Open Setup-Dialog wenn DOSSIER nicht initialisiert ist (z.B. nach
|
||||
// clean-rhino.sh oder auf einem neuen Mac).
|
||||
invoke('check_dossier_initialized')
|
||||
.then(st => {
|
||||
if (!st?.initialized) { setSettingsTab('setup'); setSettingsOpen(true) }
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// File-Meta laden sobald recent sich aendert
|
||||
@@ -750,6 +757,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
|
||||
<header style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<span>Einstellungen</span>
|
||||
<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 === 'view'} onClick={() => setTab('view')}>View</TabBtn>
|
||||
<TabBtn active={tab === 'ebenen'} onClick={() => setTab('ebenen')}>Ebenen</TabBtn>
|
||||
@@ -759,6 +767,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
|
||||
</div>
|
||||
</header>
|
||||
<div className="body">
|
||||
{tab === 'setup' && <SetupSettings />}
|
||||
{tab === 'rhino' && <RhinoSettings />}
|
||||
{tab === 'view' && <ViewSettings />}
|
||||
{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 }) {
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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())))
|
||||
@@ -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()))
|
||||
@@ -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("&", "&").replace("<", "<").replace(">", ">")
|
||||
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("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
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))
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -1507,6 +1507,14 @@ class OberleisteBridge(panel_base.BaseBridge):
|
||||
except Exception as 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) ---------------------------
|
||||
elif t == "CREATE_TEXT":
|
||||
try:
|
||||
|
||||
+7
-6
@@ -684,8 +684,8 @@ def make_panel_icon(name_or_letter, bg_hex):
|
||||
if icon_bmp is not None: chosen_path = png_path
|
||||
else: print("[panel_base] PNG geladen aber Bitmap None:",
|
||||
png_path)
|
||||
else:
|
||||
print("[panel_base] PNG nicht gefunden:", png_path)
|
||||
# PNG-not-found ist normal: Fallback auf SVG dann Material-Font.
|
||||
# Nur loggen wenn final ALLES failt (s.u.).
|
||||
if icon_bmp is None:
|
||||
svg_path = os.path.join(_PANEL_ICONS_SVG_DIR,
|
||||
name_or_letter + ".svg")
|
||||
@@ -713,10 +713,11 @@ def make_panel_icon(name_or_letter, bg_hex):
|
||||
if font_family_name:
|
||||
try:
|
||||
ff = drawing.FontFamily(font_family_name)
|
||||
# FontStyle.None: in Python3 nicht direkt zugreifbar
|
||||
# (None ist Keyword) → getattr-Workaround, sonst 0
|
||||
try: fs = getattr(drawing.FontStyle, "None")
|
||||
except Exception: fs = 0
|
||||
# FontStyle.None: in Python3 ist None ein Keyword, deshalb
|
||||
# via System.Enum.ToObject explizit konstruieren — Python.NET 3
|
||||
# konvertiert int → Enum nicht mehr implizit.
|
||||
import System
|
||||
fs = System.Enum.ToObject(drawing.FontStyle, 0)
|
||||
font = drawing.Font(ff, 20, fs)
|
||||
glyph = chr(mat_cp)
|
||||
_draw_glyph(g, size, font, glyph,
|
||||
|
||||
@@ -309,6 +309,47 @@ _PROJECT_SETTINGS_DEFAULTS = {
|
||||
"unit": "meters", # "meters" | "millimeters" | "centimeters"
|
||||
},
|
||||
"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": {
|
||||
"name": "",
|
||||
"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):
|
||||
"""Garantiert Material-Schema. Material ist REIN 3D — Section-Hatch
|
||||
(2D-Schnitt) wird via Ebenen-Settings am Layer konfiguriert.
|
||||
@@ -442,6 +525,7 @@ def load_project_settings(doc):
|
||||
out = {
|
||||
"defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]),
|
||||
"materials": list(_PROJECT_SETTINGS_DEFAULTS["materials"]),
|
||||
"wand_styles": [dict(s) for s in _PROJECT_SETTINGS_DEFAULTS["wand_styles"]],
|
||||
"project": dict(_PROJECT_SETTINGS_DEFAULTS["project"]),
|
||||
}
|
||||
if raw:
|
||||
@@ -458,6 +542,12 @@ def load_project_settings(doc):
|
||||
_normalize_material(x) for x in m
|
||||
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")
|
||||
if isinstance(pr, dict):
|
||||
out["project"] = _normalize_project_meta(pr)
|
||||
@@ -976,6 +1066,7 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
"defaults": current.get("defaults", {}),
|
||||
"project": current.get("project", {}),
|
||||
"materials": current.get("materials", []),
|
||||
"wandStyles": current.get("wand_styles", []),
|
||||
"builtinMaterials": built_in,
|
||||
"hatchPatterns": _hatch_pattern_names(doc),
|
||||
"hatchPatternsFull": _list_hatch_patterns_full(doc),
|
||||
@@ -992,6 +1083,7 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
new_settings = {
|
||||
"defaults": updated.get("defaults", {}),
|
||||
"materials": updated.get("materials", []),
|
||||
"wand_styles": updated.get("wandStyles", []),
|
||||
"project": updated.get("project", {}),
|
||||
}
|
||||
save_project_settings(doc2, new_settings)
|
||||
|
||||
@@ -309,6 +309,33 @@ def _load_all(sender, e):
|
||||
_pb._t_mark("post_init", "unit_check", _t_uc)
|
||||
except Exception as 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
|
||||
_hint_dossier_ui()
|
||||
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
|
||||
@@ -341,5 +368,12 @@ def _load_all(sender, e):
|
||||
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
|
||||
print("[STARTUP] geplant - laedt sobald Rhino idle ist")
|
||||
|
||||
+24
-10
@@ -4,19 +4,22 @@
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
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
|
||||
Partnership-Cascade (elemente._on_select_objects) + Pure-Transform-Pfad
|
||||
verschieben die Treppe bereits sauber.
|
||||
|
||||
Endpunkt-Logik pro Treppen-Art:
|
||||
Marker-Logik pro Treppen-Art:
|
||||
- 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-
|
||||
zentrum, nicht der Treppen-Anfang
|
||||
"""
|
||||
import Rhino
|
||||
import Rhino.Display as rd
|
||||
import Rhino.DocObjects as rdoc
|
||||
import Rhino.Geometry as rg
|
||||
import scriptcontext as sc
|
||||
import System.Drawing as SD
|
||||
@@ -28,8 +31,7 @@ _MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
|
||||
|
||||
|
||||
def _treppe_endpoints(axis_obj):
|
||||
"""Liefert Liste von Point3d fuer Treppen-Start + -Ende. Beachtet
|
||||
treppe_art (Wendel hat anderes Polyline-Schema)."""
|
||||
"""Liefert Liste von Point3d. Beachtet treppe_art + Polyline-Punktzahl."""
|
||||
if axis_obj is None or axis_obj.IsDeleted: return []
|
||||
a = axis_obj.Attributes
|
||||
if a.GetUserString("dossier_element_type") != "treppe_axis": return []
|
||||
@@ -41,14 +43,26 @@ def _treppe_endpoints(axis_obj):
|
||||
ok, poly = geom.TryGetPolyline()
|
||||
if not ok or poly is None or poly.Count != 3: return []
|
||||
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]
|
||||
except Exception:
|
||||
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):
|
||||
"""Zeichnet gruene Endpunkt-Marker an allen selektierten Treppen-Achsen."""
|
||||
"""Zeichnet gruene Marker an allen selektierten Treppen-Achsen."""
|
||||
|
||||
def DrawForeground(self, e):
|
||||
try:
|
||||
@@ -60,10 +74,10 @@ class _TreppeEndpointConduit(rd.DisplayConduit):
|
||||
a = obj.Attributes
|
||||
eid = a.GetUserString("dossier_element_id") or ""
|
||||
if not eid or eid in seen: continue
|
||||
# Source-Axis via element_id finden (kann anderer Obj sein
|
||||
# wenn User nur Volume oder 2D-Symbol selektiert hat)
|
||||
# Source-Axis via element_id finden — auch wenn auf hidden
|
||||
# Layer (User hat z.B. nur 2D-Plansymbol selektiert).
|
||||
axis = None
|
||||
for o in doc.Objects:
|
||||
for o in doc.Objects.GetObjectList(_enumerator_all()):
|
||||
if o is None or o.IsDeleted: continue
|
||||
try:
|
||||
a2 = o.Attributes
|
||||
|
||||
@@ -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">×</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 & 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 & 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 & 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 · 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">‹</a>
|
||||
<a class="win-btn" href="dossier:close" title="Schliessen">×</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 & Rendering" selected_item="d9ac0269-811b-47d1-aa33-777986b13715" can_be_empty="True" display_style="Bitmap">
|
||||
<name>
|
||||
<locale_1033>Display & 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>表示 & レンダリング</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>显示 & 渲染</locale_2052>
|
||||
<locale_1028>顯示 & 彩現</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>
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
deleteLayerCombination, openLayerCombinationsDialog,
|
||||
openDossierSettings, openKameraPanel,
|
||||
setMasseActive, openMasseSettings,
|
||||
openAbout, createText, setTextSettings,
|
||||
openAbout, openCheatsheet, createText, setTextSettings,
|
||||
applyTextStyle, saveTextStyle, deleteTextStyle,
|
||||
setDarstellung,
|
||||
arrangeSelection,
|
||||
@@ -255,10 +255,10 @@ export default function OberleisteApp() {
|
||||
overflowX: 'auto', overflowY: 'hidden',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{/* Logo: DOSSIER. + Version darunter (Klick = About-Fenster) */}
|
||||
{/* Logo: DOSSIER. + Version darunter (Klick = Cheatsheet, Shift+Klick = About) */}
|
||||
<button
|
||||
onClick={() => openAbout()}
|
||||
title="Über Dossier"
|
||||
onClick={(e) => e.shiftKey ? openAbout() : openCheatsheet()}
|
||||
title="Shortcuts (Shift+Klick = Über Dossier)"
|
||||
style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'flex-start', gap: 0,
|
||||
|
||||
@@ -265,20 +265,17 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
|
||||
minHeight: 24,
|
||||
}}
|
||||
>
|
||||
{/* Chevron sitzt visuell weiter rechts (marginLeft) — marginRight
|
||||
kompensiert das, damit die nachfolgenden Elemente (Auge, Code,
|
||||
Farbe, Name) nicht mitrutschen. Spacer fuer kinderlose Zeilen
|
||||
spiegelt dasselbe Offset, sonst springt die Eye-Spalte zwischen
|
||||
Parent- und Leaf-Zeilen. */}
|
||||
{/* Chevron-Slot 12w — identisch zu GeschossManager-Spacer, damit
|
||||
die Eye-Spalten beider Panels auf gleicher Position liegen. */}
|
||||
{hasChildren ? (
|
||||
<button
|
||||
className="btn-icon-xs"
|
||||
onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }}
|
||||
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>
|
||||
) : (
|
||||
<span style={{ width: 12, flexShrink: 0, marginLeft: 6, marginRight: -6 }} />
|
||||
<span style={{ width: 12, flexShrink: 0 }} />
|
||||
)}
|
||||
<button
|
||||
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
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}]
|
||||
type ∈ Line/Space (manchmal auch Continuous-Ableitungen). Width in px;
|
||||
wir skalieren die Segmente damit das Gesamtmuster in width passt. */
|
||||
@@ -625,8 +676,11 @@ export default function ProjectSettingsDialog({
|
||||
const [draft, setDraft] = useState(() => ({
|
||||
defaults: { ...(initial.defaults || {}) },
|
||||
materials: [...(initial.materials || [])],
|
||||
wandStyles: [...(initial.wandStyles || [])],
|
||||
project: { ...(initial.project || {}) },
|
||||
}))
|
||||
const [selWandStyleIdx, setSelWandStyleIdx] = useState(() =>
|
||||
(initial.wandStyles && initial.wandStyles.length) ? 0 : null)
|
||||
const setProject = (k, v) =>
|
||||
setDraft(d => ({ ...d, project: { ...(d.project || {}), [k]: v } }))
|
||||
const [selMat, setSelMat] = useState(() => {
|
||||
@@ -750,6 +804,42 @@ export default function ProjectSettingsDialog({
|
||||
setSelMat({ kind: 'local', idx: draft.materials.length })
|
||||
}
|
||||
|
||||
// Wand-Stile CRUD — gleiche Pattern wie Materialien
|
||||
const setWandStyle = (i, patch) => setDraft(d => ({
|
||||
...d, wandStyles: d.wandStyles.map((s, idx) =>
|
||||
idx === i ? { ...s, ...patch } : s),
|
||||
}))
|
||||
const delWandStyle = (i) => {
|
||||
setDraft(d => ({
|
||||
...d, wandStyles: d.wandStyles.filter((_, idx) => idx !== i),
|
||||
}))
|
||||
setSelWandStyleIdx(null)
|
||||
}
|
||||
const addWandStyle = () => {
|
||||
const newStyle = {
|
||||
id: 'style_' + Math.random().toString(36).slice(2, 10),
|
||||
name: 'Neuer Stil', prio: 500,
|
||||
dicke: 0.25, referenz: 'mid',
|
||||
layered: false, material: '', layers: [],
|
||||
}
|
||||
setDraft(d => ({
|
||||
...d, wandStyles: [...d.wandStyles, newStyle],
|
||||
}))
|
||||
setSelWandStyleIdx(draft.wandStyles.length)
|
||||
}
|
||||
const dupWandStyle = (i) => {
|
||||
setDraft(d => {
|
||||
const src = d.wandStyles[i]
|
||||
if (!src) return d
|
||||
const copy = { ...src,
|
||||
id: 'style_' + Math.random().toString(36).slice(2, 10),
|
||||
name: (src.name || 'Stil') + ' (Kopie)',
|
||||
}
|
||||
return { ...d, wandStyles: [...d.wandStyles, copy] }
|
||||
})
|
||||
setSelWandStyleIdx(draft.wandStyles.length)
|
||||
}
|
||||
|
||||
const wrapperStyle = embedded ? {
|
||||
width: '100%', height: '100%',
|
||||
background: 'var(--bg-dialog)',
|
||||
@@ -766,17 +856,30 @@ export default function ProjectSettingsDialog({
|
||||
<div style={wrapperStyle}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1,
|
||||
minHeight: 0, overflow: 'hidden' }}>
|
||||
<TabBar tabs={[
|
||||
{ key: 'defaults', label: 'Voreinstellungen' },
|
||||
{/* Body: Sidebar links + Inhalt rechts */}
|
||||
<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: 'linetypes', label: 'Linientypen' },
|
||||
{ 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',
|
||||
padding: '8px 14px' }}>
|
||||
{tab === 'defaults' && (
|
||||
@@ -873,6 +976,18 @@ export default function ProjectSettingsDialog({
|
||||
</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' && (
|
||||
<div style={{ display: 'flex', height: '100%',
|
||||
margin: '-8px -14px', /* Tab-Padding aufheben */
|
||||
@@ -1582,6 +1697,7 @@ export default function ProjectSettingsDialog({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>{/* ← schließt Body-Row (Sidebar + Tab-Content) */}
|
||||
|
||||
{/* Footer — Pill-Buttons */}
|
||||
<div style={{
|
||||
@@ -1598,3 +1714,235 @@ export default function ProjectSettingsDialog({
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -280,6 +280,7 @@ export function setSectionStyle(enabled, source, color, pattern, scale, rotation
|
||||
})
|
||||
}
|
||||
export function openAbout() { send('OPEN_ABOUT', {}) }
|
||||
export function openCheatsheet() { send('OPEN_CHEATSHEET', {}) }
|
||||
export function createText() { send('CREATE_TEXT', {}) }
|
||||
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
|
||||
export function applyTextStyle(id) { send('APPLY_TEXT_STYLE', { id }) }
|
||||
|
||||
Reference in New Issue
Block a user