AGPL-3.0 Dual-Lizenz + Pill-Stil-UI + Section-Style-Overhaul + Plan-Mode-Template

Lizenz:
- AGPL-3.0 LICENSE-File im Repo-Root (GNU Volltext)
- SPDX-Header + Copyright in allen Source-Files (Python/JSX/JS/Rust)
- license-Feld in package.json + Cargo.toml
- About-App komplett neu: Dual-Lizenz-Block (AGPL + Commercial),
  openbureau-Branding, Version-Pills, made-in-Switzerland-Footer

UI-Restyle (3 Wellen) — alle Dialoge + Satellites + Panel-Sidebars
auf gemeinsamen Pill-Stil aus BarControls (BarToggle/BarButton/BarCombo):
- Welle 1: GeschossDialog/Settings, AusschnittSettings, LayoutDialog
- Welle 2: ConfirmDeleteEbene, Kamera, MasseSettings, Osm, Swisstopo,
  TextEditor, AusschnittLayerDialog, LayerCombinations
- Welle 3: LayoutsApp, MassstabApp, WerkzeugeApp, OverridesApp,
  ZeichnungsebenenApp; Werkzeuge mit ElementeApp-PillGroup-Layout

GeschossDialog Header-Refactor: +Geschoss/+Zeichnung in Toolbar oben,
move-Pfeile-Spalte breiter (kein Overlap mit G-Haken)

Ausschnitte Rows als Pills, kein Outer-Border ums Suchfeld

Section-Style komplett neu (gestaltung.py + GestaltungApp.jsx):
- ObjectSectionAttributesSource.FromObject (richtiger Enum-Name fuer Mac)
- HatchPatternPrintColor + BoundaryPrintColor mit-setzen (Display = Print)
- BoundaryColor nur bei explizitem User-Override, sonst Rhino-Default
- background_color_hex Parameter (BackgroundFillMode=SolidColor)
- Readback aus GetCustomSectionStyle statt direkt aus Attributes
- UI: Schnittkante > Section Style > Solid-Fill mit proper SectionHead
- 'Boundary' (3D Pen) -> 'Background' weil sich's wie Section-Hintergrund verhaelt

Plan-Mode 'Dossier Plan' via Template:
- rhino/templates/dossier_plan.ini wird direkt geladen
- Fallback auf Technical-Clone + ini-Patch wenn Template fehlt
- Auto-Cleanup von Orphan-Modes vor Import (Name- oder Guid-Match)
- ClipSectionUsage=1 + TechnicalMask=15 als bekannte Soll-Werte
- Bei Template-Pfad keine ini-Patches (1:1 wie User exportiert)
- Sanity-Print listet alle registrierten Modes nach Anlegen

Bridge-Unification: 4 Settings-Apps (Ebenen/Project/Geschoss*Dialog)
benutzen jetzt chunkende send() statt eigene bridgeSend ohne Chunk-
Logik -> grosse Payloads (Hatch-Refs etc.) kommen nicht mehr truncated
bei Python an (loeste 'JSON-Fehler char 990'-Regression in Ebenen-
Settings)

Library-Imports robust: 'import library' jetzt Top-Level in elemente.py
+ rhinopanel.py (statt Lazy in Methoden) -> 'No module named library'-
Crashes weg auch wenn sys.path zwischendurch resettet wird

Tools fuer Display-Mode-Maintenance:
- _clean_display_modes.py (loescht alle Custom-Modes, Built-ins bleiben)
- _inspect_plan_mode.py / _inspect_obj_section.py / _inspect_obj_boundary.py
  (Diagnose-Skripte fuer SectionStyle-Property-Reverse-Engineering)
- _reset_rhino_settings.sh (Backup + Nuke der Rhino-Settings als
  letzte Bastion gegen korrupte Display-Modes)
This commit is contained in:
2026-05-26 17:09:18 +02:00
parent e1b63aa4e6
commit 13a5e1eb7a
100 changed files with 3147 additions and 839 deletions
+367
View File
@@ -1,5 +1,7 @@
#! python 3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
oberleiste.py
OBERLEISTE-Panel: horizontale Top-Bar mit Architektur-Kontext-Controls.
@@ -183,6 +185,363 @@ def _import_display_modes(paths):
return count
# Fest-Guid fuer 'Dossier Plan' damit Re-Imports denselben Slot
# wiederverwenden statt Duplikate zu erzeugen.
_DOSSIER_PLAN_GUID = "d0551e72-7e72-4170-b1a4-d0551e72d055"
def _apply_dossier_plan_attrs(dmd):
"""Wendet die Dossier-Plan-Visual-Settings auf einen DisplayMode an.
Wird sowohl beim Erstanlegen als auch bei jedem Reload aufgerufen —
so propagieren Attribut-Aenderungen aus dem Code automatisch."""
try:
from Rhino.Display import DisplayModeDescription
except Exception:
return
attrs = dmd.DisplayAttributes
# DEBUG: einmal pro Session alle relevanten Property-Namen loggen damit
# wir sehen welche tatsaechlich existieren (Mac vs Win Rhino unterscheidet
# sich) — sonst werden Set-Attempts still verschluckt.
if not getattr(_apply_dossier_plan_attrs, "_props_logged", False):
try:
relevant = sorted([n for n in dir(attrs)
if not n.startswith("_")
and ("idden" in n or "ngent" in n or "ilho" in n
or "ire" in n or "soc" in n or "dge" in n
or "ech" in n or "hade" in n or "ection" in n)])
print("[OBERLEISTE] Plan-Mode Attrs-Inventar:", ", ".join(relevant))
# Sub-Objekt-Settings sind in Rhino 8 oft eigene Klassen
for sub in ("CurveSettings", "ObjectSettings", "ShadingSettings",
"MeshSpecificAttributes"):
if hasattr(attrs, sub):
obj = getattr(attrs, sub)
sub_props = sorted([n for n in dir(obj)
if not n.startswith("_")
and ("idden" in n or "ngent" in n
or "soc" in n or "ire" in n
or "dge" in n)])
if sub_props:
print("[OBERLEISTE] Plan-Mode {}:".format(sub),
", ".join(sub_props))
except Exception as ex:
print("[OBERLEISTE] Plan-Mode inspect:", ex)
_apply_dossier_plan_attrs._props_logged = True
# Surfaces gefuellt + weiss + kein Shading
try: attrs.ShadingEnabled = False
except Exception: pass
try: import System.Drawing as SD
except Exception: SD = None
if SD:
try:
white = SD.Color.FromArgb(255, 255, 255, 255)
try: attrs.ShadeSurfaceColor = white
except Exception: pass
try: attrs.BackgroundColor = white
except Exception: pass
try: attrs.BackgroundColorTop = white
except Exception: pass
try: attrs.BackgroundColorBottom = white
except Exception: pass
except Exception: pass
# Wires + Isocurves AUS — sonst Plan-Linien-Noise.
# Property-Namen wie sie auf Mac Rhino 8 tatsaechlich existieren
# (per Inspektion verifiziert — frueher hatten wir falsche Schreibweisen
# die das try/except still verschluckt hat).
try: attrs.ShowIsoCurves = False # NICHT ShowIsocurves!
except Exception: pass
try: attrs.SurfaceIsoThicknessUsed = False
except Exception: pass
try: attrs.SurfaceIsoColorsUsed = False
except Exception: pass
try: attrs.ShowTangentEdges = False
except Exception: pass
try: attrs.ShowTangentSeams = False
except Exception: pass
try: attrs.ShowSurfaceEdges = True
except Exception: pass
try: attrs.ShowSurfaceEdge = True # Singular existiert auch
except Exception: pass
# Mesh-Wires AUS — die liegen auf dem Sub-Objekt MeshSpecificAttributes,
# nicht direkt auf attrs. Das sind in Top-Views haeufig die "feinen
# Punkte" auf Brep-Wand-Volumen (Rhino mesht intern fuer Display).
try:
if hasattr(attrs, "MeshSpecificAttributes"):
attrs.MeshSpecificAttributes.ShowMeshWires = False
attrs.MeshSpecificAttributes.ShowMeshVertices = False
except Exception as ex:
print("[OBERLEISTE] Plan-Mode MeshSpecificAttributes:", ex)
# Section-Styles MUESSEN aktiv sein damit Custom-Section-Styles
# (per-Layer oder per-Object) tatsaechlich gerendert werden. Default
# ist False — d.h. Section-Hatches werden zugewiesen aber nicht angezeigt.
# Diagnose: vorher + nachher loggen weil auf Mac Rhino set+UpdateDisplayMode
# diesen Wert manchmal nicht persistiert (wir patchen darum auch direkt
# die ini beim Erstanlegen).
pre_uss = None
try: pre_uss = bool(attrs.UseSectionStyles)
except Exception: pass
try: attrs.UseSectionStyles = True
except Exception as ex:
print("[OBERLEISTE] Plan-Mode UseSectionStyles set:", ex)
try: post_uss = bool(attrs.UseSectionStyles)
except Exception: post_uss = None
print("[OBERLEISTE] Plan-Mode UseSectionStyles pre={} post={}".format(
pre_uss, post_uss))
# Clipping-Edges + Fills sichtbar
try: attrs.ShowClippingEdges = True
except Exception: pass
try: attrs.ShowClippingFills = True
except Exception: pass
try: attrs.ShowClipIntersectionEdges = True
except Exception: pass
try: attrs.ShowClipIntersectionSurfaces = True
except Exception: pass
# Linewidths an — Lineweights-Toggle wirkt
try: attrs.ShowLineWidths = True
except Exception: pass
try:
DisplayModeDescription.UpdateDisplayMode(dmd)
except Exception as ex:
print("[OBERLEISTE] Plan-Mode update:", ex)
_TEMPLATE_INI_PATH = os.path.join(_HERE, "templates", "dossier_plan.ini")
def _ensure_dossier_plan_display_mode():
"""Stellt sicher dass der 'Dossier Plan' Display-Mode existiert.
Strategie: wenn eine Template-ini im Repo existiert
(rhino/templates/dossier_plan.ini), laden wir die. Sonst Fallback auf
Clone-Technical + ini-Patch. Template ist die bevorzugte Methode weil
sich Mac-Rhino-Display-Mode-Properties via Python-API unzuverlaessig
setzen lassen — der User baut den Mode einmal manuell perfekt zusammen
und exportiert ihn dort hin.
"""
print("[OBERLEISTE] Plan-Mode: check...")
try:
from Rhino.Display import DisplayModeDescription
except Exception as ex:
print("[OBERLEISTE] Plan-Mode: DMD nicht verfuegbar:", ex)
return False
import re # fuer ini-checks unten
target_name = "Dossier Plan"
try:
import System
target_guid_obj = System.Guid(_DOSSIER_PLAN_GUID)
except Exception:
target_guid_obj = None
# Template-Datei vorhanden? Wenn ja, Hash davon als "version key"
# benutzen — wir nur neu importieren wenn sich die Template-Datei
# geaendert hat.
template_exists = os.path.isfile(_TEMPLATE_INI_PATH)
print("[OBERLEISTE] Plan-Mode template: {}".format(
"found at " + _TEMPLATE_INI_PATH if template_exists else "missing → fallback"))
# Schon registriert?
try:
existing = None
for dm in DisplayModeDescription.GetDisplayModes():
try:
if dm.EnglishName == target_name or dm.LocalName == target_name:
existing = dm; break
if target_guid_obj is not None and dm.Id == target_guid_obj:
existing = dm; break
except Exception: pass
if existing is not None:
# Mode existiert bereits — in Ruhe lassen. User kann manuell
# loeschen + reloaden wenn er das Template neu laden will.
# Vermeidet delete-loop wenn das Template ini-Werte hat die mein
# alter check als "falsch" einstufte.
print("[OBERLEISTE] Plan-Mode: existing gefunden, keine Aktion (manuell loeschen fuer Refresh)")
return True
# Sonst kein existing → vor dem Import alle Orphan-Modes mit unserer
# Guid ODER Namen "Dossier Plan" wegputzen (alte kaputte Versionen
# ohne Namen sind zuvor manchmal liegen geblieben → Duplikate +
# Rhino-Crashes beim Klick).
try:
cleanup_count = 0
for dm in list(DisplayModeDescription.GetDisplayModes()):
should_delete = False
try:
if dm.EnglishName == target_name or dm.LocalName == target_name:
should_delete = True
elif target_guid_obj is not None and dm.Id == target_guid_obj:
should_delete = True
except Exception: pass
if should_delete:
try:
DisplayModeDescription.DeleteDisplayMode(dm.Id)
cleanup_count += 1
except Exception as ex:
print("[OBERLEISTE] Plan-Mode cleanup delete fail:", ex)
if cleanup_count > 0:
print("[OBERLEISTE] Plan-Mode: {} Orphan-Mode(s) entfernt vor Import".format(
cleanup_count))
except Exception as ex:
print("[OBERLEISTE] Plan-Mode cleanup:", ex)
except Exception as ex:
print("[OBERLEISTE] Plan-Mode list:", ex)
return False
# ----------------------------------------------------------------
# SOURCE: Template-ini bevorzugt (User hat den Mode manuell gebaut +
# exportiert) — sonst Fallback auf Technical-Clone.
# ----------------------------------------------------------------
import tempfile
tmp_path = os.path.join(tempfile.gettempdir(), "dossier_plan.ini")
base = None
if template_exists:
# Template-ini lesen + ueberschreiben den tmp_path
try:
with open(_TEMPLATE_INI_PATH, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
print("[OBERLEISTE] Plan-Mode: Template geladen ({} bytes)".format(len(content)))
except Exception as ex:
print("[OBERLEISTE] Plan-Mode Template read:", ex)
return False
else:
# Fallback: Technical exportieren + patchen
try:
all_modes = list(DisplayModeDescription.GetDisplayModes())
except Exception: all_modes = []
for prefer in ("Technical", "Pen", "Shaded"):
for dm in all_modes:
try:
if dm.EnglishName == prefer:
base = dm; break
except Exception: pass
if base is not None: break
if base is None:
print("[OBERLEISTE] Plan-Mode: kein Basis-Modus gefunden")
return False
try:
ok_export = False
try:
ok_export = bool(DisplayModeDescription.ExportToFile(base, tmp_path))
except Exception as ex:
print("[OBERLEISTE] Plan-Mode ExportToFile:", ex)
if not ok_export:
print("[OBERLEISTE] Plan-Mode: ExportToFile failed")
return False
with open(tmp_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
except Exception as ex:
print("[OBERLEISTE] Plan-Mode fallback read ini:", ex)
return False
try:
# Name-Feld ersetzen (verschiedene moegliche Keys)
import re
try:
content = re.sub(
r'(?i)^(\s*Name\s*=\s*)(.*)$',
r'\1' + target_name, content, count=1, flags=re.MULTILINE)
except Exception: pass
# Guid (im ini meist als [<guid>] Section-Header oder als "id="-Feld)
try:
# Bestehende Guid aus dem File extrahieren + ueberall ersetzen
old_guid_match = re.search(
r'([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})',
content)
if old_guid_match:
old_guid = old_guid_match.group(1)
content = content.replace(old_guid, _DOSSIER_PLAN_GUID)
content = content.replace(old_guid.upper(),
_DOSSIER_PLAN_GUID.upper())
except Exception: pass
# Plan-Mode-Settings in der ini patchen — Rhino-DisplayMode-ini hat
# nested Sections wie [DisplayMode\<guid>\Objects\Surfaces]. Wir
# patchen nur EXISTIERENDE Keys (Rhino's Parser stripped unbekannte
# Keys beim Re-Import wieder).
def _ini_replace(content, key, value):
"""Ersetzt erstes Vorkommen von key=... mit key=value (case-insens).
Liefert (content, found_bool)."""
pat = r'(?im)^(\s*' + re.escape(key) + r'\s*=\s*)(.*)$'
new_content, n = re.subn(pat, r'\g<1>' + str(value),
content, count=1)
return new_content, (n > 0)
# Bei Template-Pfad: NUR Name+Guid normalisieren, KEINE inhaltlichen
# Patches (User hat das Template bewusst so konfiguriert).
# Bei Fallback-Pfad (Technical-Clone): die bekannten Settings forcen.
if not template_exists:
for key, val in (
("ClipSectionUsage", "1"),
("TechnicalMask", "15"),
("ShowIsocurves", "n"),
("ShowTangentEdges", "n"),
("ShowTangentSeams", "n"),
("ShowMeshWires", "n"),
("ShowMeshEdges", "n"),
("ShadeSurface", "n"),
("ClippingShowXEdges", "y"),
("ClippingShowXSurface", "y"),
):
try:
content, found = _ini_replace(content, key, val)
if found:
print("[OBERLEISTE] Plan-Mode ini {}={}".format(key, val))
except Exception as ex:
print("[OBERLEISTE] Plan-Mode ini-set {}={} fail: {}".format(
key, val, ex))
try:
with open(tmp_path, "w", encoding="utf-8") as f:
f.write(content)
except Exception as ex:
print("[OBERLEISTE] Plan-Mode write ini:", ex)
return False
# Import
try:
ok_import = bool(DisplayModeDescription.ImportFromFile(tmp_path))
except Exception as ex:
print("[OBERLEISTE] Plan-Mode ImportFromFile:", ex)
return False
if not ok_import:
print("[OBERLEISTE] Plan-Mode: ImportFromFile gab False")
return False
except Exception as ex:
print("[OBERLEISTE] Plan-Mode clone:", ex)
return False
# Neu importierten Mode holen + tweaken
try:
dmd = None
if target_guid_obj is not None:
try: dmd = DisplayModeDescription.GetDisplayMode(target_guid_obj)
except Exception: pass
if dmd is None:
for dm in DisplayModeDescription.GetDisplayModes():
try:
if dm.EnglishName == target_name or dm.LocalName == target_name:
dmd = dm; break
except Exception: pass
if dmd is None:
print("[OBERLEISTE] Plan-Mode: nach Import nicht gefunden")
return False
# KEIN _apply_dossier_plan_attrs() hier — der wuerde
# UpdateDisplayMode() aufrufen und die ini-Werte (ClipSectionUsage,
# TechnicalMask) mit den Python-Default-Attrs ueberschreiben.
# Die ini hat schon alle richtigen Werte.
src = "Template" if template_exists else (base.EnglishName if base else "?")
print("[OBERLEISTE] Display-Mode 'Dossier Plan' angelegt (Basis: {})".format(src))
# Sanity-Check: liste alle Display-Modes auf damit wir sehen ob unser
# Mode wirklich registriert ist (und mit welchem Namen).
try:
names = []
for dm_check in DisplayModeDescription.GetDisplayModes():
try:
names.append(dm_check.EnglishName)
except Exception: pass
print("[OBERLEISTE] Plan-Mode: alle registrierten Modes: {}".format(
", ".join(names)))
except Exception: pass
# Cache invalidieren damit das Dropdown ihn sieht
try:
global _display_modes_cache
_display_modes_cache = None
except Exception: pass
return True
except Exception as ex:
print("[OBERLEISTE] Plan-Mode tweak:", ex)
return False
_THUMB_SIZE = (480, 320) # 3:2 — kompakt fuer Launcher-Cards
@@ -1730,5 +2089,13 @@ def _bridge_factory():
return b
# Custom Display-Mode 'Dossier Plan' beim Modul-Load registrieren — laeuft
# bei jedem startup.py oder _reset_panels.py, unabhaengig davon ob das
# Panel jemals geoeffnet wird. Funktion ist idempotent.
try: _ensure_dossier_plan_display_mode()
except Exception as ex:
print("[OBERLEISTE] ensure_dossier_plan_display_mode:", ex)
panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory,
icon_spec=("menu", "#2f5d54"))