Compare commits

..

89 Commits

Author SHA1 Message Date
karim d558fac2c3 Styles: default pen for new objects + csharp build doc
Gestaltung panel now shows pen controls (color, lineweight, linetype)
when nothing is selected. Settings persist in sticky and are stamped
onto every newly drawn object (curves, text, hatch, dims — not 3D
solids or DOSSIER element geometry) via the existing AddRhinoObject
listener. Active state shown with badge; resets by switching all back
to "Nach Ebene".

Also adds csharp/BUILD.md with full build + post-reinstall checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 02:00:47 +02:00
karim e8da519d29 Tools + Elements: layout per display mode
- Symbol only ('icon'): uniform grid (square aligned tiles)
- Text / Symbol+Text: original ragged content-width pills that wrap freely
  (flex-wrap), like before the grid redesign
- Container switches grid↔flex-wrap on mode; pill switches full-width↔auto
- Dropdown picks the layout: density grid for icons, readable pills for text
2026-06-06 15:34:07 +02:00
karim cd87a0b7f4 Tools + Elements: horizontal tiles in grid + dropdown moved left
- Tile content back to horizontal (icon beside label, left-aligned) while
  keeping the aligned grid columns — per user choice 'Raster + horizontal'
- Wider 'both' column (86px) to fit icon+label in one row, single-row height
- Display-mode dropdown moved to the left (justify-content flex-start)
- icon-only and text-only modes stay centered; hasMenu chevron inline next
  to label (both/text) or as corner indicator (icon-only)
2026-06-06 15:23:52 +02:00
karim c0624c0a62 Tools + Elements: smaller tiles, display-mode dropdown, dark pill look
- Tiles shrunk (minHeight 46/38/30 per mode, tighter padding, smaller icons)
- Add display-mode dropdown (Symbol / Text / Symbol+Text) at top of each
  panel; choice persists in localStorage under 'dossier_tile_mode' and is
  shared between both panels
- Grid column min-width adapts per mode (icon 40 / text 66 / both 58)
- Restore dark pill look: --bg-input background + border-light, full
  border-radius — tiles read as distinct chips again
- Icon-only mode shows a small corner chevron on dropdown tiles
  (Treppe/Stuetze/Traeger) so the right-click menu stays discoverable
2026-06-06 15:14:29 +02:00
karim e531217cb7 Tools + Elements panels: ragged pills → uniform grid tiles
Both panels shared the same flex-wrap pill layout where each pill sized to
its content, producing a ragged, hard-to-scan right edge. Switch to a uniform
CSS grid (auto-fill, minmax(68px, 1fr)) of icon-over-label tiles:

- ToolsApp: ToolPill → ToolTile, PillGroup → GridSection
- ElementeApp: PillButton → vertical tile (keeps hasMenu chevron, badge as
  corner pill, disabled state); PillGroup → grid section
- Less chrome: borderless tiles on subtle --bg-item, accent border on hover
- Dropdown tiles (Treppe/Stütze/Träger) keep their relative-positioned
  wrapper so PopupMenu still anchors correctly
2026-06-06 14:43:17 +02:00
karim b6ba83eb5d DossierSettings: show restart-required hint after language change
Instead of a temporary 'saved' checkmark, show a persistent warn-colored
'Neustart erforderlich' / 'Restart required' badge (in the newly selected
language) until the window is closed. Guards against no-op clicks (same lang).
2026-06-06 14:34:22 +02:00
karim 05a289dd65 Fix language reload: hasattr('mode') → hasattr('_mode'), val.mode → val._mode
BaseBridge stores mode as self._mode, not self.mode — so the reload
loop never found any bridges and language switching did nothing.
2026-06-06 14:25:26 +02:00
karim 82d0939a18 Fix last German log remnants: [ELEMENTS], re-persisted, after LoadDefaults, exposes 2026-06-06 13:38:04 +02:00
karim 220a1e2bb3 Fix log tags + SAVE_LANG bridge bug
panel_base: add _MODE_LOG_TAG map + _tag() function so Panel
registered/opened use [LAYERS], [TOOLBAR] etc. instead of [EBENEN]/[OBERLEISTE]

toolbar: _open_dossier_settings_panel now uses a custom _SettingsBridge
that actually handles SAVE_LANG -> writes lang to settings + reloads
all panels. Previously the default _SatelliteBridge only handled SAVE/CANCEL
so language switching did nothing.
2026-06-06 13:09:54 +02:00
karim b9a2124026 Rename all log tags to match English module names
[EBENEN] → [LAYERS], [EBENEN-BE] → [LAYERS-BE]
[ZEICHNUNGSEBENEN] → [DRAWING-LEVELS]
[GESTALTUNG] → [STYLES]
[OBERLEISTE] → [TOOLBAR]
[WERKZEUGE] → [TOOLS]
[DIMENSIONEN] → [DIMENSIONS]
[AUSSCHNITTE] → [VIEWPORTS]
[MASSSTAB] → [SCALE]
[SCHNITT] → [SECTION], [SCHNITT_GRIPS] → [SECTION-GRIPS]
[WAND_GRIPS] → [WALL-GRIPS], [TREPPE_GRIPS] → [STAIR-GRIPS]
[CURVE_DOTS] → [CURVE-DOTS]
[panel_base] → [CORE]
[ALIAS-LOADER] → [ALIASES]
[BEGIN-CMD] → [CMD-HOOK]
2026-06-06 12:48:27 +02:00
karim 84ff943f92 Fix remaining partial translations in log messages 2026-06-06 12:35:36 +02:00
karim 24f6b76f06 Translate remaining internal log messages to English
- EBENEN: drawing levels updated, sublayer not found, saved/verified
- GESTALTUNG: Linetypes before/after, fill field, opened/focused
- CLIP: disabled done
- ELEMENTE: Bulk-op, Listener bail
- Global: not found, not available, unchanged, failed, present
2026-06-06 12:19:10 +02:00
karim 9fcada260e Fix remaining German log messages: Panel registered, Listener active, sync all files to PROJECTS
- Fix sed $-anchor issue: 'Panel registriert' now replaced globally
- Translate: Listener aktiv, Select-Handler, Closing-Hook, Doppelklick-Handler
- Translate SPLASH messages: gesetzt/angewendet
- Translate ALIAS-LOADER, WELCOME, ELEMENTE migration messages
- Full rsync of all rhino/*.py to PROJECTS (previously partial sync missed schnitte.py, wand_grips.py, treppe_grips.py, text_editor.py, welcome.py etc.)
2026-06-06 12:09:12 +02:00
karim b9f661cdb3 Translate Python log messages + fix rhinopanel import in PROJECTS
- startup.py: all user-visible messages translated to English
- panel_base.py: icon/rendering log messages translated
- toolbar.py: display mode check messages translated
- Global: Panel registered/opened/listener-active across all modules
- Fix: elemente.py and other files with stale 'import rhinopanel'
  were missing from PROJECTS sync — now properly copied
2026-06-06 11:38:20 +02:00
karim 375487c10c i18n DE/EN + DossierSettings panel + English file renames
i18n:
- src/i18n/de.json + en.json: 200+ keys covering all main panels
- src/i18n/index.js: t(key, vars) reads window.DOSSIER_LANG
- panel_base.py: injects window.DOSSIER_LANG from dossier_settings.json
- EbenenManager, GeschossManager, AusschnitteApp, LayoutsApp: all
  context menus and main labels use t()

DossierSettings panel:
- DossierSettingsApp.jsx: language toggle (DE/EN pill) + launcher status
- toolbar.py: OPEN_SETTINGS opens new Rhino-hosted satellite window,
  SAVE_LANG writes lang to dossier_settings.json + reloads all panels

File renames (JSX → English):
- ZeichnungsebenenApp → DrawingLevelsApp
- GeschossManager/Dialog/Settings → Floor*
- AusschnitteApp/Settings → Viewports*
- EbenenManager/Settings → Layer*
- GestaltungApp → StylesApp, OberleisteApp → ToolbarApp
- WerkzeugeApp → ToolsApp, DimensionenApp → DimensionsApp
- MassstabApp → ScaleApp, KameraApp → CameraApp
- MasseSettingsApp → UnitsSettingsApp
- ConfirmDeleteEbene → ConfirmDeleteLayer
- AusschnittLayerDialog → ViewportLayerDialog

Python module renames:
- rhinopanel.py → layers_panel.py
- oberleiste.py → toolbar.py
- gestaltung.py → styles.py
- werkzeuge.py → tools.py
- dimensionen.py → dimensions.py
- startup.py _MODULE_TO_PY updated, all cross-imports fixed
2026-06-06 11:09:33 +02:00
karim 92b4baa285 UX: WebView native-feel + Context-Menu Redesign + Startup-Fix
- Disable text selection (CSS user-select:none) + block browser
  context menu (contextmenu preventDefault) in all panels
- ContextMenu: pill items, accent hover, entrance animation,
  optional title header — controlled via single ContextMenu.css
- EbenenManager + GeschossManager: show layer name as menu title
- startup.py: skip splash on Cmd+N when plugin already loaded
- startup.py: hook NewDocument so display-modes apply to new docs
2026-06-06 05:27:48 +02:00
karim 18443b60c3 Layer-Smart-Join entfernt — nie verwendet, auto Phase 2 macht's
- Alt+Click bypass im Cluster-Volume-Select-Handler raus.
- _layer_join_attempt + dJoin-Detection in smart_join.py raus.

Phase 2 automatic handling deckt die Use-Cases ab. Falls Manual-Layer-
Merge spaeter doch noetig: commit 118bc51 hat den Code als Referenz.
2026-06-02 01:03:28 +02:00
karim dcfafb18d1 T-Junction angled: angle-aware Extension + Miter fuer flache T-Oberseite
Bei angled T-Junctions (nicht-90°) ragten Column-Ecken ueber Through-
Wand-Face raus (= "Stummel"). Fix in zwei Pfaden:

Cluster-Union (solid walls, _build_cluster_union_brep):
- Extension entlang my_tan = n_half / |cross(my_tan, n_tan)|. Fuer 90°
  bleibt = n_half, fuer angled wird laenger damit mitered End-Face auf
  Through-Body-Far-Face landet.
- Miter mit Nachbar-Tangent als miter_dir clippt L+R Curves an flache
  horizontale Linie aligned mit Through-Wand-Richtung.
- _make_volume_geometry erhaelt miter_start/_end.

Phase 2 (layered walls, _regenerate_element_body):
- Gleiches angle-aware Extension-Factor fuer _backbone_axis_ext.
- _layer_rect_2d erhaelt miter_start/_end Parameter.
- Backbone-col bekommt Miter am extended-end-Position.
- Non-backbone Spalten bekommen Std-Miter am Snap-Position.
- Beide aligned mit through-tan (b_tan) Richtung → T-Oberseite flach.

Diagnostic-Prints (bb-ext-calc, PRE-CARVE, CARVE cb, DO/SKIP carve)
bleiben drin fuer kommende Edge-Cases.
2026-06-02 00:50:09 +02:00
karim 118bc51cc5 T-Junction Phase 2 + Layer-Smart-Join: Backbone-Drill bis Stahl-Layer-Far-Face
Phase-2-Fixes:
- Backbone-Ext berechnet bis MATCHING-Layer (Stahl) far-face, nicht bis
  Body-far-face. Beton drillt durch Stahl-Band, nicht durch Daemm/Putz.
- Case A/B detection via dot(out_dir, RhinoPerp). Rhino Curve.Offset
  benutzt (tan × +z) = (b_tan.Y, -b_tan.X), nicht (-b_tan.Y, b_tan.X).
- Backbone-axis-ext separat von _my_axis_ext: nur Backbone-Column
  extended, non-backbone columns stoppen am Snap.
- Through-only Mats (z.B. Daemm wo T-stem keinen hat) werden auch
  durchlaufen damit hoehere-prio backbone diese carven kann.
- Post-Carve Union: gleiche-Material-Pieces mergen (Backbone-Beton-Col
  + Through-Stahl-Band → T-Shape).
- BBox-overlap strict-Filter vor BoolDiff: touching coplanar faces
  ueberspringen, vermeidet Rhino BoolDiff "punch-through" Artefakte.
- _has_my_cols guard: KeyError beim consume von T-stem-Layern fuer
  through-only mats verhindert.

Layer-Smart-Join (smart_join.py):
- Neue _layer_join_attempt: 2 selektierte wand_volume Breps gleicher
  Material via BoolUnion mergen. Manueller Override fuer Edge-Cases
  wo auto Phase 2 nicht reicht.

Cluster-Volume Select-Handler (elemente.py):
- Alt-Click bypassed Cluster-Swap → User kann einzelne Layer-Breps
  direkt anwaehlen (= fuer Layer-Smart-Join).
2026-06-01 21:54:02 +02:00
karim df56a54b66 T-Junction Phase 2 mit 3D Brep Union + Material-Prio-Carve
Asymmetric L-merge fuer Schichtdurchdringung:
- Backbone (= hoechste Material-Prio in beiden Waenden) bildet T-form
- Non-backbone Layer werden gecarved mit backbone-Column + through-Bands
  hoeherer Prio
- ext=0 fuer T-Stem-Axis (= column endet am snap, kein "drueber")
- 3D Brep Union via Brep.CreateBooleanUnion mit cross-junction safety
  (= aktueller through-Brep statt von Meta neu zu bauen)
- Cleanup: MergeCoplanarFaces

Plus:
- Innenwand Beton 20cm Style (Putz + Beton + Putz, ref-mid)
- curve_vertex_dots.py: gruene Vertex-Punkte fuer Polylinen/Curves
- Cluster-Volume Select Handler: Shift-Modifier fuer Multi-Select
- startup.py: Top-View maximieren on Doc-Open

Known limitation: Putz-Schicht kann in bestimmten Konfigurationen visuell
suedlich des Daemm-Band-Top weiter sichtbar sein (= edge case fuer
asymmetric layers). Naechster Schritt: manuelle 2D-Polygon-Konstruktion
statt 3D Boolean.
2026-06-01 14:32:55 +02:00
karim 9cce8199c3 T-Join 1-Wand-Modus: nur die zu snappende Wand selektieren reicht
UX-Verbesserung: User-Frage "muss ich nur das Element anwaehlen das ich
snappen moechte?" — jetzt ja.

Wenn nur 1 wand_axis in der Selection ist, sucht T-Join automatisch die
naechste andere Wand-Achse im Doc (innerhalb 1m) und snappt die selektierte
Wand auf jene. Die andere bleibt unangetastet — wie bei klassischem T-Stem
gegen Through-Wand.

2-Wand-Modus bleibt: dann werden GENAU die beiden selektierten betrachtet
(z.B. wenn 3+ Waende in der Nähe sind und User exakt eine andere meinen will).

Selection-Hint vereinfacht: nur noch warnen wenn 0 Wand-Achsen aber wand-
Objekte vorhanden (z.B. nur Volumen selektiert, wo Axis nicht mitkam).
2026-05-31 13:44:40 +02:00
karim e50134ce32 T-Join Tolerance auf 1m gelockert + Selection-Hint
User-Test: Bei T-Konfig waren Wand-Achsen 55cm auseinander (vermutlich
auf Outline statt Axis gesnappt). Mit 20cm tol fiel T-Join durch → L-Join
machte Tangent-Schnittpunkt (= L-Form falsch fuer T-Intent).

Fix: T-Join Snap-Radius von 20cm auf 1m. Generous genug fuer typische
Drift-Faelle (Outline-Snap statt Axis-Snap, ungefaehre Platzierung etc.),
aber tight genug damit absichtliche Lücken (>1m) nicht versehentlich
zugesnapped werden.

Plus: Hint wenn < 2 Wand-Achsen selektiert → User weiss explizit dass
GENAU 2 Wand selektiert sein muss.
2026-05-31 13:42:20 +02:00
karim c81d2c0c43 T-Join Diagnostic: zeigt warum jeder Endpunkt-Check verworfen wurde
User-Test: L-Join layered funktioniert (3/3 layers built). Aber T-Join laeuft
fuer layered immer in den L-Join-Fallback. Um zu sehen warum, jetzt fuer
jeden der 4 Endpunkt-Checks im T-Join:
- distance zur anderen Curve
- distance zur deren Endpunkten
- Reason warum verworfen (zu weit / nahe Endpunkt / schon snapped)

So koennen wir live im Log sehen ob T-Stem geometrisch zu weit weg ist
oder ob er an einem Endpunkt der Through-Wand landet (= L-Sache).
2026-05-31 13:20:27 +02:00
karim 26214a704d Replace-Listener: wand_layered/wand_layers/wand_style_id/wand_joint_rolle nach Replace bewahren
ROOT CAUSE: Wenn doc.Objects.Replace einer wand_axis ausgefuehrt wird (z.B.
nach L-Join von smart_join), prueft der Replace-Listener ob das neue Object
schon Meta hat. Wenn nicht, re-attached er via _attach_meta — aber die
wand-spezifischen Felder (wand_layered, wand_layers, wand_style_id,
wand_joint_rolle) wurden NICHT mit-uebergeben.

Effekt: layered Wand verlor wand_layered=True nach L-Join → naechster Regen
sieht is_layered=False → baut single SOLID Brep statt per-Layer-Breps →
"die gemerged walls sind dann leider solid walls".

Fix: Im Re-Attach-Aufruf die 4 wand-Felder mit weiterreichen aus meta.

Folge-Effekt: auch wand_style_id und wand_joint_rolle waren weg, was
Material-Lookup + Joint-Rolle-Override gebrochen hat. Beide jetzt fix.
2026-05-31 13:07:02 +02:00
karim 3e54fa46a6 smart_join: UserString-Key war FALSCH ("dossier_type" statt "dossier_element_type")
Bug: in _walls_and_curves_from_sel + safety check + diagnostic wurde
"dossier_type" als UserString-Key gelesen, aber der echte Key (definiert
in elemente.py via _KEY_TYPE) ist "dossier_element_type".

Effekt: kein einziges Objekt wurde als wand_axis/wand_volume erkannt.
ALLES landete im "elif t == '':" Branch (= generic curves).

Solid L-Join funktionierte per ZUFALL: bei Solid-Wand (axis + outline +
volume) sind nur 1 Curve open (axis); outline ist closed rectangle. Bei
2 Solid-Waenden waren also 2 offene Curves in generic → L-Join fand die
2 vermeintlich generic Curves (= eigentlich Achsen).

Bei Layered scheiterte es: 2 Achsen + 2 Centerlines = 4 offene Curves im
generic. L-Join Bedingung "len(generic) == 2" nicht erfuellt → silent
return → Fallthrough zu _Join.

Fix: alle 4 Vorkommen auf "dossier_element_type" gefixt. Jetzt erkennt
smart_join Waende richtig, dedupliziert per wall_id, und T-Join/L-Join/
Safety-Check funktionieren wie geplant.
2026-05-31 12:49:42 +02:00
karim 17ff7a8017 T-Junction-Toleranz auf 1cm gelockert + dJoin-Safety: keine _Join auf Achsen
T-Junction-Detection (_detect_t_junction): pos_tol von 1mm auf 1cm erhoeht.
User-Feedback: bei manuellem Snap kann's leicht ein paar mm danebengehen,
1mm war zu eng. 1cm ist immer noch tight genug fuer Architektur-Workflow.

smart_join (dJoin): Safety-Check vor _Join-Fallback. Wenn IRGENDEINE
wand_axis in der Selection ist (auch zusaetzlich zu anderen Curves), wird
NICHT auf Standard-_Join gefallen — sonst kleistert _Join mehrere Wand-
Achsen zu einer Polyline zusammen, der Listener detektiert das als
"Source-Duplikat" und vergibt neue Wall-IDs → alle Meta-Verknuepfungen
brechen.

Stattdessen: Print-Meldung dass T-Join/L-Join nicht gegriffen hat. User
muss GENAU 2 Waende selektieren die verbunden werden sollen.
2026-05-31 12:29:54 +02:00
karim 9999f3d0ad Schichtdurchdringung am T-Junction: pro Schicht material-basiertes Verbinden
Bisher: bei T-Junction stoppen ALLE Layers des T-Stems uniform am Near-Face
der Through-Wand (uniformer T-Miter pro Wand).

Neu: per-Schicht Logik im _make_wand_layer_breps:
- T-Stem-Schicht mit Material das in Through-Wand auch vorkommt → Layer-Axis
  extends um through_dicke/2 → Layer durchstoesst Through-Wand bis zur
  Far-Face → visuell verbunden, kein sichtbarer Joint
- Schicht ohne Material-Match in Through-Wand → standard T-Miter, stoppt
  am Near-Face

Beispiel "Aussenwand 30cm" (Beton/Daemmung/Putz) T auf gleicher Aussenwand:
alle 3 Schichten matchen → durchgehend gemerged.

Beispiel "Aussenwand 30cm" T auf "Beton solid 15cm":
- T-Stem Beton matched (Through-Solid hat Beton via Style) → durchstoesst
- T-Stem Daemmung/Putz: kein Match → stoppen am Through-Beton-Aussenkante

Implementation:
- _make_wand_layer_breps: per_layer_ext_start/end + per_layer_miter_start/end
- _t_junction_layer_overrides(doc, my_meta, through_meta, ...): baut die
  per-Layer Overrides via Material-Set-Lookup. Solid through wird via
  _wand_solid_material auf Style-Material aufgeloest.
- Regen-Pfad: t_junction_start/end speichern (oid+tan+dicke+ep+out_dir),
  vor _make_wand_layer_breps die Overrides bauen.
2026-05-31 12:07:32 +02:00
karim 3609236da9 dJoin: T-Join Ergaenzung — Endpunkt einer Wand mitten auf andere snappen
Bisher konnte dJoin nur L-Verbindungen herstellen (zwei Endpunkte zum
Schnittpunkt der verlaengerten Tangenten ziehen). Neu auch T-Verbindungen:

_t_join_attempt: pro Endpunkt-Kombination wird der naechste Punkt auf der
ANDEREN Curve gesucht. Wenn distance < 20cm UND nicht nahe deren Endpunkt
(= waere L-Sache) → snap diesen Endpunkt exakt auf die Curve. Die andere
Curve bleibt unveraendert (= Through-Wand stays).

_run: T-Join wird ZUERST probiert (spezifischer), L-Join als Fallback.

UX: User selektiert 2 Waende die fast aber nicht ganz verbinden →
Cmd+J (dJoin) → System erkennt T- oder L-Konfig und snappt entsprechend.
Predictable + intentional, kein auto-snap-Magic mehr.
2026-05-31 11:45:53 +02:00
karim e9e727c66f Auto-Snap on Move zurueckgenommen — zu magisch, User wollten Kontrolle
User-Feedback: bei vielen Waenden im Layout wurde unklar wann sich Wand-
Endpunkte automatisch verbinden und wann nicht. Auto-Snap auf Move/Mirror
machte das Verhalten unvorhersehbar.

Jetzt zurueck zum Standard-BIM-Workflow:
- Wand-Endpunkte verbinden sich NUR wenn User explizit Rhino's End/Mid-
  Snap nutzt (oder das L-Join in dJoin)
- T-Junction-Detection bleibt 1mm-tight → praezise Geometrie erforderlich
- Joint-Rolle (auto/durchgehend/anstossend) regelt weiterhin wer am
  geprueften T-Stoss durchgeht

Helper _snap_endpoint_to_other_wand_axis bleibt — fuer evtl. spaetere
explizite Connect-Befehle.
2026-05-31 11:42:53 +02:00
karim bc87ae1acc Per-Wand Joint-Rolle: explizit waehlen wer am T-Stoss durchgeht
Neues UserString-Feld 'dossier_wand_joint_rolle' per wand_axis:
- 'auto' (default): bisherige Logik. Bei beidseitig auto entscheidet die
  Style-Prio (hoehere = durchgehend)
- 'durchgehend': diese Wand ueberschreibt → ich gehe durch, T-Miter wird
  NICHT auf mich angewendet
- 'anstossend': diese Wand stoppt immer am Joint, auch wenn ich Prio-haerter waere

T-Junction-Detection in regen ruft jetzt _wand_should_apply_t_miter:
- my.rolle entscheidet zuerst
- bei my=auto: through.rolle entscheidet
- bei beide=auto: Prio-Vergleich (hoehere Prio = durchgehend)
- Default: T-Miter applied (= ich stoppe am Through)

Frontend: neue Dropdown 'Joint' in WallProperties zwischen Stil und Aufbau:
- Auto (Prio entscheidet)
- Durchgehend
- Anstossend

Backend-Pipeline: _attach_meta + _read_meta um wand_joint_rolle erweitert,
state-JSON sendet 'jointRolle', _update_wall_body handhabt jointRolle-Patch.
2026-05-31 08:04:17 +02:00
karim 0171785b42 dWall: Aufbau-Toggle (Solid/Mehrschichtig) + gefilterte Stil-Liste
Cluster-Dispatch: is_layered_meta Check zurueck — layered + non-linear (T,
Verzweigung) ist geometrisch sinnlos via per-Layer-Union (Schichten orthogonal
zwischen perpendikulaeren Waenden). Faellt zu Solo+Miter durch.

dWall-Prompt:
- Neue Option 'Aufbau' (Solid|Mehrschichtig)
- Stil-Liste gefiltert auf den gewaehlten Aufbau-Typ
- Beim Toggle: erster Style des neuen Typs uebernommen + dicke/referenz
- Default-Aufbau aus dem zuletzt verwendeten Style
2026-05-31 00:26:51 +02:00
karim f011e2ca94 Cascade-Cleanup: wand_centerline + wand_outline beim wand_axis-Delete mit-loeschen
Bug: nach _Delete einer Wand blieben die Hilfslinien (Centerline, Outline)
als Orphan-Curves stehen. _find_all_volumes filtert nur VOLUME_TYPES, in
denen Centerline/Outline nicht sind.

Fix: bei wand_axis-Delete-Event zusaetzlich alle wand_centerline/wand_outline
Curves mit derselben wall_id ID-Liste ergaenzen → nach 500ms cascade-delete
raeumt sie mit weg.

Layered Cluster + per-Layer BooleanUnion via _build_cluster_layered_breps
+ erweiterter _regen_cluster_anchor sind im selben commit drin (siehe
diff vor diesem Bugfix).
2026-05-31 00:03:10 +02:00
karim 5fdad504da Stale-Detection bei Project-Settings: nur betroffene Waende regenen
Bisher: bei jedem Project-Settings-Save wurden ALLE wand_axis im Doc regenert
(auch wenn nichts wand-relevantes geaendert wurde, z.B. nur ein Default-Wert
oder ein Library-Eintrag).

Jetzt: Diff der wand_styles + materials vor/nach Save. Sammle nur die Waende
die einen geaenderten Style nutzen ODER ein geaendertes Material (direkt via
Style oder als Schicht in layered Wand). Regen nur diese.

Plus: Joint-Cache-Batch-Flag waehrend des Regens setzen (war im sync-Pfad
sonst pro Regen invalidiert worden → unnoetiger Overhead).

Log zeigt jetzt z.B. "3 Waende regenert (1 Stile, 2 Materials geaendert)"
statt blind "N Waende regenert".
2026-05-30 23:45:45 +02:00
karim 0cc06364e8 Pre-warm OpenNURBS native libs am Plugin-Start
Beim install_listeners() werden dummy Curve.Offset, Extrusion.Create und
Brep.CreateBooleanUnion/Difference Aufrufe gemacht damit der lazy-loaded
native code geladen ist. Soll den First-Call-Lag bei der ersten echten
Wand-Operation reduzieren.

In der Praxis hilft das bei der Tech-Drawing-Failure nicht (das ist eine
Rhino-interne Limitation der Hidden-Line-Analyse die mit rapid brep
Delete+Add nicht klar kommt) — aber schadet auch nicht und kann andere
Cold-Start-Edge-Cases verbessern.
2026-05-30 23:39:59 +02:00
karim 5c7611ad40 Tech-Drawing-Fallback fix: batch-flag im sync Post-Cmd Regen
Bug: Beim Verbinden/Trennen zweier Waende via Move (= synchroner Replace-
Event-Pfad) lief jeder _regenerate_element-Call ohne den
_dossier_regen_batch_active Flag → jeder Regen invalidierte den Joint-Cache
neu (O(N²) Rebuilds) → Rhinos Technical-Drawing-Analyse choked →
"Switching all technical views to wireframe display".

Fix: Im post-Command Regen-Loop (line ~17285) den Flag setzen + Cache
einmal invalidieren bevor die affected_walls durchlaufen. Pattern analog
zur Idle-Batch-Logik die das schon hatte.

Outline + Centerline wieder aktiviert — waren nicht der Schuldige.
2026-05-30 23:16:09 +02:00
karim 66971eaa7a Wand-Outlines temporaer deaktiviert (Tech-Drawing-Konflikt)
Die Outline-Curves lagen exakt auf z=0 wie die unteren Brep-Edges. Rhinos
Technical-Drawing Hidden-Line-Analyse choked nach jedem Regen mit Duplikat-
Linien → wireframe-Fallback.

- _regen_wall_lines.outline branch auskommentiert
- _migrate_strip_wand_outlines_once raeumt existierende wand_outline-Curves
  beim naechsten Doc-Open weg
- Centerline (offset, dashed, locked) bleibt — kein Edge-Overlap

Wenn Outline-Visualisierung wieder gewollt: via Display-Conduit (visual-only,
keine Tech-Drawing-Interferenz).
2026-05-30 16:54:35 +02:00
karim 98824c1680 Wand-Stil im Elemente-Panel editierbar
Backend
- wand-State enthaelt jetzt styleId (aus _KEY_WAND_STYLE_ID)
- STATE-Payload sendet wandStyles (analog oeffStyles)
- _update_wall_body handhabt styleId-Patch: bei Stil-Wechsel uebernimmt die
  dicke aus dem Stil (wenn nicht explizit im selben Patch ueberschrieben);
  wand_style_id wird per _attach_meta auf die Achse persistiert

Frontend
- WallProperties bekommt wandStyles-Prop + zeigt Stil-Picker zwischen
  Geschoss und Aufbau (nur wenn Stile vorhanden)
- Dropdown: "kein Stil" + alle definierten Stile mit (dicke, prio)
- PropertiesView + ElementeApp + ElementePropertiesApp propagieren wandStyles
2026-05-30 16:48:54 +02:00
karim 080659ab95 Wand-Hilfslinien: Outline + Centerline auf allen Waenden, Petrol-Farbe, Pairing-Verfeinerung
Outline + Centerline
- Outline-Curves (geschlossenes Viereck) jetzt fuer ALLE Waende (auch
  Cluster/Chain-Member) — in_cluster-Flag suppressed Outline nicht mehr
- Mode = Normal (statt Locked) damit ObjectColor (Petrol #5fa896) durchschlaegt
  und doc.Objects.Replace bei Pure-Transform funktioniert
- Trade-off: User kann Hilfslinien greifen, aber jeder Axis-Regen schreibt sie
  neu (selbst-korrigierend)

Pure-Transform Sync
- wand_centerline + wand_outline werden im Pure-Transform-Pfad explizit
  mit-transformed (vorher nur SOURCE_TYPES + VOLUME_TYPES → Linien blieben
  in alter Position haengen)

Selection Pairing
- wand_axis zurueck in _PAIRED_SOURCE_TYPES → Achsen-Klick selektiert
  Outline + Centerline mit (alle "Referenzlinien" leuchten zusammen auf)
- _collect_partners special-case fuer wand_axis: NUR Hilfslinien als Partner,
  NICHT das Volume (sonst wuerde das ganze Brep mit-aufleuchten)
- wand_volume Pairing wie bisher: Volume-Klick → Achsen + Centerlines +
  Outlines aller Cluster-Members
2026-05-30 16:37:18 +02:00
karim 250853d7d0 Waende: Cluster-Boolean-Union + Click-UX + Outline/Centerline + Smart-L-Join
Geometrie
- _find_wall_cluster: BFS ueber alle same-material verbundenen Waende inkl.
  T-Junctions (Stem auf Through-Achse + Through-Wand-Mitte erkannt)
- _build_cluster_union_brep: per-Wand-Rect-Extrude + Boolean-Union zu einem
  einheitlichen Brep. Walls ueberlappen am Joint via Extension um
  nachbar_dicke/2 (Far-Face-Reach ohne Stummel)
- _regen_cluster_anchor: Anchor-Pattern wie Chain — anchor haelt cluster_brep
  + alle openings als BoolDiff cutouts pro Member-Wand
- _is_linear_chain: nur lineare 2-Wall-Endpoint-Sequenzen → existing
  Polyline-Extrude. Komplexe Cluster (verzweigt / mit T-Junction) → Union

Auto-T-Snap
- _t_snap_to_wand_axis mit zwei Pfaden:
  - Volume-Hit: IsPointInside (strict=False) auf wand_volume Brep → snap zur
    naechsten Cluster-Achse, unabhaengig von Wand-Dicke
  - Axis-Near: dynamische Toleranz max(15cm, dicke/2+10cm) → dicke Waende
    kriegen groessere Snap-Zone
- Endpunkt-Bias 10cm → naher Endpunkt gewinnt fuer saubere Corner
- Aufruf in _collect_wall_polyline + first-pt der Wand-Erstellung

Click-Verhalten
- _ClusterVolumeSelectHandler (MouseCallback): in Plan-View
  - Klick INNEN im Volume → naechste Achse selektieren
  - Klick auf Vertex (12 px) → Volume selektieren (Standard)
  - Klick auf Edge (8 px) → Volume selektieren (Standard)
  - Klick direkt auf Achse (5 px) → Rhino-Standard, Achse selektiert
- wand_axis aus _PAIRED_SOURCE_TYPES raus → Klick auf Linie selektiert NUR
  die Linie (kein Mit-Selektieren des Volumens)
- wand_volume bleibt in _PAIRED_VOLUME_TYPES + _collect_partners erweitert:
  Volume-Klick sammelt alle Cluster-Member-Achsen + Centerlines + Outlines
  → alle Referenzlinien leuchten bei Volume-Klick mit auf
- Auto-Group fuer alle Waende entfernt + Startup-Migration
  _migrate_strip_wall_auto_groups_once raeumt alte Memberships

Outline + Centerline
- _make_wall_centerline: parallele Achse-Offset bei ref != mid → Centerline
- _make_wall_outline: geschlossenes Viereck (linker + rechter Offset +
  perpendikulare Caps)
- _regen_wall_lines: LOCKED Curves auf Referenzen-Sublayer
  - Centerline (dashed): nur bei ref=left/right
  - Outline (solid): nur Solo-Waende (Cluster-Member ueber merged Brep)
- Beide mit dossier_type-Tag fuer Cleanup beim naechsten Regen

Smart-L-Join (dJoin)
- _l_join_attempt: 2 OFFENE Curves mit nicht-parallelen Tangenten →
  unendliche-Linien-Schnitt + Endpunkte beider Curves auf Schnittpunkt
  ersetzen (extend / shorten zu L)
- _walls_and_curves_from_sel: dedupliziert Selection via wall_id, akzeptiert
  axis+volume Auto-Group als 1 Wand
- Fallback zu Standard _Join wenn nicht passend

Performance
- Joint-Cache per-batch invalidieren statt per-regen (sticky
  _dossier_regen_batch_active)
2026-05-30 16:12:48 +02:00
karim 18d6d98e07 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
2026-05-30 12:46:53 +02:00
karim 7930705d01 L-Treppe: Schrittmass-Clamp bei Setzen + 3D-Podest Lage-aware
Bisher griff der Schrittmass-Clamp nur bei treppe_art=='gerade'.
Fuer L-Treppen jetzt:
- gp2 (Eck-Klick): clampt erste Lauf-Laenge auf [1-Stufe-min, (n-1)-Stufen-max]
- gp3 (End-Klick): schaetzt N1 aus erster Lauf-Laenge, clampt zweiten Lauf
  auf den S/A-konformen Bereich (mit cut_back kompensiert)

Volume-Fix _make_treppe_l_volume:
- cut_back am Eckpunkt war hardcoded half_b (= passt nur fuer Lage=mid).
  Fuer Lage=links/rechts wird die FULL breite cut-back gebraucht — sonst
  ueberlappen die Lauf-Volumen am Eckpunkt mit dem Podest falsch.
  Fix: cut_back = half_b if mid else breite.
2026-05-28 12:57:37 +02:00
karim d8966cc035 L-Treppe: 3-Punkt-Axis behalten + Referenz IMMER aussen
User-Feedback: L-Treppe bleibt 3-Punkt-Polyline (Start/Eck/End), Podest
ergibt sich aus dem Eckpunkt + Breite. Aber Referenz darf nie 'mid'
sein — sonst kollidieren die Laeufe am Eck. Constraint:
- _update_wall: bei treppe_art=='l' und tref=='mid' → 'links' erzwungen
- Frontend: REF_OPTIONS filtert 'mittig' raus wenn treppeArt=='l'

Dead-Code: _l_segments + _aussen_l_polygon + _lauflinie_l + _make_treppe_l_volume
behalten 4-Punkt-Handling als optionalen Pfad (gerade nicht erreicht weil
Creation immer 3-Punkt produziert) — schaden nicht, koennen spaeter
wieder aktiviert werden wenn Podest-als-Segment doch gewuenscht.

Schlafsession-Status: Trittmass-Lock + Lauflinie zentriert + Pfeile +
Cmd+Z + Pure-Transform fuer hidden Layer = alles in main.
2026-05-28 12:49:32 +02:00
karim e406e8d9b2 L-Aussen-Polygon: Z-Konsistenz fix + try/except defensive
_line_intersect_xy lieferte Z=0 was nicht zum Polygon-Z (=OKFF) passte,
PolylineCurve mit gemischten Z konnte fehlschlagen → 2D-Generierung
broken fuer L-Treppen. Fix: _at_z(p, fallback) Helper setzt Z explicit.
Plus try/except um die ganze Polygon-Konstruktion — Fallback auf
2-Rechteck-Variante bei Fehler.
2026-05-28 02:40:09 +02:00
karim bb64e4d41e Lock: Targets clearen bei Disable + Wendel-Sweep clamp auf 2π 2026-05-28 02:19:22 +02:00
karim 6060c74b17 Treppe L+Wendel: Lauflinie, Aussen-Polygon, Pfeil-voll, Lock + Cmd+Z
Cmd+Z:
- _update_wall wrapped in BeginUndoRecord/EndUndoRecord — sodass
  Property-Patches + Regen-Delete/Add als ein Undo-Schritt rueckgaengig

L-Treppe Aussenlinie:
- _aussen_l_polygon: sauberes 6-Punkt L-Polygon mit korrekt projizierten
  Ecken (Outer + Inner via Linien-Schnitt der mid-versetzten Seiten)
- _aussen_l: nutzt Polygon wenn kein Cut, faellt sonst zurueck auf
  per-Lauf Rechtecke mit Diagonal-Cut (wie bisher)

L-Treppe Lauflinie:
- ueber BEIDE Laeufe, mid-perp versetzt, Pfeil am Treppen-Ende
- Eck-Mitte als Linien-Schnitt der zwei versetzten Schaft-Linien →
  sauberer Übergang auch bei Lage=links/rechts (kein Versatz an der Ecke)
- 'voll'-Pfeil-Style mit wide-Offsets relativ zur Lauflinie

Wendel-Treppe:
- _lauflinie_wendel: 'voll'-Pfeil-Style mit r_inner/r_outer-Spitzen
  relativ zu r_mid (Radial-Offsets)

Trittmass-Lock auf alle Treppen-Arten:
- L: Beide Laeufe proportional skalieren (ratio = N*target_A / (L1+L2))
- Wendel: Sweep-Winkel anpassen (new_delta = sign × N*target_A/r_mid)
- Axis-Geometrie wird in-place via Replace ausgetauscht — Source moves
  fliessen in regulären Regen-Pfad ein
2026-05-28 02:16:57 +02:00
karim 970281e10a Treppen UX-Polish: Start-Z, Trittmass-Lock, Pfeil-Stile, Grips
Properties-Panel:
- Konsistentes 50px/1fr/14px Grid fuer alle Treppen-Rows
- Lage + Unten als Dropdown (lowercase Labels)
- Versatz: Dropdown (Geschoss-OKFF) oder eigenes Z mit Input + x-Button
- Ziel: gleich (Geschoss-Liste oder eigene Hoehe), Geschosse-Filter
  excludes das Start-Geschoss
- Start-Dropdown filtert auf okff < Ziel-Z (kein hoeheres Geschoss als
  Start waehlbar, beachtet auch eigene-Hoehe-Ziel)
- Stufen: Dropdown 2-40 (statt freie Eingabe), mit Lock nur S-konforme
  Werte
- Dropdowns nutzen System-Font (statt mono)
- Ausgrenzung 'Aussenlinie'-Toggle (Aussenlinie immer an)
- Pfeil-Style-Dropdown unter Lauflinie-Checkbox: klassisch / gefuellt
  (Solid-Hatch) / breit / voll (Spitzen bis Treppen-Aussenkanten)

Backend Treppe:
- Start-Z-Override via treppe_uk_over (m Offset relativ zu Geschoss-OKFF)
- 2D-Symbol bleibt auf OKFF (egal ob Versatz) — Symbol klebt am Boden
- Lauflinie-Schaft auf visuellen Treppen-Mittelpunkt versetzt
  (bei Lage=links/rechts), nicht mehr auf der Referenz-Achse
- Trittmass-Lock: treppe_lock_s + target_S/A. Beim Aktivieren werden
  S+A als Ziel gespeichert. Bei H-Change wird N=round(H/target_S)
  recomputed + Axis-Laenge auf N*target_A angepasst (gerade Treppen)
- Bruchsymbol-Toggle aus: ganze Treppe ungesplittet zeichnen
  (eff_cut_h=0 → kein Lower/Upper-Split)
- Treppen-Endpunkt-Marker (treppe_grips.py) — gruene Punkte an Start/
  Ende der Lauflinie, beachtet treppe_art (Wendel: poly[1]/poly[2])

Verdoppelungs-Fix:
- _find_target_volume skipt treppe_2d_symbol explicit (sind 2D-Curves,
  kein Volume). Vorher konnte Replace(curve, brep) fehlschlagen → das
  echte Treppen-Brep blieb stehen + neues kam dazu → Duplikat
- _find_objects_by_wall_id mit HiddenObjects+LockedObjects-Iterator,
  findet auch Objs auf hidden 3D-Layer
- Anti-Dup-Cleanup in _regenerate_element: bei mehreren treppe_volume
  mit gleicher element_id → alle ausser dem ersten loeschen

State-Pipeline:
- geschosse-Liste enthaelt jetzt okff+hoehe (fuer Frontend-Constraints)
- Treppe-State neu: ukOver, arrowStyle, lockS, targetS, targetA
- Hidden-Source-Fallback in _send_state findet auch Treppen wenn der
  3D-Layer aus ist (sodass Properties-Panel angezeigt wird)

Dimensionen-Panel:
- on_select + on_idle skippen waehrend Partnership-Cascade oder
  User-Transform — kein Flicker mehr beim Drag

Andere:
- Wand-Polyline-Vertex-Grips (alle Vertices, nicht nur Enden)
- PopupMenu unterstuetzt _divider + checked-Items
- TREPPEN/RAEUME Layer-Migration auf Capital-Case
- selection-partnership tolerant: hidden Source wird trotzdem in die
  Selection genommen (sonst kann Drag nicht durch Pure-Transform)
2026-05-28 02:09:38 +02:00
karim bcf7d557b1 Treppen 2D-Plansymbol + Pure-Transform fuer hidden Layer
Frontend:
- 2D-Plansymbol pro Treppe (Tritte/Lauflinie/Aussenlinie/Bruchsymbol)
  mit per-Treppe-Toggles in Properties-Panel
- 'Obere Stufen gestrichelt'-Toggle splittet Tritte/Aussenlinie an
  Schnittebene; Lauflinie hat zwei Pfeile bei Bruch
- Wand-Polyline-Grips fuer alle Vertices (nicht nur Enden)
- PopupMenu unterstuetzt Divider + Checkbox-Items

Backend:
- Eigener Layer 41_Treppen_2D fuer Plansymbol, Layer-Default schwarz
- Aussenlinie-Polygone folgen der Bruch-Diagonale (kein Versatz mehr)
- Linetype-Fallback laedt Dashed bei Bedarf nach
- Tritten-immer-an (Toggle entfernt), Z auf Geschoss-OKFF
- TREPPEN/RAEUME Layer-Migration auf Capital-Case (Treppen/Raeume)
- Selection-Partnership: treppe_2d_symbol pairs in axis + volume

Pure-Transform fuer Treppen-Move:
- treppe_2d_symbol + treppe_volume in VOLUME_TYPES → cascade-Support
- Phase 1.5 Volume-only-Detection: wenn Source unbewegt aber Volumes
  uniform translated → synthetisiere canonical aus Avg-Delta der
  bewegten Volumes (unbewegte rausgefiltert sonst Verzerrung)
- Hidden-inclusive ObjectEnumerator in Snapshot + Apply-Loop damit
  hidden treppe_axis auf 40_Treppen mit-transformiert wird
- Properties-Fallback im _send_state findet hidden Sources via
  expliziter Iteration → Panel zeigt Treppe auch bei 3D-Layer aus
- Dimensionen-Panel skipt on_select/idle waehrend UT_ACTIVE oder
  Partnership-Cascade → keine Flicker beim Drag mehr
2026-05-28 00:41:05 +02:00
karim d9589e99f5 Cleanup: gitignore Rhino-Testdateien, Layout-Skip-Versuch zurueck (WebView-Bug) 2026-05-27 20:56:22 +02:00
karim f8d1cfe3fe Splash: borderless+transparent+launcher-dedup, idle-dispatch hide 2026-05-27 20:09:09 +02:00
karim 264327432d Splash + Layout-Skip-Revert
User-Bug: Layout-Skip-Optimierung war zu aggressiv — Mac Rhino haelt die
Panel-Anordnung zwischen Sessions doch nicht im internen State, also
wurden Panels falsch platziert nach Quick-Restart. Skip-Logik raus, der
~3s _-WindowLayout-Apply laeuft wieder jedes Mal. Das ist OK weil der
Splash diese Wartezeit jetzt optisch abdeckt.

Splash verbessert:
- _try_borderless_mac(): direkter NSWindow-Zugriff via Eto.ControlObject
  + ObjC-Methoden (setStyleMask_, setOpaque_, setHasShadow_,
  setBackgroundColor_, setMovableByWindowBackground_) — produziert
  echten borderless Mac-Look wie der Launcher-Splash
- Form-BackgroundColor auf transparent damit das gradient des WebView-
  HTMLs durchscheint (rounded petrol gradient mit weichem Verlauf)
- WebView selber transparenter Hintergrund
- Closeable/Minimizable/Maximizable/Resizable alle False
- [SPLASH] visible log fuer Debug-Sichtbarkeit
2026-05-27 19:36:09 +02:00
karim 6a13ede6b7 Startup-Splash (petrol-gruen) waehrend Plugin-Init
Verdeckt visuell die 3+ Sekunden Wartezeit beim Cold-Start (Panel-
Registration + WindowLayout-Apply). Stilistisch identisch zum
Launcher-Splash: petrol-gruener Verlauf mit "DOSSIER."-Logo, pulsierendem
Dot, animierter Progress-Bar.

Architektur:
- _startup_splash.py: zentrale show() / hide() Helpers
  - Borderless Eto.Forms.Form (420x160), Topmost, kein Taskbar-Eintrag
  - WebView mit Inline-HTML (gleicher Stil wie launcher/public/splash.html)
  - Sticky-Key _dossier_startup_splash haelt die Form-Referenz
  - Safety-Timeout 8s falls hide() vergessen wird

- startup.py _load_all: show() ganz am Anfang (bevor Imports laufen)
- oberleiste._on_ready: hide() via 200ms-Timer NACH window-layout-apply
  (bzw. nach skip) — Layout-Animation ist auf Panels in Finalposition
  kurz sichtbar bevor Splash verschwindet

Effekt: User sieht sofort einen schoenen Branded-Loading-Screen statt
3s grauer Rhino-UI mit halb-geladenen Panels.
2026-05-27 19:31:00 +02:00
karim edaf83229b Startup-Performance: WindowLayout-Apply skippen bei Quick-Restart
Diagnose-Log zeigte: 3046 ms (= 93% der 3.3s Cold-Start-Zeit) gehen
allein in den _-WindowLayout RunScript-Call beim ersten oberleiste-
_on_ready. Rhino dockt dabei alle Panels neu — teuer.

Optimierung: Marker-File-basiertes Skip
- Nach erfolgreichem Apply: ~/Library/Application Support/
  ch.gabrielevarano.Dossier/layout_marker.json mit Timestamp + Name
- Bei _on_ready: wenn Marker < 10 min alt UND derselbe Layout-Name →
  skip (Rhino "remembers" die Panel-Positionen meistens noch im
  internen State, nur unzuverlaessig zwischen LANGEN Pausen)
- Sonst (kalter Tag, langer Reboot, anderer Name): apply normal

Effekt fuer typischen Dev-Workflow (haeufige Quick-Restarts):
- Erster Restart heute: 3s Apply (wie bisher) + Marker
- Naechste Restarts < 10 min spaeter: ~0ms (skip)
- Nach Pause > 10 min: erneuter Apply

Helpers _is_layout_recently_applied + _mark_layout_applied. Marker
wird auch geschrieben wenn User manuell apply (z.B. via Settings-
Dialog oder Launcher pendingApplyLayout) → konsistent.

Falls Layout doch falsch ist nach Skip: Oberleiste-Settings →
"Jetzt anwenden"-Button erzwingt Apply.
2026-05-27 19:24:02 +02:00
karim 6fee7bd143 Startup-Diagnose: Imports + Display-Modes + Window-Layout messen
Bisher: Wall-time 3.2s vs gemessene Arbeit 0.9s → 2.3s unmeasured.
Jetzt werden auch die bisher blinden Stellen getrackt:

- 9× Modul-Import (rhinopanel, oberleiste, ausschnitte, ...) als
  phase="import", label=mod_id
- oberleiste display-mode-Ensure-Loop (4× DisplayModeDescription-Calls)
  als phase="display_modes"
- _apply_window_layout RunScript-Sequenz als phase="window_layout"
- text_editor doppelklick-Hook + view-modes assign + unit-check als
  post_init/hook Phases

Nach Reload sollte das Summary zeigen wo die 2.3s versickern → daraus
gezielte Optimierungen.
2026-05-27 19:08:02 +02:00
karim 61923e1b2b SIA-Bilanz CSV-Export aus Elemente-Uebersicht
Download-Button (file_download Icon) neben den Expand/Collapse-Buttons
in der Toolbar. Klick → SaveFileDialog (Default 'sia_bilanz.csv') →
schreibt Excel-kompatible CSV.

Backend (elemente_uebersicht._export_bilanz):
- Wide-Format: Spalten = Kategorie + ein Geschoss + Total
- Zeilen: HNF, NNF, NF, VF, FF, NGF, GF, AGF, Räume (count), Personen
- Werte via _elm.compute_sia_bilanz fuer jeden Scope (gleiche Logik
  wie Bilanz-Stempel + Uebersicht — single source of truth)
- Format: Semikolon-Separator + UTF-8 BOM + Komma als Dezimaltrenner
  (Excel CH/DE-kompatibel, oeffnet ohne Umweg)
- Flaechen mit 2 Nachkommastellen, Raume/Personen als int
2026-05-27 01:13:08 +02:00
karim e2d66a5d64 Personen-Belegung pro Raum + Stempel-Aggregation
Architekten-Workflow: bei Schulen/Buero/Versammlungsstaetten muss die
Personenzahl pro Raum erfasst werden (SIA: m²/Person als Indikator).

Backend:
- UserString dossier_raum_personen (int)
- _attach_meta + _read_meta + state-emit
- _update_wall raum-Branch akzeptiert "personen" im Patch
- compute_sia_bilanz aggregiert personen-Summe ueber Scope
- Stempel: neue Show-Flag stempel_show_personen (default false) +
  Bilanz-Renderer-Zeile "N Personen"
- Stempel-Stil-Field showPersonen mit dabei

Frontend (RaumProperties):
- Personen-Input (number, min 0) zwischen Funktion + Layout-Builder
- Nur sichtbar bei normalen Raeumen (nicht GF/AGF)
- Live-Anzeige "m²/Person" Suffix wenn area + personen > 0
  → User sieht sofort ob die Belegung sinnvoll ist (SIA-Vergleich)

Frontend (StempelProperties):
- Neuer Show-Toggle "Personen" (default off)
- Live-Vorschau zeigt Personen-Summe wenn aktiv + > 0
- _OFF_BY_DEFAULT Set generalisiert Default-Handling (showCount, showPersonen)
2026-05-27 01:08:58 +02:00
karim 1c3b0f3919 Ebenen: Plangrafik 60 → 80, RAEUME/GF/AGF auf 60/61/62
Doppelbelegung Code 60: alte Konvention "Plangrafik" steht im Weg fuer
die neuen Raum-Layer (RAEUME 60, GF 61, AGF 62). Wenn ein Doc beides
hat, fiel die Auto-Add-Logik in _find_ebene_sublayer_name aus → Raeume
landeten auf Rhino-Layer "60_RAEUME" der aber im Dossier-Ebenen-Panel
nie auftauchte (dort stand 60 = Plangrafik).

Fix in zwei Teilen:

1) Frontend-Default-Schema (App.jsx INITIAL_EBENEN):
   - 60: RAEUME (neu, fuer HNF/NNF/VF/FF)
   - 61: GF (Geschossflaeche)
   - 62: AGF (Aussengeschossflaeche)
   - 80: Plangrafik (verschoben von 60)

2) One-shot Migration in elemente._migrate_plangrafik_60_to_80_once():
   - Detect: dossier_ebenen hat code=60 + name=Plangrafik
   - Action: code 60 → 80, RAEUME/GF/AGF auf 60/61/62 hinzufuegen
   - Rhino-Layer rename: alle "60_Plangrafik" Layer → "80_Plangrafik"
   - build_layers + broadcast_state → Ebenen-Manager UI aktualisiert
   - Sticky-Flag verhindert Re-Run

Plus kleinerer UX-Fix: Skala-Dropdown-Labels gekuerzt
("fix (m)" / "massstaeblich (mm)" statt langer Beschreibungen).
2026-05-27 01:00:33 +02:00
karim 2a838aee93 Stempelstile-Tab in ProjectSettings (Manager analog Raumstile)
Beide Stempel-Familien (Raum-Stempel + Bilanz-Stempel) sind jetzt
konsistent verwaltbar in den Project-Settings.

Backend:
- elemente.py: DUPLICATE_STEMPEL_STIL + REORDER_STEMPEL_STILE handlers
- rhinopanel.py ProjectSettings-Bridge:
  - stempel_stile in params (initial)
  - 4 neue dispatch-Handler (SAVE/DELETE/DUPLICATE/REORDER_STEMPEL_STIL)
  - direkter Dispatch zu elemente.load/save_stempel_stile (kein Roundtrip)
  - STEMPEL_STILE_UPDATED-Message nach jeder Op

Frontend:
- rhinoBridge.js: reorderStempelStile + duplicateStempelStil exports
- ProjectSettingsDialog:
  - Neuer Tab "Stempelstile" parallel zu "Raumstile"
  - Drag-Reorder via HTML5 native, Inline-Rename, Duplicate, Delete
  - Mini-Preview: Header + Anzahl aktiver Show-Flags
  - Name wird mit Font/Bold/Italic des Stils gerendert als Vorschau
2026-05-27 00:51:57 +02:00
karim f457db93e7 Raum-Properties: SIA-Dropdown-Labels kompakter (GF Geschossflaeche statt GF — Geschossflaeche (gross …)) 2026-05-27 00:46:21 +02:00
karim 975071c995 Stempel: Stile + Layout-Customisation (Header, Show-Toggles)
Bilanz-Stempel hat jetzt:
- Custom Header (Default "Nutzflächen", frei editierbar)
- Per-Stempel Show-Toggles: scope, hnf, nnf, nf, vf, ff, ngf, gf, agf,
  count (Anzahl Raeume), separators (Trennlinien)
- Stempel-Stile (Presets) per Doc — separate Storage von Raumstempel-
  Stilen (dossier_stempel_stile)

Backend (elemente.py):
- UserStrings: dossier_stempel_header + dossier_stempel_show_* (11 Flags)
  + dossier_stempel_stil_id
- _make_stempel_text erweitert: header_text, show_scope, show_separators,
  show_count, visibility-dict
- _format_bilanz_lines: visibility + show_separators + show_count Args
- load_stempel_stile / save_stempel_stile (eigener doc.Strings-Key)
- Bridge-Handler SAVE/DELETE/APPLY_STEMPEL_STIL (analog Raum-Stil-Pattern,
  inkl. Bulk-Apply via Selection + applyToIds-Mechanism wie SaveRaum-Stil)
- _sync_stempel_to_source spiegelt Font/Bold/Italic/TextHeight vom Live-
  TextEntity zur UserString-Storage (Oberleiste-Edits ueberleben Regen)
- _update_wall stempel-Branch um header + show-Flags erweitert

Frontend (StempelProperties):
- Stil-Picker-Dropdown analog Raum (Stil wählen / + speichern / 🗑 löschen)
- Header-Input (Inline-Text)
- 11 Show-Toggles als kompakte Grid (Check-Box-Stil)
- Live-Vorschau respektiert Visibility-Flags + bilanz.count
2026-05-27 00:36:31 +02:00
karim ac7b2f2ee5 GF/AGF-Outlines: eigene Layer + minimaler Stempel/UI
Geschossflaeche (GF) und Aussengeschossflaeche (AGF) sind reine
Flaechen-Ausweisungs-Outlines — keine Raeume mit Name/Nummer/Funktion.

Backend:
- Neue Layer-Helpers _layer_path_raum_gf (61_GF) und _layer_path_raum_agf
  (62_AGF) — eigene Sublayer pro Geschoss, eigene Default-Farben
- _layer_path_for_raum_sia(doc, gname, sia): routet sia=gf → GF-Layer,
  sia=agf → AGF-Layer, sonst RAEUME-Layer (HNF/NNF/VF/FF/leer)
- _regenerate_element_body raum_outline-Branch nutzt das Routing →
  Source-Outline migriert automatisch auf den richtigen Layer
- _update_wall raum-Branch: bei SIA-Wechsel (z.B. HNF → GF) auch
  Layer-Migration
- _make_raum_stamp_text: bei sia=gf/agf default-layout
  override auf [["sia", "area"]] → Stempel zeigt nur "GF 234.5 m²" /
  "AGF 18.0 m²" ohne Nummer/Name/Funktion

Frontend (RaumProperties):
- Conditional isFlaeche = sia in (gf, agf)
- Versteckt bei isFlaeche: Stil-Picker, Nummer-Input, Name-Input,
  Funktion-Input, StempelLayoutBuilder
- Bleibt sichtbar: Geschoss, SIA-Tag-Selector, Fuellung, Rundung,
  Skala-Modus, Flaeche/Umfang-Footer
- Info-Zeile zeigt bei isFlaeche: "Flaechen-Outline (GF) auf eigenem
  Layer · Stempel zeigt nur GF + Flaeche"
2026-05-27 00:24:57 +02:00
karim 2386366566 Stempel-Element: SIA-Bilanz als platzierbares Viewport-Objekt
Neuer Element-Type "stempel" — TextEntity die automatisch eine SIA-416
Bilanz aggregiert und im Plan platziert wird. Re-rendert sich live wenn
sich Raeume im Scope aendern.

Backend (elemente.py):
- Neue SIA-Tags: GF (Geschossflaeche), AGF (Aussengeschossflaeche)
  mit eigenen Labels + Pastell-Farben in _SIA_COLORS_HEX
- "stempel" als SOURCE_TYPE; eigene UserStrings:
  - stempel_scope: "total" | "geschoss:<gid>"
  - stempel_txt_h, stempel_font, stempel_bold, stempel_italic
- compute_sia_bilanz(doc, scope): aggregiert nach SIA-Tags, liefert
  HNF/NNF/VF/FF/GF/AGF + abgeleitet NF/NGF/count + Scope-Label
- _format_bilanz_lines: kompakte Stempel-Textzeilen ("HNF  120.5 m²"),
  Trennlinien + nur Kategorien > 0
- _make_stempel_text: TextEntity-Builder mit Header "Nutzflächen · {Scope}"
- _regenerate_element_body "stempel"-Branch: in-place Replace mit
  aktualisiertem Text (Position bleibt aus alter Geometrie)
- _regenerate_stempel_for_geschoss: regennt alle Stempel im selben
  Geschoss + alle "total"-Stempel
- Auto-Cascade: raum_outline-Regen setzt sticky-marker; nach REGEN_BUSY-
  Release wird _regenerate_stempel_for_geschoss aufgerufen
- _cmd_create_stempel: GetPoint im Viewport, layer = aktives Geschoss
  Raum-Sublayer, default-Scope = aktives Geschoss
- _update_wall "stempel"-Branch: scope/txtH/font/bold/italic via patch
- elemente_uebersicht: SIA-Bilanz um gf/agf/ngf erweitert; "stempel"
  als KIND-Eintrag

Frontend:
- createStempel-Bridge-Export
- "Stempel"-Pill-Button in der "Raeume"-PillGroup
- StempelProperties-Component: Scope-Dropdown (Total + alle Geschosse),
  Bilanz-Vorschau mit Hervorhebung der NF (Accent-Farbe)
- KIND_META + RAUM_SIA_KINDS um GF/AGF erweitert

Workflow: Pill "Stempel" klicken → Punkt im Viewport → Stempel erscheint
mit Header "Nutzflächen · EG" + Bilanz. Properties: Scope auf Total
umstellen oder anderes Geschoss waehlen. Neue Raeume taggen mit SIA-
Tag → Stempel aktualisiert sich automatisch.
2026-05-27 00:10:02 +02:00
karim 7fbda8c289 Stempel-Stil-Bulk-Apply + Manager-Tab in ProjectSettings
PHASE 1 — Bulk-Apply auf mehrere selektierte Raeume:
- Backend _cmd_apply_raum_stil: leere ids im Patch → nimmt automatisch
  alle aktuell SELEKTIERTEN raum_outlines aus dem Doc (Bulk-Fallback)
- Frontend sendet leere ids fuer die Default-Bulk-Variante. User
  selektiert N Raeume im Viewport → klickt Stil im Properties-Picker →
  Stil wird auf alle angewendet. Inkludiert immer den Properties-Raum.

PHASE 2 — Stil-Manager als Settings-Tab:
- Neuer Tab "Raumstile" in ProjectSettings-Dialog
- Liste aller Stile mit Drag-Reorder (HTML5 native), Inline-Rename,
  Duplicate (content_copy-Icon), Delete (mit Confirm)
- Mini-Preview: zeigt Layout-Struktur (z.B. "nummer·name / funktion / area")
  und der Name wird mit Font/Bold/Italic des Stils gerendert als Vorschau
- Backend _cmd_reorder_raum_stile + _cmd_duplicate_raum_stil in elemente.py
- ProjectSettings-Bridge dispatcht direkt zu elemente.load/save_raum_stempel_stile
  (kein Roundtrip via Elemente-Bridge noetig)
- STILE_UPDATED-Message nach jeder Op pusht die neue Liste zum Dialog
- params bei _open_project_settings enthalten jetzt raumStempelStile + fonts

Bridge-Exports: reorderRaumStile / duplicateRaumStil in rhinoBridge.js
2026-05-26 23:47:38 +02:00
karim f208e7fc00 Raumstempel-Panel: Hoehe + Ausrichtung weg, kommt aus Oberleiste
UX-Konsolidierung: Texthoehe und Ausrichtung wurden bisher doppelt
gesetzt (Panel + Oberleiste-Text-Block). Jetzt nur noch via Oberleiste
wenn der Stempel selektiert ist — Panel zeigt nur noch den Skala-Modus
(fix/masstab) als kompakten Dropdown.

Backend: _sync_raum_stamps_to_source spiegelt jetzt auch
TextHorizontalAlignment vom Stempel zur Source (Left/Center/Right →
links/mid/rechts). Damit kommt die User-Wahl aus der Oberleiste sauber
in die UserStrings — und wird beim naechsten Regen + bei Stil-Speichern
korrekt uebernommen.

Frontend RaumProperties:
- Ausrichtung-Zeile (3 BarToggles) raus
- Hoehe-Zeile reduziert auf Skala-Modus-Dropdown:
  "fix · Hoehe = Oberleiste-Wert (m)" | "masstab · skaliert mit Plan-Massstab"
- Local-State txtH + setTxtH entfernt (unused)
- Stempel-Stil-Speichern liest weiter raum.txtH + raum.align (kommen
  jetzt vom Sync) → captured korrekt
2026-05-26 23:33:38 +02:00
karim 662ce87e98 Elemente: 'Karte'-Button raus, Map-Icon auf Swisstopo uebertragen 2026-05-26 23:30:25 +02:00
karim 46970fd4f0 Raumstempel: Hoehe-Zeile kompakter — Modus als Dropdown statt Toggle-Pair 2026-05-26 23:28:49 +02:00
karim eff0878f53 Raumstempel-Stile: Active-Stil-Tracking
UserString dossier_raum_stil_id auf der raum_outline. Wird gesetzt:
- bei APPLY_RAUM_STIL → schreibt sid auf alle Ziel-Raeume
- bei SAVE_RAUM_STIL mit applyToIds=[…] → schreibt neue sid auf
  uebergebene Raeume (Frontend nutzt das beim "+ Speichern…"-Flow um
  den aktuellen Raum mit dem neuen Stil zu verknuepfen)

_read_meta liest stil_id mit → State-Emit feldet "stilId" zum Raum.
Frontend's Stil-Dropdown markiert dadurch automatisch den aktiven Stil
(via activeStilId = raum.stilId).

Manuelle Edits (UPDATE_ELEMENT) touchen stil_id NICHT — User kann
seinen Stil leicht tweaken ohne dass die Verknuepfung verloren geht.
Wenn der Tweak weit weg ist, kann er den Stil aktualisieren via
"+ Aktuelle Settings als Stil speichern" (uebernimmt dann mit der
NEUEN stil_id).
2026-05-26 23:23:42 +02:00
karim 3c28d2e29c Elemente-Uebersicht: SIA-416 Bilanz pro Geschoss
Aggregiert alle raum_outline-Flaechen nach raum_sia-Klassifikation
(hnf/nnf/vf/ff) und zeigt sie als kompakte Mini-Tabelle direkt unter
dem Geschoss-Header in der Project-Browser-Uebersicht.

Backend (_build_overview):
- Neuer Returnschluessel siaBilanz: {geschossId: {hnf, nnf, vf, ff,
  ohne, nf, total, count}} in m^2
- NF = HNF + NNF (Nutzflaeche nach SIA 416)
- Raeume ohne SIA-Tag landen in "ohne"

Frontend (ElementeUebersichtApp):
- Direkt unter dem Geschoss-Header eine Inline-Tabelle mit nur den
  Klassen die > 0 sind (kein Spam wenn nichts klassifiziert ist)
- NF separat hervorgehoben (Accent-Farbe) als wichtigste Kennzahl
- Read-only, aktualisiert sich mit jedem state-emit (Raum-Aenderung,
  SIA-Tag setzen, neue Raeume) automatisch
2026-05-26 23:20:00 +02:00
karim f1860ae85d Raumstempel-Stile (Presets) — speichern + anwenden per Doc
Damit User wiederkehrende Stempel-Configs (Wettbewerb / Bauantrag /
Mobiliar etc.) nicht jedes Mal neu klicken muss.

Backend (elemente.py):
- Storage in doc.Strings dossier_raum_stempel_stile als JSON-Array
- load_raum_stempel_stile / save_raum_stempel_stile Helpers
- Bridge-Handler:
  - SAVE_RAUM_STIL: upsert by id (neu wenn leer)
  - DELETE_RAUM_STIL: remove by id
  - APPLY_RAUM_STIL: schreibt Stil-Felder auf Raum-IDs + Regen
- _RAUM_STIL_FIELDS umfasst font/bold/italic/txtH/txtModus/align/
  rundung/fuellung/showSia/layout (alles was die Optik bestimmt)
- raumStempelStile im STATE-Emit zum Frontend

Frontend:
- saveRaumStil/deleteRaumStil/applyRaumStil in rhinoBridge.js
- RaumProperties: neue "Stil"-Sektion oben mit Dropdown
  * gespeicherte Stile + "+ Aktuelle Settings als Stil speichern" +
    "🗑 Aktiven Stil loeschen"
  * Klick auf Stil → applyRaumStil mit aktueller Raum-ID
- Beide PropertiesView-Aufrufe (inline + satellite) bekommen die Liste
2026-05-26 23:17:08 +02:00
karim da0fd365f2 Arbeitseinheit als Project-Setting + Doc-Open-Check
Statt jeden Wert im Code zu konvertieren wird sichergestellt dass das
Doc in der gewuenschten Unit ist:

- defaults.unit ('meters'|'millimeters'|'centimeters') in
  dossier_project_settings, Default 'meters'
- ProjectSettings-Dialog "Voreinstellungen" Tab: neue Sektion
  "Arbeitseinheit" mit Toggle-Group fuer m/cm/mm
- get_project_unit() + get_project_unit_enum() Helper in rhinopanel
- startup._check_doc_unit() prueft beim Doc-Open ob ModelUnitSystem
  matched — bei Mismatch Eto-MessageBox "Doc auf X umstellen?"
- "Yes" ruft _-Units _Model _<Unit> _Yes (Geometrie wird mit-skaliert)
- "No" setzt doc.Strings-Flag dossier_unit_checked → keine erneute Frage
- Check laeuft beim _on_doc_opened-Hook + initial fuer aktives Doc

Vorgehen ist deutlich sauberer als der vor-revert unit-aware Code
(135 Zeilen Konvertierungslogik vs 80 Zeilen Check+Convert).
2026-05-26 23:11:36 +02:00
karim cd626b0707 Revert "Unit-aware: m → Doc-Units im Regen-Pfad"
This reverts commit dd5ccec881.
2026-05-26 22:06:27 +02:00
karim dd5ccec881 Unit-aware: m → Doc-Units im Regen-Pfad
DOSSIER-UI nimmt immer Meter entgegen — bisher wurden die Werte 1:1 als
Doc-Units verwendet, was bei Doc=Millimeter winzige Geometrie ergab
(0.25m getippt → 0.25mm Wand).

Storage-Konvention bleibt METER (UI-friendly). Konvertierung passiert
beim Geometrie-Bau:

- Neue Helpers _m2u / _u2m via Rhino.RhinoMath.UnitScale
- _regenerate_element_body normalisiert ALLE m-typischen Meta-Felder am
  Eingang via _m2u — Geometrie-Code darunter bleibt unveraendert und
  arbeitet in Doc-Units (funktioniert in jedem Unit-System)
- Lokaler _gs() Wrapper konvertiert Geschoss-OKFF + Hoehe → Doc-Units
- _resolve_uk_ok / _resolve_decke_z / _resolve_dach_base konvertieren
  Geschoss-Heights aus JSON (m) → Doc-Units
- _resolve_raum_text_height_m masstab-Pfad: m-Result → Doc-Units
- _sync_raum_stamps_to_source: Stempel-TextHeight + Position-Delta sind
  in Doc-Units, werden vor Storage via _u2m → m konvertiert

Effekt:
- Doc in Metern: kein sichtbarer Unterschied (UnitScale=1, no-op)
- Doc in Millimeter: 0.25 (m) wird zu 250 (mm) Wand → richtig dick
- State-Emit zum Frontend bleibt in m → UI konsistent
2026-05-26 21:59:33 +02:00
karim 95678d4394 Massstab: Diagnose-Prints raus (Magnify funktioniert wie erwartet) 2026-05-26 21:38:16 +02:00
karim 067cb56584 Massstab: Diagnose-Prints fuer _apply_scale (pre/post Magnify) 2026-05-26 21:24:21 +02:00
karim b10760a704 Masstab: Persist vor Raum-Regen — sonst regent mit alter Skala
regen_masstab_raeume liest get_applied_scale_ratio() um die paper-mm-
Hoehe in Modell-m umzurechnen. Lief bisher VOR _set_applied_scale_for_vp
→ las immer noch den alten Wert. Resultat: nach Skala-Wechsel war die
Stempel-Hoehe um die alte Skala berechnet, naechster Wechsel um die
vorletzte usw.

Reihenfolge korrigiert: erst write_user_scale + set_applied_scale_for_vp,
DANN regen_masstab_raeume.
2026-05-26 21:10:28 +02:00
karim e56ee2cb8f Raumstempel: Auto-Konvertierung beim Modus-Wechsel + Font-API-Fix
Bug: Beim Toggle fix↔masstab blieb txt_h derselbe Zahlenwert mit anderer
Einheit. 0.20m wurde zu 0.20mm@1:100 = 0.02m Text (2cm). Stempel
unsichtbar klein.

Fix: Bridge konvertiert txt_h automatisch wenn der Modus wechselt UND
kein expliziter txtH-Wert im Patch ist:
  fix → masstab: paper_mm = m * 1000 / scale
  masstab → fix: m       = paper_mm * scale / 1000
Frontend sendet beim Modus-Toggle nur { txtModus } ohne txtH → Backend-
Konvertierung greift.

Plus: _list_system_fonts probiert jetzt InstalledFonts() + AvailableFontFaces
(InstalledFontsAsString existiert auf Mac Rhino 8 nicht); Ergebnis wird
session-weit gecacht damit der Error nicht jeden state-emit spamt.
2026-05-26 20:58:28 +02:00
karim 238d7d062b Raumstempel: masstab-Modus — Texthoehe als Paper-mm @ Plan-Massstab
Neuer UserString dossier_raum_txt_modus = "fix" | "masstab" (default fix).
- fix:     raum_txt_h ist Meter (Modellhoehe, bisheriges Verhalten)
- masstab: raum_txt_h ist Paper-mm. Render-Hoehe (m) =
           paper_mm * applied_scale / 1000 — wird zur Render-Zeit aus
           massstab.get_applied_scale_ratio() gelesen, Fallback 1:100.

Massstab-Sync:
- massstab._apply_scale ruft nach Skala-Wechsel elemente.regen_masstab_raeume(doc)
  → alle Raeume im masstab-Modus regennen automatisch, Texthoehe folgt der
  neuen Skala (z.B. Switch 1:100 → 1:50 halbiert die Modellhoehe).

_sync_raum_stamps_to_source masstab-aware: im masstab-Modus wird die
TextHeight am Stempel NICHT zurueck auf raum_txt_h gespiegelt (sie ist
abgeleitet, nicht die Wahrheit) — sonst waere der naechste Regen sofort
falsch positioniert. Offset + Font/Style werden weiterhin gespiegelt.

UI: Modus-Toggle "fix m" / "masstab mm" + Hoehen-Input + Einheits-Suffix
in RaumProperties zwischen Ausrichtung und Funktion.
2026-05-26 20:49:26 +02:00
karim 01b6501a0c Raumstempel: Drag-Drop-Layout + persistente Position + Oberleiste-Sync
Datenmodell auf der raum_outline:
- dossier_raum_stamp_dx/dy: Stempel-Offset zum Outline-Centroid
- dossier_raum_layout: JSON-Array of-Rows fuer Multi-Field-pro-Zeile
- dossier_raum_txt_font/bold/italic + raum_show_*: Typografie-Overrides
- Legacy show_*-Flags bleiben Fallback wenn kein Layout gesetzt

Backend:
- _make_raum_stamp_text: Layout-Renderer (Rows zu Lines), Offset wird in
  _regenerate_element_body auf den Centroid addiert
- _sync_raum_stamps_to_source: laeuft am Anfang von _on_command_end,
  spiegelt aktuelle Stempel-Position + Font/Size/Style auf die Source
  zurueck → User-Edits via Move/Oberleiste/Properties ueberleben Regen
- _list_system_fonts: System-Fonts fuer Frontend-Dropdown
- Raumstempel-Bug-Fix: raum_outline jetzt in _on_command_end Regen-Pfad
  per Length-Check getriggert, snapshot um length erweitert. Vertex-Drag
  aktualisiert Flaechen-Wert. Outline-Sources fuegen affected_walls hinzu.
- raum_stamp aus _PAIRED_VOLUME_TYPES entfernt → einzeln greifbar/
  verschiebbar; Klick auf Outline pairt weiter alles drei.

Frontend (ElementeApp.jsx):
- StempelLayoutBuilder: HTML5 drag-and-drop UI, verfuegbare Felder als
  Pills oben (Klick = neue Row, Drag = in bestehende Row), bestehende
  Rows als drop-targets, × zum Entfernen
- Typografie-Block raus aus RaumProperties; Hinweis-Text auf Oberleiste
- PropertiesView nimmt jetzt fonts={state.fonts} (auch Satellite-Window)
2026-05-26 20:42:32 +02:00
karim 02a00a9b4a Display-Modes: 3D-Template + Auto-Assign + Material/Raytracing-Slots
- Neues Template rhino/templates/dossier_3d.ini fuer perspektivische Views
- Registry-Loop in oberleiste.py generalisiert (Plan + 3D + Material +
  Raytracing) — Material/Raytracing skippen wenn kein Template vorhanden,
  um Cycles-Pipeline-Clone-Crash zu vermeiden
- Guid-Replace praezisiert: nur Section-Header-Guid, nie PipelineId
- Plan-spezifische ini-Patches auf target_name=="Dossier Plan" gegated
- Auto-Assign in startup.py: Parallel-Viewports -> Plan, Perspective -> 3D,
  einmal-pro-Doc via doc.Strings-Flag (User-Overrides bleiben)
- schnitte.activate_schnitt setzt Dossier Plan explizit (Hatches auch in
  Schnittperspektive sichtbar)
- GestaltungApp Section-Block neu strukturiert: PenBlock fuer 3D als 'Fill'
  innerhalb der Section, Solid-Fill-Toggle entfernt (war Duplikat)
2026-05-26 18:26:49 +02:00
karim 13a5e1eb7a 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)
2026-05-26 17:09:18 +02:00
karim e1b63aa4e6 Library-Thumbnails: Auto-Capture + Base64-Preview in der UI
Beim Hinzufuegen oder Importieren eines Library-Items wird automatisch
ein PNG-Thumbnail vom Item generiert (Top-View, 128x128) und in
library/previews/<id>.png abgelegt. Frontend rendert die Previews als
Base64-Data-URIs (sicher gegen WebKit-file://-Restriktionen).

library.py:
- _previews_dir(): legt previews/-Folder an
- _preview_rel_for(asset_rel): predictable PNG-Pfad pro Item
- _capture_thumbnail_of_objects(): hided andere Objekte temporaer,
  switcht auf Top-Parallel, ZoomBoundingBox, CaptureToBitmap → PNG,
  restored Viewport + Hidden-State
- read_preview_data_uri(): liest PNG + encoded als data:image/png;base64
- Hook in convert_to_3dm_via_import (vor Cleanup) + save_selection_to_asset

rhinopanel.py:
- _enrich_library_items_with_previews(): haengt previewDataUri an
  jedes Item das ein preview-Feld hat
- Initial-Params + _send_library + ElementeBridge._cmd_list_library
  liefern angereicherte Items
- _add_library_file + _save_selection_as_library setzen preview-Pfad
  im Item wenn Thumbnail-Datei existiert

Frontend:
- SymbolPicker.ItemPreview: rendert <div backgroundImage> mit Base64-URI
  wenn vorhanden, sonst Icon-Fallback
- ProjectSettingsDialog Symbole-Tab: List-Row + Detail-Identity zeigen
  Thumbnail (32px in Liste, 56px im Detail), Icon nur als Fallback

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:07:33 +02:00
karim 827bd8d4d7 Library: Format-Konvertierung beim Import (.dwg/.obj/.fbx/.dae/.stl/...)
User kann jetzt im Symbole-Tab Dateien in vielen Formaten waehlen — Rhino
konvertiert automatisch nach .3dm via _-Import + File3dm.Write.

library.py — convert_to_3dm_via_import():
- Snapshot der existierenden Object-IDs im aktiven Doc
- User-Selection sichern
- _-Import scripted ausfuehren → neue Objekte in Doc
- Diff -> neue Objekte sammeln
- BoundingBox.Min als Origin → in File3dm packen
- f3.Write nach library/assets/<name>.3dm
- Neue Objekte aus Doc wieder loeschen + User-Selection restoren
- sticky 'dossier_library_import_busy' + 'dossier_swisstopo_busy' damit
  unsere Listener nichts cascaden waehrend Doc kurzzeitig die Importe haelt

rhinopanel.py — _add_library_file():
- .3dm: copy_to_assets (wie bisher)
- .dwg/.dxf/.obj/.fbx/.dae/.stl/.3ds/.skp/.iges/.step/.ply: konvertieren
- Sonst LIBRARY_ERROR

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:45:58 +02:00
karim 68b9d14453 Auswahl als Library-Item speichern (Step 3)
User selektiert in Rhino → klickt im Symbole-Tab 'Aus Auswahl' →
Selection wird in eine .3dm-Datei verpackt + Library-Item entsteht
(oder bestehendes wird aktualisiert).

Backend (library.py):
- save_selection_to_asset(doc, target_name): erstellt File3dm aus der
  aktuellen Selection, packt Geometry relativ zu BoundingBox.Min (Block-
  Origin am Ursprung), schreibt nach library/assets/

Backend (ProjectSettingsBridge):
- SAVE_SELECTION_AS_LIBRARY-Handler: holt Selection, fragt bei neuen
  Items via Rhino-GetString nach Name, schreibt .3dm, fuegt Item zum
  Manifest oder updated bestehendes (variant '2d'/'3d')

Frontend (Symbole-Tab):
- List-Footer: 'Aus Datei' + 'Aus Auswahl' Pills (neues Item)
- Pro 2D/3D-Slot im Detail: 'Datei wählen' + 'Aus Auswahl' Pills
  (Variante eines bestehenden Items befüllen)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 04:05:48 +02:00
karim c993935b17 Symbole-Tab in Project-Settings (Library-Item-Management)
Project-Settings hat jetzt 5 Tabs. Neuer 'Symbole'-Tab managt die
Dossier-Library: List/Detail wie Materialien, mit 2D + 3D Slot pro Item.

Backend (library.py):
- save_manifest, update_item, delete_item, add_item — full CRUD aufs
  library.json
- copy_to_assets: kopiert User-Dateien in library/assets/ mit
  Konflikt-Resolution (auto-suffix)

Backend (rhinopanel.py / ProjectSettingsBridge):
- _send_library: aktuelle Items + libraryRoot an Frontend
- _add_library_file: File-Picker (.3dm direkt; .dwg/.obj/etc. zeigt
  Hinweis fuer kuenftige Konvertierung), kopiert + appended ans Item
  (variant 2d/3d) oder erstellt neues Item
- _update_library_item: patch by id
- _delete_library_item: entfernt Eintrag aus Manifest
- LIBRARY_ITEMS + LIBRARY_ERROR Messages ans Frontend

Frontend:
- Neuer 'Symbole'-Tab mit List/Detail
- Liste: Name, Type-Icon, '2D'/'3D' Status-Badge
- Detail rechts: Name-Edit (live persist on blur), Type-Toggle
  (Symbol/Objekt), 2D/3D-File-Slots mit Datei-Picker, Tags-Editor
- 'Neues Objekt' Button im Listen-Footer

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:58:41 +02:00
karim de57c320c2 Symbol-Picker als Satellite-Fenster (statt Modal im Elemente-Panel)
UX-Verbesserung: Modal-Overlay im engen Elemente-Panel war unpraktisch.
Symbol-Picker oeffnet sich jetzt als eigenstaendiges Eto.Form-Fenster
(wie Library/Project-Settings).

Frontend:
- SymbolPicker bekommt embedded-Prop (Satellite-Mount vs Modal-Overlay)
- Neuer SymbolPickerApp Satellite-Wrapper (PANEL_PARAMS lesen + Bridge)
- main.jsx: 'symbol_picker' Mode-Routing
- ElementeApp: Symbol-Button ruft nur noch listLibrary() — Backend
  oeffnet das Fenster

Backend:
- _cmd_list_library oeffnet jetzt das Satellite-Window mit eigener
  Bridge (PICK -> CREATE_SYMBOL, CANCEL -> Close)
- PICK schliesst Fenster + triggert interactive GetPoint im Viewport

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:39:03 +02:00
karim 8184f559fc Symbol-Funktion in Elemente-Panel (Phase S1+S2)
Schema (library.py):
- Item-Format erweitert: files2d + files3d (Backwards-compat zu 'files')
- _build_variant_block + _place_instance + Layer-Routing pro Variante
- import_item akzeptiert at_point + layer2d/layer3d
- _ensure_block_definition mit variant-Suffix (dossier_lib_<id>_2d/_3d)

Backend (elemente.py):
- _layer_path_symbole(geschoss_name, variant) → <geschoss>::40_SYMBOLE::
  SYMBOLE_2D bzw. SYMBOLE_3D
- Default-Ebene 40 SYMBOLE via _find_ebene_sublayer_name
- LIST_LIBRARY-Handler: sendet Library-Manifest als LIBRARY_LIST
- CREATE_SYMBOL-Handler: interactive GetPoint im aktiven Viewport,
  laedt Block-Def + platziert Instanz(en) auf den richtigen Ebenen
- Pair-Items (2D+3D) werden an gleichem Punkt beidseitig platziert →
  Top zeigt 2D-Layer, Persp zeigt 3D-Layer wenn User entsprechend
  Sichtbarkeit setzt

Frontend:
- SymbolPicker Modal-Component: Grid mit Symbol/Object-Cards, Search,
  Type-Filter (Alle/Symbole/Objekte), Doppelklick = Pick
- Symbol-Button in ElementeApp (PillGroup "Library") oeffnet Modal +
  triggert listLibrary() fuer aktuelle Items
- createSymbol(id) → Backend → GetPoint → Place

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:31:54 +02:00
karim 8f691e37c4 Projektdaten in Project-Settings + Swisstopo-Adress-Prefill
Schema-Erweiterung:
- _PROJECT_SETTINGS_DEFAULTS hat jetzt 'project'-Block mit
  name / number / address / bauherr / architekt / notes / projectZeroMum
- _normalize_project_meta stripped Strings + clampt mum als float
- load/save_project_settings handeln das 'project'-feld
- save_project_settings spiegelt projectZeroMum auch in den Legacy-Key
  dossier_project_zero_mum (fuer Geschoss-Settings-Dialog)
- load_project_settings liest Legacy-Key als Fallback wenn neuer Wert
  noch nicht gesetzt

UI:
- InlineTextField + TextareaField Helpers (Pill-Stil)
- Projektdaten-Section in Voreinstellungen-Tab:
  Name, Projekt-Nr., Adresse, Bauherrschaft, Architekt:in,
  EG-Nullpunkt m.ü.M (mit Hinweis auf Swisstopo-Nutzung), Notizen

Swisstopo:
- _cmd_open_swisstopo_dialog laedt Projekt-Adresse + sendet projectAddress
  im SWISSTOPO_STATE
- SwisstopoApp: vorbelegt searchText mit projectAddress wenn Feld leer
  ist (User-Input wird nicht ueberschrieben)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:21:19 +02:00
karim a597b58c93 Linientypen + Schraffuren-Tabs in Project-Settings + Datei-Import
Project-Settings hat jetzt 4 Tabs:
- Voreinstellungen (kompakte InlineNumberField, gruppiert in Sections)
- Materialien (List/Detail, ohne Hatch)
- Linientypen (List/Detail mit SVG-Strich-Vorschau)
- Schraffuren (List/Detail mit echtem HatchLine-Renderer)

Backend (rhinopanel.py):
- _list_linetypes_full liefert Segmente {length, type: Line/Space/Dot}
  (Mac Rhino 8 GetSegment returnt (length, isLine: bool))
- _list_hatch_patterns_full liefert HatchLines mit angle/base/offset/dashes
  (hl.Dashes optional ueber 3 API-Variants)
- CRUD: RENAME / DELETE / LOAD_DEFAULTS
- File-Import: IMPORT_LINETYPE_FILE (.lin), IMPORT_HATCH_FILE (.pat)
  via Eto.OpenFileDialog → Linetypes.Load / HatchPatterns.LoadFromFile

Frontend (ProjectSettingsDialog.jsx):
- LinetypePreview: SVG mit tile-fenster (4 Repetitions), Line als <line>,
  Dot als <circle>, currentColor fuer Renderer-Robustheit
- HatchPreview: rendert pro HatchLine alle parallelen Linien mit Angle,
  Offset (Spacing + Stagger), Dashes als stroke-dasharray
- TABLES_UPDATED Message vom Backend re-rendert Listen
- Import-Pills im List-Footer

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:14:28 +02:00
karim 8d3b3af882 Material/Ebene-Separation: Hatch raus aus Material (Refactor a)
Material ist jetzt rein 3D — Section-Hatch (2D-Schnitt) wird nicht mehr
am Material definiert, sondern am Layer (via Rhino-Layer-Dialog oder
spaeter via Ebenen-Settings + neuer Hatch-Tab im Project-Settings).

Schema-Aenderungen:
- _normalize_material: hatch + scale entfernt
- _MATERIAL_LIBRARY (elemente.py): hatch + scale aus allen Builtin-Mats
- _get_all_materials: ohne hatch
- _send_state materials payload: nur {name, color}
- Library import_material: PBR + Texturen werden weitergegeben

Backend:
- _ensure_material_sublayer: erstellt Sublayer mit Color, RESETTET aber
  alten SectionHatchIndex auf -1 (= "kein eigener Hatch") damit
  Inheritance/User-Override greift. Vorher wurden alte Material-Hatch-
  Werte da haengen geblieben.

Frontend:
- MaterialDetail: Schraffur-Section entfernt
- hatchPatterns-Prop entfernt

Konsequenz: existierende Waende verlieren ihren Section-Hatch beim
naechsten Regen. User definiert Section-Hatches jetzt auf Layer-Ebene.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 02:10:04 +02:00
karim ad56d9e930 Fix Texture-Picker: Custom-Bridge fuer Project-Settings Satellite
open_satellite_window ohne bridge=-Param verwendet eine Inline-Bridge die
nur READY/SAVE/CANCEL kennt — PICK_TEXTURE_FILE-Messages wurden stumm
verworfen.

Fix: _ProjectSettingsBridge als BaseBridge-Subclass mit eigener
PICK_TEXTURE_FILE-Handler-Logik. Sendet TEXTURE_PICKED direkt zurueck an
die selbe WebView.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 18:14:13 +02:00
154 changed files with 22391 additions and 2448 deletions
+6
View File
@@ -26,6 +26,12 @@ __pycache__/
*.pyc
*.pyo
# Rhino-Testdateien (rhino/-Ordner)
rhino/*.3dm
rhino/*.3dm.thumb.png
rhino/*.3dmbak
rhino/dossier.project.json
# Claude Code
.claude/
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+77
View File
@@ -0,0 +1,77 @@
# DOSSIER C# Plugin — Build & Install
Das Plugin (`.rhp`) bootstrappt beim Rhino-Start die Python-Module und
registriert native Commands (`dWall`, `dDoor`, `dSlab`, …).
## Voraussetzungen
```bash
brew install dotnet@7
```
Oder direkt von Microsoft: https://dotnet.microsoft.com/download/dotnet/7.0
RhinoCommon wird beim ersten Build automatisch via NuGet geladen.
## Repo-Pfad setzen (nach Neuinstallation wichtig)
Das Plugin sucht das Repo in dieser Reihenfolge:
1. Env-Var `DOSSIER_HOME`
2. Datei `~/.dossier_home` (eine Zeile: absoluter Pfad zum Repo-Root)
3. Hardcoded Fallback `/Users/karim/STUDIO/DOSSIER`
Einfachste Variante — einmalig nach dem Klonen:
```bash
echo "/Users/karim/PROJECTS/DOSSIER" > ~/.dossier_home
```
Ohne das findet das Plugin `rhino/startup.py` nicht und bootet nicht.
## Build
```bash
cd csharp/DOSSIER
./build.sh # Release → bin/Release/net7.0/DOSSIER.rhp
./build.sh debug # Debug-Build mit Symbols
./build.sh clean # bin/ + obj/ löschen
./build.sh install # Build + yak install in Rhino-User-Plugin-Pfad
```
## Installation in Rhino (einmalig nach Build)
Mac Rhino 8 unterstützt kein Drag & Drop für `.rhp`-Dateien.
1. Rhino 8 öffnen
2. Befehl: `PluginManager`
3. Button **Install…**`csharp/DOSSIER/bin/Release/net7.0/DOSSIER.rhp`
4. Rhino neu starten
Der Pfad bleibt in Rhinos Settings-XML registriert. Bei späteren Builds
einfach wieder in denselben Output-Pfad bauen — Rhino lädt den neuen Stand
automatisch beim nächsten Start.
## Startup-Eintrag (Python-Bootstrap)
Der Launcher trägt den Startup-Eintrag automatisch ein. Für manuelle
Dev-Setups ohne Launcher:
Rhino → Options → General → „Run these commands every time a model is opened":
```
_-RunPythonScript "/Users/karim/PROJECTS/DOSSIER/rhino/startup.py"
```
(Mit Dash, mit Quotes, voller Pfad — siehe CLAUDE.md für Details warum.)
## Nach Neuinstallation Mac — Checkliste
- [ ] Repo klonen: `git clone https://git.kgva.ch/karim/DOSSIER.git`
- [ ] `echo "/Users/karim/PROJECTS/DOSSIER" > ~/.dossier_home`
- [ ] `brew install dotnet@7 node`
- [ ] `cd DOSSIER && npm install && npm run build`
- [ ] `cd csharp/DOSSIER && ./build.sh install`
- [ ] Rhino öffnen → PluginManager → Install → `.rhp` registrieren
- [ ] Rhino neu starten → DOSSIER bootstrappt (Panels + Commands)
- [ ] `~/Library/Application Support/RhinoPanel/override_presets.json` von Backup zurückkopieren
+7
View File
@@ -0,0 +1,7 @@
bin/
obj/
dist/
*.user
*.suo
.vs/
.idea/
+94
View File
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
//
// BIM-Commands: jeweils Wrapper auf das Python-Script in rhino/aliases/cmd/.
// Naming-Convention: d-Prefix + englischer BIM-Begriff (VisualARQ-Stil).
// Klassen-Guids sind frei generiert (uuidgen) — wichtig nur dass sie
// stabil bleiben, damit Rhino sie ueber Sessions wiedererkennt.
using System.Runtime.InteropServices;
namespace DOSSIER.Cmd;
[Guid("9A87B609-719F-468B-AF2A-6E59A9B61062")]
public class DWall : DossierPythonCommand
{
public override string EnglishName => "dWall";
protected override string ScriptRelativePath => "cmd/wand.py";
}
[Guid("80278984-16B8-485B-8876-3D63806BCA58")]
public class DDoor : DossierPythonCommand
{
public override string EnglishName => "dDoor";
protected override string ScriptRelativePath => "cmd/tuer.py";
}
[Guid("20D22047-03FA-4CF3-ACF3-3424A109BD91")]
public class DWindow : DossierPythonCommand
{
public override string EnglishName => "dWindow";
protected override string ScriptRelativePath => "cmd/fenster.py";
}
[Guid("536641ED-93D4-4D49-A028-9F2C4EEE2A24")]
public class DSlab : DossierPythonCommand
{
public override string EnglishName => "dSlab";
protected override string ScriptRelativePath => "cmd/decke.py";
}
[Guid("CF196C6A-EEAE-478C-8EB0-C69B6F7B9942")]
public class DStair : DossierPythonCommand
{
public override string EnglishName => "dStair";
protected override string ScriptRelativePath => "cmd/treppe.py";
}
[Guid("B4A19B8B-4056-428B-BA2E-DF69A2A8DA9A")]
public class DColumn : DossierPythonCommand
{
public override string EnglishName => "dColumn";
protected override string ScriptRelativePath => "cmd/stuetze.py";
}
[Guid("CCDF2D03-1FBD-4BC3-A06E-6D3FEEE575AB")]
public class DBeam : DossierPythonCommand
{
public override string EnglishName => "dBeam";
protected override string ScriptRelativePath => "cmd/traeger.py";
}
[Guid("6ADF4344-0C05-48D1-BB2A-B330E3057CE4")]
public class DRoom : DossierPythonCommand
{
public override string EnglishName => "dRoom";
protected override string ScriptRelativePath => "cmd/raum.py";
}
[Guid("A9D471FD-CB75-4C4E-8236-3C8B9A491266")]
public class DSymbol : DossierPythonCommand
{
public override string EnglishName => "dSymbol";
protected override string ScriptRelativePath => "cmd/symbol.py";
}
[Guid("5388E3A7-B40E-40CE-B958-4A294B1E9F4F")]
public class DTag : DossierPythonCommand
{
public override string EnglishName => "dTag";
protected override string ScriptRelativePath => "cmd/stempel.py";
}
[Guid("F0A5E3B0-F77E-4316-B521-294979F1E9CA")]
public class DRoof : DossierPythonCommand
{
public override string EnglishName => "dRoof";
protected override string ScriptRelativePath => "cmd/dach.py";
}
[Guid("404E4389-F8BF-4BAE-A972-60EADB33941C")]
public class DVoid : DossierPythonCommand
{
public override string EnglishName => "dVoid";
protected override string ScriptRelativePath => "cmd/aussparung.py";
}
+47
View File
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
using System.Runtime.InteropServices;
namespace DOSSIER.Cmd;
[Guid("07F23908-EF40-4A98-A550-C8D8A1F80A7F")]
public class DJoin : DossierPythonCommand
{
public override string EnglishName => "dJoin";
protected override string ScriptRelativePath => "cmd/smart_join.py";
}
[Guid("69DBE84C-5E44-4155-84CB-D67329B64830")]
public class DSplit : DossierPythonCommand
{
public override string EnglishName => "dSplit";
protected override string ScriptRelativePath => "cmd/smart_split.py";
}
[Guid("38E80D26-5270-45C6-B5F3-2E2179545C47")]
public class DPipette : DossierPythonCommand
{
public override string EnglishName => "dPipette";
protected override string ScriptRelativePath => "cmd/pipette.py";
}
[Guid("F2C8B5A1-9D4E-4F73-B2C6-1A8E7D3F5C42")]
public class DSection : DossierPythonCommand
{
public override string EnglishName => "dSection";
protected override string ScriptRelativePath => "cmd/section.py";
}
[Guid("66647D04-F324-459F-82B9-0FD82307FA93")]
public class DKeys : DossierPythonCommand
{
public override string EnglishName => "dKeys";
protected override string ScriptRelativePath => "cmd/dkeys.py";
}
[Guid("93406D93-E9AC-424D-BFBD-3B7A542A85A7")]
public class DWelcome : DossierPythonCommand
{
public override string EnglishName => "dWelcome";
protected override string ScriptRelativePath => "cmd/dwelcome.py";
}
+40
View File
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
using System.Runtime.InteropServices;
namespace DOSSIER.Cmd;
[Guid("4498B184-E064-4049-8B43-873721ECEE71")]
public class DPlan : DossierPythonCommand
{
public override string EnglishName => "dPlan";
protected override string ScriptRelativePath => "view/plan.py";
}
[Guid("D6089B7C-C513-4A39-A62B-5A5E91764A18")]
public class D3D : DossierPythonCommand
{
public override string EnglishName => "d3D";
protected override string ScriptRelativePath => "view/persp3d.py";
}
[Guid("BA89B2DE-2301-4E0D-8542-3BDF393BF7A7")]
public class DMaterial : DossierPythonCommand
{
public override string EnglishName => "dMaterial";
protected override string ScriptRelativePath => "view/material.py";
}
[Guid("A802824C-BC9B-405B-88A4-77125AA7D5A9")]
public class DLevelUp : DossierPythonCommand
{
public override string EnglishName => "dLevelUp";
protected override string ScriptRelativePath => "view/geschoss_up.py";
}
[Guid("A034FF6F-0BCC-48D7-9AC9-8447D5718D32")]
public class DLevelDown : DossierPythonCommand
{
public override string EnglishName => "dLevelDown";
protected override string ScriptRelativePath => "view/geschoss_down.py";
}
+65
View File
@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<AssemblyName>DOSSIER</AssemblyName>
<RootNamespace>DOSSIER</RootNamespace>
<Version>0.2.0</Version>
<Title>DOSSIER</Title>
<Company>Karim Gabriele Varano</Company>
<Description>DOSSIER — Architektur-Studio-Plugin fuer Rhino 8. Bootstrappt beim Plugin-Load die Python-Module (Panels, Aliases, View-Modes, Welcome) und registriert native Commands (dWall, dDoor, dStair, ...) als saubere Wrapper auf die jeweiligen Python-Scripts.</Description>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<!-- Rhino-Plugin-Output: .rhp statt .dll -->
<TargetExt>.rhp</TargetExt>
<NoWarn>NU1701;NETSDK1086</NoWarn>
<EnableDynamicLoading>true</EnableDynamicLoading>
<!-- Kein Konflikt mit Rhinos Eto/WPF -->
<UseWindowsForms>false</UseWindowsForms>
<UseWpf>false</UseWpf>
<!-- Plugin-Metadaten (sichtbar im _PluginManager) -->
<AssemblyTitle>DOSSIER</AssemblyTitle>
<Copyright>Copyright (C) 2026 Karim Gabriele Varano. AGPL-3.0-or-later.</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" Version="8.0.23304.9001" IncludeAssets="compile;build" />
<!-- Rhino-CPython3-Runtime — direkt aus dem App-Bundle linken.
Erlaubt RhinoCode-API ohne den Umweg ueber _-RunPythonScript-Command. -->
<Reference Include="Rhino.Runtime.Code">
<HintPath>/Applications/Rhino 8.app/Contents/Frameworks/RhCore.framework/Versions/A/Resources/Rhino.Runtime.Code.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<!-- Plugin-Guid als Assembly-Attribut (Rhino registriert Plugin via dieser ID) -->
<ItemGroup>
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
<_Parameter1>Rhino.PlugIns.DescriptionType.Address</_Parameter1>
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
<_Parameter2>-</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
<_Parameter1>Rhino.PlugIns.DescriptionType.Email</_Parameter1>
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
<_Parameter2>karim@gabrielevarano.ch</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
<_Parameter1>Rhino.PlugIns.DescriptionType.Organization</_Parameter1>
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
<_Parameter2>Karim Gabriele Varano</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
<_Parameter1>Rhino.PlugIns.DescriptionType.WebSite</_Parameter1>
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
<_Parameter2>https://github.com/karimgvarano/DOSSIER</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.InteropServices.GuidAttribute">
<_Parameter1>e8a4d2c1-6b3f-4e89-9c5a-1d2e3f4a5b6c</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
+61
View File
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
using System;
using System.IO;
namespace DOSSIER;
/// <summary>
/// Locator-Pfade fuer das DOSSIER-Repo. Reihenfolge:
/// 1. Env-Var DOSSIER_HOME
/// 2. File ~/.dossier_home (eine Zeile mit dem Pfad)
/// 3. Hardcoded Fallback /Users/karim/STUDIO/DOSSIER (Dev-Setup)
/// </summary>
internal static class DossierPaths
{
private const string FallbackRoot = "/Users/karim/STUDIO/DOSSIER";
private const string MarkerFile = ".dossier_home";
private static string? _cachedRoot;
public static string? Root
{
get
{
if (_cachedRoot is not null) return _cachedRoot;
_cachedRoot = ResolveRoot();
return _cachedRoot;
}
}
public static string RhinoDir => Path.Combine(Root ?? FallbackRoot, "rhino");
public static string AliasDir => Path.Combine(RhinoDir, "aliases");
public static string CmdDir => Path.Combine(AliasDir, "cmd");
public static string ViewDir => Path.Combine(AliasDir, "view");
public static string StartupPy => Path.Combine(RhinoDir, "startup.py");
private static string? ResolveRoot()
{
var env = Environment.GetEnvironmentVariable("DOSSIER_HOME");
if (!string.IsNullOrEmpty(env) && Directory.Exists(env)) return env;
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var marker = Path.Combine(home, MarkerFile);
if (File.Exists(marker))
{
try
{
var p = File.ReadAllText(marker).Trim();
if (!string.IsNullOrEmpty(p) && Directory.Exists(p)) return p;
}
catch { /* ignore */ }
}
if (Directory.Exists(FallbackRoot)) return FallbackRoot;
return null;
}
}
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
using Rhino;
using Rhino.PlugIns;
namespace DOSSIER;
/// <summary>
/// DOSSIER-Plugin. Drei Aufgaben:
/// 1. Bootstrappt beim Plugin-Load die Python-Module: Panels, Aliases,
/// View-Modes, BeginCommand-Hook, Welcome-Screen (alles ueber rhino/startup.py).
/// 2. Registriert native Rhino-Commands (dWall, dDoor, dStair, ...) die
/// jeweils das passende Python-Script in rhino/aliases/cmd/ ausfuehren.
/// 3. Loest das Echo-/Autocomplete-Problem der frueheren Keyboard-Macros
/// (jetzt zeigt die History "dWall" statt "_-RunPythonScript ...").
///
/// Installation: Plugin via _PluginManager → Install... registrieren. Beim
/// naechsten Rhino-Start laeuft DOSSIER automatisch. Kein zusaetzlicher
/// StartupCommands-XML-Eintrag noetig.
/// </summary>
public class DossierPlugin : PlugIn
{
public DossierPlugin() { Instance = this; }
public static DossierPlugin Instance { get; private set; } = null!;
/// <summary>Plugin bei jedem Rhino-Start automatisch laden — default ist
/// "WhenNeeded" (erst beim ersten Command-Aufruf). Wir brauchen aber
/// AtStartup, damit OnLoad → startup.py-Bootstrap immer feuert.</summary>
public override PlugInLoadTime LoadTime => PlugInLoadTime.AtStartup;
protected override LoadReturnCode OnLoad(ref string errorMessage)
{
var root = DossierPaths.Root;
if (root is null)
{
errorMessage =
"DOSSIER Root nicht gefunden. Setze Env-Var DOSSIER_HOME " +
"auf den DOSSIER-Repo-Ordner (z.B. /Users/karim/STUDIO/DOSSIER) " +
"oder leg ein File ~/.dossier_home an.";
return LoadReturnCode.ErrorShowDialog;
}
RhinoApp.WriteLine($"[DOSSIER] Plugin geladen (root={root})");
// Python-Bootstrap deferred auf Idle — OnLoad feuert vor Eto-UI-Init,
// Panels brauchen aber MainWindow + Idle-Event. PythonRunner.RunDeferred
// wartet auf naechstes Idle und ruft dann startup.py auf.
PythonRunner.RunDeferred(DossierPaths.StartupPy, "startup");
return LoadReturnCode.Success;
}
}
+36
View File
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
using System.IO;
using Rhino;
using Rhino.Commands;
namespace DOSSIER;
/// <summary>
/// Abstrakte Basis fuer alle DOSSIER-Commands die ein Python-Script
/// ausfuehren. Subklasse setzt nur EnglishName + ScriptRelativePath.
///
/// Mechanik: Rhino erlaubt kein synchrones Command-in-Command-Nesting fuer
/// _-RunPythonScript. PythonRunner.RunDeferred wartet auf das naechste Idle-
/// Event und versucht dann zuerst die RhinoCode-API (keine Echo) und faellt
/// auf _-RunPythonScript zurueck. Der Outer-Command beendet sauber mit Success,
/// das Python-Script laeuft direkt danach. Ergebnis fuer den User: sauberer
/// Command-Name in der History (z.B. "dWall") statt "_-RunPythonScript ...".
/// </summary>
public abstract class DossierPythonCommand : Command
{
/// <summary>z.B. "cmd/wand.py" oder "view/plan.py" — relativ zu rhino/aliases/.</summary>
protected abstract string ScriptRelativePath { get; }
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
var scriptPath = Path.Combine(DossierPaths.AliasDir, ScriptRelativePath);
if (!File.Exists(scriptPath))
{
RhinoApp.WriteLine($"[DOSSIER] FEHLER: Script nicht gefunden: {scriptPath}");
return Result.Failure;
}
PythonRunner.RunDeferred(scriptPath, EnglishName);
return Result.Success;
}
}
+79
View File
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
using System;
using System.IO;
using Rhino;
namespace DOSSIER;
/// <summary>
/// Geteilter Python-Script-Runner fuer Plugin-Startup (startup.py) und
/// Commands (cmd/*.py). Primaer ueber Rhino.Runtime.Code-API (CPython3),
/// Fallback ueber _-RunPythonScript-Command.
/// </summary>
internal static class PythonRunner
{
/// <summary>Fuehrt das Script aus. Versucht RhinoCode-API zuerst,
/// faellt auf _-RunPythonScript zurueck. Labels nur fuer Logs.</summary>
public static bool Run(string scriptPath, string label)
{
if (!File.Exists(scriptPath))
{
RhinoApp.WriteLine($"[DOSSIER] {label}: Script nicht gefunden: {scriptPath}");
return false;
}
if (TryRunViaRhinoCode(scriptPath, label)) return true;
try
{
RhinoApp.RunScript($"_-RunPythonScript \"{scriptPath}\"", echo: false);
return true;
}
catch (Exception ex)
{
RhinoApp.WriteLine($"[DOSSIER] {label} RunScript: {ex.Message}");
return false;
}
}
/// <summary>Wie Run, aber defern auf das naechste Idle-Event.
/// Erlaubt safe Invocation aus Plugin-OnLoad oder Command-RunCommand
/// (Rhino mag kein direktes _-RunPythonScript aus diesen Kontexten).</summary>
public static void RunDeferred(string scriptPath, string label)
{
EventHandler? handler = null;
handler = (sender, e) =>
{
RhinoApp.Idle -= handler;
Run(scriptPath, label);
};
RhinoApp.Idle += handler;
}
private static bool TryRunViaRhinoCode(string scriptPath, string label)
{
try
{
var spec = new Rhino.Runtime.Code.Languages.LanguageSpec("*.*.python", "3.*");
var lang = Rhino.Runtime.Code.RhinoCode.Languages.QueryLatest(spec);
if (lang == null) return false;
// RhinoCode.CreateCode(text) setzt __file__/sys.path NICHT automatisch
// — die DOSSIER-Scripts erwarten beides. Injizieren vorne rein.
var pathLit = scriptPath.Replace("\\", "/");
var preamble =
"import sys, os\n" +
$"__file__ = r'{pathLit}'\n" +
"sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n" +
"sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n";
var code = lang.CreateCode(preamble + File.ReadAllText(scriptPath));
var ctx = new Rhino.Runtime.Code.Execution.RunContext();
code.Run(ctx);
return true;
}
catch (Exception ex)
{
RhinoApp.WriteLine($"[DOSSIER] {label} RhinoCode: {ex.Message}");
return false;
}
}
}
+144
View File
@@ -0,0 +1,144 @@
#!/bin/bash
# DOSSIER — Build-Skript
# Baut das C#-Plugin (.rhp) das beim Rhino-Start die Python-Module
# bootstrappt (Panels, Aliases, Welcome) und Native-Commands registriert
# (dWall, dDoor, dStair, dSlab, ...).
#
# === Voraussetzungen ===
# 1. .NET 7 SDK installiert. Auf Mac:
# brew install dotnet@7
# Oder direkt von Microsoft:
# https://dotnet.microsoft.com/download/dotnet/7.0
#
# 2. RhinoCommon NuGet-Package wird beim ersten Build automatisch geladen.
#
# === Build ===
# ./build.sh — Release-Build, output in bin/Release/net7.0/
# ./build.sh debug — Debug-Build mit Symbols
# ./build.sh clean — bin/obj loeschen
# ./build.sh install — Build + ins Rhino Plug-In-Verzeichnis kopieren
#
# === Installation in Rhino (einmalig, auf Mac) ===
# WICHTIG: Mac Rhino 8 unterstuetzt KEIN Drag-Drop fuer .rhp-Plugins
# (der Drag landet im Datei-Oeffnen-Handler, nicht im Plugin-Loader).
#
# Richtiger Weg:
# 1. Rhino 8 oeffnen
# 2. Command-Prompt: PluginManager
# (oder Tools-Menue → Options → Plug-Ins)
# 3. Button "Install..." → browse zur .rhp
# bin/Release/net7.0/DOSSIER.rhp
# 4. Open → Rhino registriert das Plugin
# 5. Rhino restart — DOSSIER bootstrappt (Panels/Aliases/Welcome) +
# Commands dWall/dDoor/... sind verfuegbar
#
# Pfad bleibt in Rhinos settings-XML registriert. Bei spaeteren Builds
# einfach in den gleichen Output-Pfad bauen — Rhino laedt den neuen Stand
# automatisch beim naechsten Start.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# --- Param-Parsing ---
MODE="${1:-release}"
case "$MODE" in
debug|Debug)
CONFIG="Debug"
;;
release|Release|"")
CONFIG="Release"
;;
clean)
echo "==> Loesche bin/ + obj/"
rm -rf bin obj
exit 0
;;
install)
CONFIG="Release"
DO_INSTALL=1
;;
*)
echo "Usage: $0 [release|debug|clean|install]"
exit 1
;;
esac
# --- dotnet-Check ---
if ! command -v dotnet &>/dev/null; then
echo "FEHLER: dotnet nicht installiert."
echo "Install: brew install dotnet@7"
echo " oder https://dotnet.microsoft.com/download/dotnet/7.0"
exit 1
fi
# --- Build ---
echo "==> Build: $CONFIG"
dotnet build -c "$CONFIG"
OUTPUT="$SCRIPT_DIR/bin/$CONFIG/net7.0/DOSSIER.rhp"
if [ ! -f "$OUTPUT" ]; then
echo "FEHLER: Build-Output nicht gefunden: $OUTPUT"
exit 1
fi
echo "==> .rhp Output: $OUTPUT"
# --- Yak-Paket bauen ---
# yak (Rhinos Package Manager) ist im Rhino-App-Bundle dabei. Wir packen .rhp +
# manifest.yml in ein .yak-Archiv das der Launcher bundlet + via "yak install"
# in den User-Plugin-Pfad legt. Dort wird's von Rhino aus dem trusted Yak-
# Verzeichnis geladen.
YAK="/Applications/Rhino 8.app/Contents/Resources/bin/yak"
DIST_DIR="$SCRIPT_DIR/dist"
mkdir -p "$DIST_DIR"
if [ -x "$YAK" ]; then
BUILD_DIR="$SCRIPT_DIR/bin/$CONFIG/net7.0"
pushd "$BUILD_DIR" >/dev/null
# yak spec failed mit Exit-1 wenn manifest.yml schon existiert — kein Fehler
"$YAK" spec --input DOSSIER.rhp >/dev/null 2>&1 || true
rm -f dossier-*.yak
YAK_OUT=$("$YAK" build 2>&1 | grep -oE '/.*\.yak$' | head -1)
popd >/dev/null
if [ -n "$YAK_OUT" ] && [ -f "$YAK_OUT" ]; then
# Versionierter Filename rein damit Launcher die Version vom Filename ablesen kann
YAK_NAME=$(basename "$YAK_OUT")
# Alte .yak im dist/ wegraeumen
rm -f "$DIST_DIR"/dossier-*.yak
cp -v "$YAK_OUT" "$DIST_DIR/$YAK_NAME"
# Stabilen Symlink fuer Launcher (immer 'dossier.yak') zusaetzlich
ln -sf "$YAK_NAME" "$DIST_DIR/dossier.yak"
# Version separat als Textdatei (extrahiert aus manifest.yml im .yak)
VERSION=$(grep '^version:' "$BUILD_DIR/manifest.yml" | awk '{print $2}')
echo -n "$VERSION" > "$DIST_DIR/dossier-version.txt"
echo "==> .yak Output: $DIST_DIR/$YAK_NAME (version=$VERSION)"
else
echo "WARN: yak build hat keinen Output produziert"
fi
else
echo "WARN: yak CLI nicht gefunden ($YAK) — kein .yak-Paket gebaut"
fi
# --- Install: lokales Test-Install via yak ---
# Fuer Dev-Iteration: installiert das frische .yak direkt in den Rhino-User-
# Plugin-Pfad (~/Library/Application Support/McNeel/Rhinoceros/packages/8.0/).
# In Production macht der Launcher das automatisch beim ersten Rhino-Start.
if [ -n "$DO_INSTALL" ]; then
# Alte Manuell-Install-Standorte aufraeumen
OLD_MANUAL="$HOME/Library/Application Support/Dossier/Plugin"
for old in "/Applications/Rhino 8.app/Contents/PlugIns/DOSSIER.rhp" \
"/Applications/Rhino 8.app/Contents/PlugIns/DossierCommands.rhp" \
"$OLD_MANUAL/DOSSIER.rhp"; do
if [ -f "$old" ]; then rm -v "$old"; fi
done
if [ -x "$YAK" ] && [ -f "$DIST_DIR/dossier.yak" ]; then
# yak install nimmt Quelle als Verzeichnis (treats local dir as source server)
"$YAK" install dossier --source "$DIST_DIR" 2>&1 | sed 's/^/ /'
echo "==> Yak-Install fertig. Rhino restart noetig (Plugin laedt on-demand beim ersten Command)."
echo "==> StartupCommands-XML-Eintrag wird vom Launcher gesetzt — fuer Dev manuell pruefen:"
echo " Options → General → Run these commands every time a model is opened"
echo " soll enthalten: _-RunPythonScript \"$SCRIPT_DIR/../../rhino/startup.py\""
fi
fi
echo "OK."
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
+1
View File
@@ -2,6 +2,7 @@
"name": "dossier-launcher",
"private": true,
"version": "0.6.3",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
"dev": "vite",
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# clean-rhino.sh — setzt DOSSIER in Rhino zurueck auf "frisch installiert" Zustand.
# Damit kann das Setup im Launcher (Settings → Setup tab) jederzeit von Null
# durchgespielt werden.
#
# Aufgaben:
# 1. yak uninstall dossier (Plugin raus)
# 2. Window-Layout-Datei loeschen (workspaces/<guid>.xml)
# 3. StartupCommands-XML-Eintrag entfernen (Python-Bootstrap-Trigger)
#
# Bleibt unangetastet:
# - dossier_settings.json (User-Praeferenzen, Tags, etc.)
# - launcher recent.json
# - alles ausserhalb DOSSIER
set -e
RHINO_APP="/Applications/Rhino 8.app"
YAK="$RHINO_APP/Contents/Resources/bin/yak"
SETTINGS_XML="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"
WORKSPACES_DIR="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces"
LAYOUT_GUID="b6b68c03-3031-4899-bca2-fe6e425146fc"
# --- Safety: Rhino muss zu sein ---
if pgrep -f "Rhino 8.app/Contents/MacOS/Rhinoceros$" >/dev/null; then
echo "FEHLER: Rhino laeuft. Bitte beenden und nochmal."
exit 1
fi
# --- 1. Yak uninstall (idempotent — meldet 'package not installed' wenn schon weg) ---
echo "==> 1. Yak uninstall dossier"
if [ -x "$YAK" ]; then
"$YAK" uninstall dossier 2>&1 | sed 's/^/ /' || true
else
echo " WARN: yak nicht gefunden — skip"
fi
# --- 2. Window-Layout-Datei loeschen ---
echo "==> 2. Window-Layout-Datei loeschen"
LAYOUT_FILE="$WORKSPACES_DIR/$LAYOUT_GUID.xml"
if [ -f "$LAYOUT_FILE" ]; then
rm -v "$LAYOUT_FILE" | sed 's/^/ /'
else
echo " schon weg"
fi
# --- 3. StartupCommands-Eintrag aus XML entfernen ---
echo "==> 3. StartupCommands-Eintrag entfernen"
if [ -f "$SETTINGS_XML" ]; then
# sed: matche genau unsere DOSSIER-Zeile und loesche
# (egal welcher Pfad — solange startup.py drin steht)
if grep -q 'StartupCommands.*startup.py' "$SETTINGS_XML"; then
# macOS sed braucht leeres Backup-Suffix
sed -i '' '/<entry key="StartupCommands">.*startup\.py.*<\/entry>/d' "$SETTINGS_XML"
echo " entfernt"
else
echo " schon weg"
fi
else
echo " WARN: Rhino-settings-XML nicht gefunden"
fi
echo
echo "Clean fertig. Naechster Schritt:"
echo " → Launcher → Settings → Setup → 'Setup starten'"
+1
View File
@@ -3,6 +3,7 @@ name = "dossier-launcher"
version = "0.6.3"
description = "Dossier — Projekt-Launcher fuer Rhino"
authors = ["Karim Gabriele Varano"]
license = "AGPL-3.0-or-later"
edition = "2021"
[lib]
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
fn main() {
tauri_build::build()
}
+279 -2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
// Verhindert ein extra Konsolen-Fenster auf Windows im Release-Build.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
@@ -292,11 +294,26 @@ fn plugin_loaded_marker_path() -> PathBuf {
dossier_dir().join("plugin_loaded.flag")
}
fn splash_owner_marker_path() -> PathBuf {
// Vor Launch von Rhino schreibt Launcher diesen Marker → Plugin-Splash
// (rhino/_startup_splash.py) prueft beim Show ob Marker frisch (<30s)
// ist und skippt dann, damit nicht beide Splashes gleichzeitig laufen.
dossier_dir().join("splash_owner_launcher.flag")
}
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);
@@ -312,8 +329,11 @@ fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), Stri
// Splash NUR zeigen wenn Auto-Load aktiv (sonst gibt's nichts zu warten).
let show_splash = settings.auto_load_plugin;
let marker = plugin_loaded_marker_path();
let owner_marker = splash_owner_marker_path();
if show_splash {
let _ = fs::remove_file(&marker);
// Owner-Marker: signalisiert dem Plugin-Splash dass Launcher uebernimmt
let _ = fs::write(&owner_marker, b"launcher");
if let Some(splash) = app.get_webview_window("splash") {
let _ = splash.show();
}
@@ -342,6 +362,7 @@ fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), Stri
std::thread::sleep(std::time::Duration::from_millis(250));
}
let _ = fs::remove_file(&marker);
let _ = fs::remove_file(&owner_marker);
if let Some(splash) = app_clone.get_webview_window("splash") {
let _ = splash.hide();
}
@@ -453,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-
@@ -911,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,
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
// Tauri 2 Konvention: main.rs ist nur Einstieg, Logik in lib.rs (fuer Mobile-
// Unterstuetzung und damit `tauri::generate_context!` korrekt aufgeloest wird).
fn main() {
+3 -1
View File
@@ -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": {
+153
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import React, { useEffect, useState, useMemo, useCallback } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
@@ -124,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
@@ -748,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>
@@ -757,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 />}
@@ -772,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
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
// Material-Symbols-Outlined-style Icons als Inline-SVG. Keine Font-Loads,
// kein Codepoint-Mapping — sauber zu themen via currentColor + stroke-width.
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useState, useCallback } from "react";
import { checkForAppUpdate, installAppUpdate, skipUpdateVersion, isTauri } from "../utils/updater.js";
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
// Shared helpers fuer den Tauri-Updater. Verwendet vom Auto-Check Modal
// (UpdateNotifier) und dem manuellen Check in den Einstellungen.
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+1
View File
@@ -2,6 +2,7 @@
"name": "dossier",
"private": true,
"version": "0.1.0",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
"dev": "vite",
+66
View File
@@ -0,0 +1,66 @@
#! python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""Loescht ALLE Custom-Display-Modes (User-erstellte) — laesst die Rhino-
Built-ins (Wireframe, Shaded, Rendered, Ghosted, XRay, Technical, Artistic,
Pen, Monochrome, Arctic, Raytraced) in Ruhe.
Loescht auch Orphan-Modes ohne Namen (die manchmal bei abgebrochenen
Imports hierbleiben und Rhino zum Crash bringen wenn man sie anklickt).
Vorgehen:
_RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_clean_display_modes.py
"""
from Rhino.Display import DisplayModeDescription
BUILTIN_NAMES = {
"Wireframe", "Shaded", "Rendered", "Ghosted",
"X-Ray", "XRay", "X Ray",
"Technical", "Artistic", "Pen", "Monochrome",
"Arctic", "Raytraced",
}
deleted = []
kept = []
errors = []
for dm in list(DisplayModeDescription.GetDisplayModes()):
name_en = name_local = None
try: name_en = dm.EnglishName
except Exception: pass
try: name_local = dm.LocalName
except Exception: pass
name_display = name_en or name_local or "(Orphan, kein Name)"
is_builtin = (name_en in BUILTIN_NAMES) or (name_local in BUILTIN_NAMES)
if is_builtin:
kept.append(name_display)
continue
# Custom oder Orphan → loeschen
try:
dm_id = dm.Id
ok = DisplayModeDescription.DeleteDisplayMode(dm_id)
if ok:
deleted.append("{} ({})".format(name_display, dm_id))
else:
errors.append("{} → DeleteDisplayMode returned False".format(name_display))
except Exception as ex:
errors.append("{}{}".format(name_display, ex))
print("[CLEAN] Display-Modes gesaeubert.")
print("[CLEAN] Built-ins behalten ({}):".format(len(kept)))
for n in kept:
print("{}".format(n))
print("")
print("[CLEAN] Geloescht ({}):".format(len(deleted)))
for n in deleted:
print(" × {}".format(n))
if errors:
print("")
print("[CLEAN] Fehler ({}):".format(len(errors)))
for e in errors:
print(" ! {}".format(e))
print("")
print("[CLEAN] Fertig. Jetzt _reset_panels.py laufen lassen damit der")
print("[CLEAN] Plugin den 'Dossier Plan' aus dem Template neu importiert.")
+67
View File
@@ -0,0 +1,67 @@
#! python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""Boundary/Hatch-Inspector — zeigt was Rhino setzt wenn du via
Properties-Panel die Section-Boundary aenderst.
Vorgehen:
1. Objekt selektieren
2. In Rhinos Properties → Section Style → Custom → Boundary verstellen
(Farbe ändern, Visible toggeln, Width setzen)
3. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_obj_boundary.py
4. Output schicken — speziell die Boundary-Properties
"""
import Rhino
doc = Rhino.RhinoDoc.ActiveDoc
objs = list(doc.Objects.GetSelectedObjects(False, False))
if not objs:
print("[INSPECT] Bitte Objekt selektieren")
else:
obj = objs[0]
a = obj.Attributes
print("[INSPECT] Object {}".format(str(obj.Id)[:8]))
print("")
print("=== Attributes.SectionAttributesSource ===")
try: print(" =", a.SectionAttributesSource)
except Exception as ex: print(" err:", ex)
print("")
print("=== Attributes.GetCustomSectionStyle() — alle Props ===")
try:
css = a.GetCustomSectionStyle()
if css is None:
print(" None (kein Custom-SectionStyle)")
else:
for n in sorted(dir(css)):
if n.startswith("_"): continue
try:
v = getattr(css, n)
if callable(v): continue
sv = str(v)
if len(sv) > 80: sv = sv[:77] + "..."
print(" {} = {}".format(n, sv))
except Exception as ex:
print(" {} <unreadable: {}>".format(n, ex))
except Exception as ex:
print(" err:", ex)
print("")
print("=== Layer.GetCustomSectionStyle (Layer-Default) ===")
try:
lyr = doc.Layers[a.LayerIndex]
print(" Layer:", lyr.FullPath)
if hasattr(lyr, "GetCustomSectionStyle"):
css = lyr.GetCustomSectionStyle()
if css is None:
print(" Layer hat KEIN Custom-SectionStyle")
else:
for n in sorted(dir(css)):
if n.startswith("_"): continue
try:
v = getattr(css, n)
if callable(v): continue
sv = str(v)
if len(sv) > 80: sv = sv[:77] + "..."
print(" {} = {}".format(n, sv))
except Exception: pass
except Exception as ex:
print(" err:", ex)
+112
View File
@@ -0,0 +1,112 @@
#! python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""Dumpt ALLE Section-/Hatch-relevanten Properties des selektierten Objekts.
So sehen wir was Rhino's eigene Section-Style-UI tatsaechlich setzt vs.
was unser Plugin-Code setzt.
Vorgehen:
1. Ein 3D-Objekt selektieren (Wand, Box, ...)
2. In Rhinos Properties-Panel manuell SectionStyle → Custom mit spezifischen
Werten setzen (z.B. Pattern Color=Gruen, Pattern Rotation=20, Pattern
Scale=2.4, Boundary Color=Rot, Boundary Width Scale=6) → Apply
3. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_obj_section.py
4. Output an Claude
"""
import Rhino
def _fmt(v):
if v is None: return "None"
s = str(v)
if len(s) > 80: s = s[:77] + "..."
return s
def _dump_group(css, prefix, title):
"""Dumpt Properties auf css deren Name mit `prefix` (case-insens) anfaengt."""
print("--- {} ---".format(title))
p_lower = prefix.lower()
found = False
for n in sorted(dir(css)):
if n.startswith("_"): continue
if p_lower not in n.lower(): continue
try:
v = getattr(css, n)
if callable(v): continue
found = True
print(" {:32s} = {}".format(n, _fmt(v)))
except Exception as ex:
print(" {:32s} = <unreadable: {}>".format(n, ex))
if not found:
print(" (nichts)")
doc = Rhino.RhinoDoc.ActiveDoc
objs = list(doc.Objects.GetSelectedObjects(False, False))
if not objs:
print("[INSPECT] Bitte ein Objekt selektieren")
else:
obj = objs[0]
a = obj.Attributes
print("[INSPECT] Object: {} (Id={})".format(type(obj).__name__, obj.Id))
# SectionAttributesSource (FromLayer / FromObject)
print("")
print("=== Attributes ===")
try:
print(" SectionAttributesSource =", a.SectionAttributesSource)
except Exception as ex:
print(" SectionAttributesSource err:", ex)
try:
print(" HatchBackgroundFillColor =", a.HatchBackgroundFillColor)
except Exception: pass
try:
print(" HatchBoundaryVisible =", a.HatchBoundaryVisible)
except Exception: pass
# Custom SectionStyle aus Object
print("")
print("=== Object.GetCustomSectionStyle() ===")
css = None
if hasattr(a, "GetCustomSectionStyle"):
try:
css = a.GetCustomSectionStyle()
except Exception as ex:
print(" err:", ex)
if css is None:
print(" None (kein Custom-SectionStyle set)")
else:
print(" Type:", type(css).__name__)
print("")
# Gruppierte Property-Dumps damit Mapping zu Rhino-UI klar wird
_dump_group(css, "Hatch", "Hatch (Pattern, Color, Scale, Rotation)")
print("")
_dump_group(css, "Boundary", "Boundary (Visible, Color, Width)")
print("")
_dump_group(css, "Background", "Background (FillColor, FillMode)")
print("")
# Section-spezifisch (SectionFillRule etc.)
print("--- Misc Section ---")
for n in ("SectionFillRule", "Name", "Id", "HasUserData", "Index"):
if hasattr(css, n):
try: print(" {:32s} = {}".format(n, _fmt(getattr(css, n))))
except Exception: pass
# Layer-Default SectionStyle als Vergleich
print("")
print("=== Layer.GetCustomSectionStyle (Layer-Default) ===")
try:
lyr = doc.Layers[a.LayerIndex]
print(" Layer:", lyr.FullPath, "Color:", lyr.Color)
if hasattr(lyr, "GetCustomSectionStyle"):
l_css = lyr.GetCustomSectionStyle()
if l_css is None:
print(" Layer hat KEIN Custom-SectionStyle")
else:
_dump_group(l_css, "Hatch", "Layer.Hatch")
_dump_group(l_css, "Boundary", "Layer.Boundary")
_dump_group(l_css, "Background", "Layer.Background")
except Exception as ex:
print(" err:", ex)
+96
View File
@@ -0,0 +1,96 @@
#! python3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""One-Shot-Diagnose: dumpt alle Properties + Werte des 'Dossier Plan'
Display-Modes und exportiert ihn als ini neben dem Skript.
Vorgehen:
1. In Rhinos Display-Mode-Editor: 'Show HiddenLines' AUS schalten +
Apply
2. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_plan_mode.py
3. Resultat: zeigt alle Hidden/Tangent/Silhouette-Properties +
/tmp/dossier_plan_inspect.ini
So koennen wir sehen welche Property-Namen Mac Rhino tatsaechlich hat.
"""
import os
from Rhino.Display import DisplayModeDescription
target_name = "Dossier Plan"
dmd = None
for dm in DisplayModeDescription.GetDisplayModes():
if dm.EnglishName == target_name or dm.LocalName == target_name:
dmd = dm; break
if dmd is None:
print("[INSPECT] 'Dossier Plan' not found")
else:
attrs = dmd.DisplayAttributes
print("[INSPECT] Mode gefunden: {} (Id={})".format(dmd.EnglishName, dmd.Id))
print("")
print("=== ALLE DisplayAttributes Properties mit Werten ===")
for n in sorted(dir(attrs)):
if n.startswith("_"): continue
try:
v = getattr(attrs, n)
if callable(v): continue
sv = str(v)
if len(sv) > 80: sv = sv[:77] + "..."
print(" {} = {}".format(n, sv))
except Exception as ex:
print(" {} = <unreadable: {}>".format(n, ex))
print("")
print("=== Sub-Objekt Properties (ALLE) ===")
# Erst alle Sub-Objekt-Properties autodetect (anything mit "+" im String)
sub_names = set()
for n in dir(attrs):
if n.startswith("_"): continue
try:
v = getattr(attrs, n)
if callable(v): continue
if "DisplayPipelineAttributes+" in str(v):
sub_names.add(n)
except Exception: pass
# Plus die expliziten Kandidaten
for hard in ("CurveSettings", "ObjectSettings", "ShadingSettings",
"MeshSpecificAttributes", "SubObjectDisplayMode",
"ViewSpecificAttributes"):
if hasattr(attrs, hard): sub_names.add(hard)
for sub_name in sorted(sub_names):
try:
sub = getattr(attrs, sub_name)
print(" --- {} ---".format(sub_name))
for n in sorted(dir(sub)):
if n.startswith("_"): continue
try:
v = getattr(sub, n)
if callable(v): continue
sv = str(v)
if len(sv) > 80: sv = sv[:77] + "..."
print(" {} = {}".format(n, sv))
except Exception as ex:
print(" {} = <unreadable: {}>".format(n, ex))
except Exception as ex:
print(" {} couldn't be inspected: {}".format(sub_name, ex))
print("")
print("=== ini-Export ===")
# In den Desktop schreiben damit der User die Datei einfach manuell
# oeffnen + mir den Inhalt schicken kann (in /tmp gehts manchmal verloren).
ini_path = os.path.expanduser("~/Desktop/dossier_plan_inspect.ini")
try:
ok = DisplayModeDescription.ExportToFile(dmd, ini_path)
print(" Export OK: {}{}".format(ok, ini_path))
if ok and os.path.exists(ini_path):
with open(ini_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
print(" ini-Inhalt ({} chars) — siehe Datei auf dem Desktop.".format(len(content)))
# Falls Rhinos Log das Print durchlaesst, hier ueberhaupt rein
print("===INI-START===")
for line in content.split("\n"):
print(line)
print("===INI-END===")
except Exception as ex:
print(" Export-Fehler:", ex)
+2
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
"""Hilfsscript: alle Dossier-Panel-Registrierungs-Flags clearen + Module
neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos
Panel-Manager cached die Icon-Bindung pro GUID — fuer NEUE Icons hilft
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
#
# Nuke + Reset Rhino-8 Settings (Mac). Backupt vorher in einen Ordner mit
# Zeitstempel — verlorene Settings koennen daraus rekonstruiert werden.
#
# Was geht VERLOREN:
# - Alle Custom-Display-Modes (Dossier Plan, DOSSIER2D, etc.)
# - Window-Layouts, Toolbar-Customizations
# - Custom-Keyboard-Shortcuts
# - Tab-Panel-Positions
#
# Was bleibt:
# - Lizenz (License Manager Ordner wird NICHT angefasst)
# - .3dm Templates
# - Scripts unter scripts/
# - Plugin-Einstellungen (in Plug-ins/-Unterordnern)
#
# Vorgehen:
# 1. Rhino komplett quitten (Cmd+Q)
# 2. ./rhino/_reset_rhino_settings.sh
# 3. Rhino neu starten
# 4. _RunPythonScript .../_reset_panels.py → Plan-Mode aus Template
set -e
SETTINGS_DIR="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP="$SETTINGS_DIR.backup-$TIMESTAMP"
if [ ! -d "$SETTINGS_DIR" ]; then
echo "[RESET] Settings-Ordner nicht gefunden: $SETTINGS_DIR"
exit 1
fi
# Check ob Rhino läuft
if pgrep -x "Rhinoceros" > /dev/null; then
echo "[RESET] FEHLER: Rhino läuft noch. Bitte erst Cmd+Q drücken."
exit 1
fi
echo "[RESET] Backup → $BACKUP"
mv "$SETTINGS_DIR" "$BACKUP"
echo "[RESET] Settings nun zurückgesetzt."
echo "[RESET] Beim nächsten Rhino-Start werden Defaults regeneriert."
echo "[RESET] Backup liegt unter: $BACKUP"
echo ""
echo "[RESET] Nächste Schritte:"
echo " 1. Rhino starten"
echo " 2. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_reset_panels.py"
echo " → Dossier-Plan wird aus Template neu erstellt"
+381
View File
@@ -0,0 +1,381 @@
#! python 3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
_startup_splash.py
Petrol-grüner Splash-Screen waehrend des DOSSIER-Plugin-Startups.
Borderless Eto-Form mit WebView + Inline-HTML im selben Stil wie der
Launcher-Splash. Bedeckt visuell die 3+ Sekunden waehrend Rhino die
Panels registriert + WindowLayout neu anwendet.
Wird von startup.py beim ersten Idle gezeigt und nach Layout-Apply
(oder Timeout) wieder versteckt.
"""
import os
import time
import Rhino
import scriptcontext as sc
_SPLASH_KEY = "_dossier_startup_splash"
_SPLASH_SHOWN_AT_KEY = "_dossier_startup_splash_shown_at"
_SAFETY_TIMEOUT_SEC = 12.0 # spaetestens nach 12s wegmachen, falls Hide-Hook nicht feuert
# Marker den der Launcher direkt vor `open -a Rhinoceros` schreibt, damit
# Plugin-Splash NICHT zusaetzlich zum Launcher-Splash erscheint.
_OWNER_MARKER = os.path.expanduser(
"~/Library/Application Support/ch.gabrielevarano.Dossier/splash_owner_launcher.flag"
)
_OWNER_FRESH_SEC = 30.0 # Stale-Schutz falls Launcher crasht
_SPLASH_HTML = '''<!DOCTYPE html>
<html lang="de"><head><meta charset="utf-8"/><title>Dossier laedt</title>
<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.72); --paper-faint: rgba(255,255,255,0.45);
--font-display: Krungthep, 'Archivo Black', sans-serif;
--font-mono: 'DM Mono', 'Menlo', monospace;
}
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; cursor:default; }
.frame { box-sizing:border-box; width:100%; height:100%; padding:22px 26px;
display:grid; grid-template-rows:auto 1fr auto; gap:0;
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:28px; 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; }
.status-row { align-self:end; display:flex; align-items:center; gap:10px;
margin-top:18px; font-size:11px; letter-spacing:0.10em; color:var(--paper);
text-transform:uppercase; }
.dot-pulse { width:7px; height:7px; border-radius:50%; background:var(--paper); }
.bar { position:relative; height:2px; width:100%; background:rgba(255,255,255,0.28);
border-radius:2px; margin-top:12px; }
.meta-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px;
margin-top:10px; font-size:9px; letter-spacing:0.14em; color:var(--paper-faint);
text-transform:uppercase; }
</style></head><body>
<div class="frame">
<div class="brand-row">
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
<div class="version">Rhino 8 Plugin</div>
</div>
<div>
<div class="status-row">
<span class="dot-pulse"></span>
<span>Plugin laedt &mdash; Panels werden platziert</span>
</div>
<div class="bar"></div>
</div>
<div class="meta-row">
<span>AGPL-3.0 &middot; Karim Gabriele Varano</span>
<span>CPython 3.9</span>
</div>
</div></body></html>
'''
def _try_borderless_mac(form):
"""Mac-spezifisch: direkter NSWindow-Zugriff via Eto.ControlObject um
titlebar/Decorations komplett zu killen.
Eto.Mac.Forms.EtoWindow IST-A NSWindow (Xamarin.Mac-Subclass).
StyleMask ist ein .NET-Enum-Property — Python.NET 3 verlangt explizite
Enum-Konversion (kein impliziter int → Enum cast mehr). Wir leiten
den Enum-Typ zur Laufzeit aus dem Getter ab und konstruieren den
Borderless-Wert via System.Enum.ToObject."""
nswindow = getattr(form, "ControlObject", None)
if nswindow is None:
print("[SPLASH] keine ControlObject auf Form")
return False
print("[SPLASH] ControlObject type:", str(type(nswindow)))
import System
ok = False
# NSWindowStyleMaskBorderless = 0
# NSWindowStyleMaskTitled = 1, FullSizeContentView = 32768
try:
current = nswindow.StyleMask
style_type = type(current)
borderless = System.Enum.ToObject(style_type, 0)
nswindow.StyleMask = borderless
print("[SPLASH] StyleMask=0 (Borderless) applied")
ok = True
except Exception as ex:
print("[SPLASH] StyleMask Enum:", ex)
# Fallback: FullSizeContentView (32768) + TitlebarAppearsTransparent
# damit Content unter die (transparente) Titlebar reicht
try:
current = nswindow.StyleMask
style_type = type(current)
full = System.Enum.ToObject(style_type, 1 | 32768)
nswindow.StyleMask = full
print("[SPLASH] StyleMask=Titled|FullSize set (Fallback)")
ok = True
except Exception as ex2:
print("[SPLASH] StyleMask Fallback:", ex2)
# Titlebar transparent + Titel unsichtbar
def _set_prop(prop, value, log=False):
try:
setattr(nswindow, prop, value)
if log: print("[SPLASH] {}={} OK".format(prop, value))
return True
except Exception as ex:
if log: print("[SPLASH] {}:".format(prop), ex)
return False
_set_prop("TitlebarAppearsTransparent", True, True)
# NSWindowTitleHidden = 1
try:
tv_type = type(nswindow.TitleVisibility)
nswindow.TitleVisibility = System.Enum.ToObject(tv_type, 1)
print("[SPLASH] TitleVisibility=Hidden OK")
except Exception as ex:
print("[SPLASH] TitleVisibility:", ex)
_set_prop("IsOpaque", False)
_set_prop("HasShadow", True)
_set_prop("MovableByWindowBackground", True)
# Clear NSWindow background damit rounded corners aus dem HTML sichtbar
# werden. Xamarin.Mac exponiert NSColor.Clear als statische Property.
try:
from AppKit import NSColor as _NSC
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
if clear is not None:
nswindow.BackgroundColor = clear
print("[SPLASH] BackgroundColor=Clear OK")
except Exception as ex:
print("[SPLASH] BackgroundColor Clear:", ex)
# Force-Paint: Splash MUSS sichtbar sein BEVOR Rhino den Script-Thread
# weiter belegt. Python-Script blockiert sonst die Main-Loop und der
# Splash wuerde erst nach Script-Ende paintet werden — viel zu spaet.
try: nswindow.OrderFrontRegardless()
except Exception: pass
try: nswindow.DisplayIfNeeded()
except Exception: pass
try: nswindow.Display()
except Exception: pass
return ok
def _try_transparent_webview_mac(wv):
"""WKWebView transparent machen damit der NSWindow-Hintergrund (oder
nichts) durchscheint und runde Ecken sichtbar werden. wv.ControlObject
ist die WKWebView."""
wk = getattr(wv, "ControlObject", None)
if wk is None:
print("[SPLASH] WebView: keine ControlObject"); return
print("[SPLASH] WebView ControlObject type:", str(type(wk)))
# KVC: setValue:forKey:@"drawsBackground" → @NO. Funktioniert sowohl bei
# WebView (alt) als auch WKWebView (NSObject KVC). Das ist der zuverlaessige
# Weg WebView-Hintergrund komplett zu entfernen, besser als UnderPageBg.
try:
from Foundation import NSNumber, NSString
try:
wk.SetValueForKey(NSNumber.FromBoolean(False), NSString("drawsBackground"))
print("[SPLASH] WebView drawsBackground=NO via KVC OK")
except Exception as ex:
print("[SPLASH] KVC drawsBackground:", ex)
except Exception as ex:
print("[SPLASH] Foundation import:", 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
print("[SPLASH] WebView Layer transparent OK")
except Exception as ex:
print("[SPLASH] WebView Layer:", ex)
except Exception as ex:
print("[SPLASH] WebView NSColor:", ex)
def _dispatch_to_main(fn):
"""Fuehrt fn beim naechsten Rhino-Idle-Event aus. Mac Eto/AppKit
erfordert UI-Mutationen auf dem Main-Thread; threading.Timer-Callbacks
laufen im falschen Thread und Close() crasht oder no-op't dort."""
handler_ref = [None]
def _idle(sender, e):
try: Rhino.RhinoApp.Idle -= handler_ref[0]
except Exception: pass
try: fn()
except Exception as ex:
print("[SPLASH] dispatched fn:", ex)
handler_ref[0] = _idle
try: Rhino.RhinoApp.Idle += _idle
except Exception as ex:
print("[SPLASH] idle subscribe:", ex)
try: fn()
except Exception: pass
def _install_safety_timeout():
"""Registriert Idle-Handler der periodisch prueft ob _SAFETY_TIMEOUT_SEC
erreicht ist. Cleanup-self wenn Splash bereits zu."""
handler_ref = [None]
def _idle(sender, e):
try:
if sc.sticky.get(_SPLASH_KEY) is None:
try: Rhino.RhinoApp.Idle -= handler_ref[0]
except Exception: pass
return
shown_at = sc.sticky.get(_SPLASH_SHOWN_AT_KEY) or 0
if shown_at and (time.time() - shown_at) >= _SAFETY_TIMEOUT_SEC:
try: Rhino.RhinoApp.Idle -= handler_ref[0]
except Exception: pass
print("[SPLASH] safety-timeout — auto-hide")
try: _hide_main()
except Exception: pass
except Exception: pass
handler_ref[0] = _idle
try: Rhino.RhinoApp.Idle += _idle
except Exception as ex:
print("[SPLASH] safety install:", ex)
def _launcher_owns_splash():
"""True wenn Launcher direkt vor Rhino-Launch einen frischen Owner-
Marker geschrieben hat. Verhindert doppelte Splashes."""
try:
if not os.path.isfile(_OWNER_MARKER):
return False
age = time.time() - os.path.getmtime(_OWNER_MARKER)
if age <= _OWNER_FRESH_SEC:
return True
except Exception: pass
return False
def show():
"""Zeigt den Splash. Idempotent — zweiter Aufruf bringt das bestehende
Fenster nur in den Vordergrund. Auto-Hide nach _SAFETY_TIMEOUT_SEC
als Fallback via Idle-Polling (NICHT threading.Timer — Mac UI braucht
Main-Thread). Skipt wenn Launcher seinen eigenen Splash zeigt."""
if _launcher_owns_splash():
print("[SPLASH] Launcher zeigt eigenen Splash — skip"); return
if sc.sticky.get(_SPLASH_KEY) is not None:
print("[SPLASH] schon offen — skip"); return
try:
import Eto.Forms as ef
import Eto.Drawing as ed
except Exception as ex:
print("[SPLASH] Eto-Import:", ex); return
try:
form = ef.Form()
form.Title = "" # leerer Titel hilft bei Mac-Titlebar-Reduktion
# Versuche WindowStyle.None (Eto-API, funktioniert nicht immer auf Mac)
try: form.WindowStyle = getattr(ef.WindowStyle, "None")
except Exception: pass
# Alle Window-Chrome-Optionen aus
for attr, val in (
("Resizable", False), ("Minimizable", False),
("Maximizable", False), ("Closeable", False),
("ShowInTaskbar", False), ("Topmost", True),
):
try: setattr(form, attr, val)
except Exception: pass
try: form.Size = ed.Size(420, 160)
except Exception: pass
# Transparent so dass WebView's eigene rounded gradient sichtbar wird
try:
form.BackgroundColor = ed.Colors.Transparent
except Exception:
try: form.BackgroundColor = ed.Color(0.37, 0.66, 0.59)
except Exception: pass
wv = ef.WebView()
try:
# WebView selber transparent damit das Form-Hintergrund durchscheint
wv.BackgroundColor = ed.Colors.Transparent
except Exception: pass
try:
wv.LoadHtml(_SPLASH_HTML)
except Exception as ex:
print("[SPLASH] LoadHtml:", ex)
form.Content = wv
# Center on screen
try:
screen = ef.Screen.PrimaryScreen
sb = screen.Bounds
x = int(sb.X + (sb.Width - form.Size.Width) / 2)
y = int(sb.Y + (sb.Height - form.Size.Height) / 2 - 100)
form.Location = ed.Point(x, y)
except Exception as ex:
print("[SPLASH] center:", ex)
try: form.Show()
except Exception as ex:
print("[SPLASH] Show:", ex); return
# Mac-spezifischer Borderless-Hack — MUSS nach Show() laufen damit
# die NSWindow existiert
try:
if _try_borderless_mac(form):
print("[SPLASH] Borderless (Mac NSWindow) applied")
except Exception as ex:
print("[SPLASH] borderless-mac:", ex)
# WebView transparent (rounded corners via HTML border-radius)
try: _try_transparent_webview_mac(wv)
except Exception as ex:
print("[SPLASH] webview-clear:", ex)
# Event-Loop einmal explizit pumpen damit Splash gepainted wird
# bevor das Script weiter blockiert (sonst sieht Nutzer die Panels
# zuerst entstehen und Splash erscheint erst danach).
try:
ef.Application.Instance.RunIteration()
except Exception:
pass
sc.sticky[_SPLASH_KEY] = form
sc.sticky[_SPLASH_SHOWN_AT_KEY] = time.time()
print("[SPLASH] visible")
# Safety-Timeout via Idle-Polling (Main-Thread, Mac-safe)
_install_safety_timeout()
except Exception as ex:
print("[SPLASH] show:", ex)
def _hide_main():
"""Synchroner Close — MUSS auf Main-Thread laufen. Nur intern aufrufen,
extern hide() verwenden."""
form = sc.sticky.get(_SPLASH_KEY)
if form is None:
return
sc.sticky[_SPLASH_KEY] = None
sc.sticky[_SPLASH_SHOWN_AT_KEY] = None
try: form.Close()
except Exception:
try: form.Visible = False
except Exception as ex:
print("[SPLASH] hide visible:", ex)
print("[SPLASH] hidden")
def hide():
"""Versteckt + entsorgt den Splash. Idempotent + thread-safe —
dispatcht auf Rhino-Main-Thread via Idle-Event."""
if sc.sticky.get(_SPLASH_KEY) is None:
return
_dispatch_to_main(_hide_main)
+2
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
"""
about.py
About-Dialog als Eto-Form + WebView. Vom DOSSIER-Logo-Klick in der
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'aussparung'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("aussparung")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'dach'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("dach")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'decke'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("decke")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Alias 'dkeys': oeffnet DOSSIER Shortcuts-Cheatsheet
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import welcome
welcome.show_cheatsheet()
+8
View File
@@ -0,0 +1,8 @@
#! python3
# -*- coding: utf-8 -*-
# Alias 'dwelcome': zeigt DOSSIER Welcome-Screen manuell (force-mode,
# ignoriert version-marker + optout)
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import welcome
welcome._show_welcome_now()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'fenster'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("fenster")
+436
View File
@@ -0,0 +1,436 @@
#! python3
# -*- coding: utf-8 -*-
# Pipette / Einstellungen-übernehmen: User klickt ein Source-Objekt, dessen
# Attribute werden zur aktuellen Default-Einstellung gemacht — der naechste
# gezeichnete Curve/Rectangle/etc. erbt sie automatisch.
#
# Was uebernommen wird:
# 1. Layer → wird zum Current Layer
# 2. Color (wenn per-Object Override) → wird Current Object-Color
# 3. Linetype (per-Object) → Current
# 4. PlotWeight (per-Object) → Current
# 5. Fuer DOSSIER-Elemente (wand_axis, treppe_axis, etc.) → spezifische
# UserStrings (Dicke, Modus, Breite, Stufen etc.) werden in sticky
# gespeichert als _last_* → nachste Create-Wand/Treppe etc. nimmt sie.
# 6. Bei Hatch-Quelle → wechselt auf den Curve dahinter (Hatch hat selten
# direkt Sinn als Pipette-Quelle, eher der gefuellte Rahmen).
import scriptcontext as sc
import Rhino
import Rhino.Input.Custom as ric
import Rhino.DocObjects as rdoc
from Rhino.Input import GetResult
# Welche UserStrings pro DOSSIER-Type als sticky _last_* gespeichert werden,
# damit das naechste Create-Cmd sie als Default uebernimmt.
_DOSSIER_INHERIT = {
"wand_axis": [
("dossier_wand_dicke", "wand_dicke"),
("dossier_wand_referenz", "wand_referenz"),
("dossier_wand_modus", "wand_modus"),
],
"treppe_axis": [
("dossier_treppe_breite", "treppe_breite"),
("dossier_treppe_n", "treppe_n"),
("dossier_treppe_referenz", "treppe_referenz"),
("dossier_treppe_modus", "treppe_modus"),
("dossier_treppe_lauf_d", "treppe_lauf_d"),
("dossier_treppe_art", "treppe_art"),
],
"decke_outline": [
("dossier_decke_dicke", "decke_dicke"),
("dossier_decke_modus", "decke_modus"),
],
"dach_outline": [
("dossier_dach_dicke", "dach_dicke"),
("dossier_dach_neigung", "dach_neigung"),
],
"stuetze_point": [
("dossier_trag_profil", "stuetze_profil"),
("dossier_trag_b", "stuetze_b"),
("dossier_trag_h", "stuetze_h"),
],
"traeger_axis": [
("dossier_trag_profil", "traeger_profil"),
("dossier_trag_b", "traeger_b"),
("dossier_trag_h", "traeger_h"),
],
"oeffnung_point": [
("dossier_oeff_breite", "oeff_breite"),
("dossier_oeff_hoehe", "oeff_hoehe"),
],
}
def _save_sticky(key, value):
sc.sticky["elemente_last_" + key] = value
def _find_curve_behind_hatch(doc, hatch_obj):
"""Hatches haben in DOSSIER oft eine zugeordnete Source-Curve (gestaltung
speichert die Curve-ID auf der Hatch via 'ebenen_fill_owner')."""
try:
owner = hatch_obj.Attributes.GetUserString("ebenen_fill_owner") or ""
if owner:
import System
cid = System.Guid(owner)
cobj = doc.Objects.FindId(cid)
if cobj is not None and not cobj.IsDeleted: return cobj
except Exception: pass
return None
def _run():
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
go = ric.GetObject()
go.SetCommandPrompt("Pipette: Quell-Objekt picken (Attribute uebernehmen)")
go.GeometryFilter = (rdoc.ObjectType.Curve
| rdoc.ObjectType.Brep
| rdoc.ObjectType.Hatch
| rdoc.ObjectType.PointSet
| rdoc.ObjectType.Point
| rdoc.ObjectType.Annotation
| rdoc.ObjectType.TextDot)
go.SubObjectSelect = False
if go.Get() != GetResult.Object:
print("[PIPETTE] abgebrochen"); return
src = go.Object(0).Object()
if src is None: return
# Wenn Hatch gepickt, switch zur Source-Curve (gefuelltes Rechteck als
# Pipette-Quelle ist intuitiver als die Hatch selbst)
src_geom_type = type(src.Geometry).__name__
if src_geom_type == "Hatch":
cobj = _find_curve_behind_hatch(doc, src)
if cobj is not None:
src = cobj
print("[PIPETTE] Hatch → zugeordnete Curve verwendet")
sa = src.Attributes
msgs = []
# 1. Layer als Current setzen
try:
if doc.Layers.CurrentLayerIndex != sa.LayerIndex:
doc.Layers.SetCurrentLayerIndex(sa.LayerIndex, True)
try: lname = doc.Layers[sa.LayerIndex].FullPath
except Exception: lname = "idx=" + str(sa.LayerIndex)
msgs.append("Layer={}".format(lname))
except Exception as ex:
print("[PIPETTE] Layer-Set:", ex)
# 2. Color
try:
cs = Rhino.ApplicationSettings.AppearanceSettings
if sa.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromObject
cs.DefaultObjectColor = sa.ObjectColor
msgs.append("Color=obj")
else:
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromLayer
msgs.append("Color=byLayer")
except Exception as ex:
print("[PIPETTE] Color-Set:", ex)
# 3. Linetype + 4. PlotWeight — komplexer, weil Rhino keine direkten
# AppearanceSettings dafuer hat. Wir ueberspringen bewusst, weil der
# Layer-Wechsel die meisten Faelle abdeckt (Linetype + PlotWeight
# kommen typisch ByLayer).
# 5. DOSSIER-spezifische Attrs in sticky uebernehmen
try:
dtype = sa.GetUserString("dossier_element_type") or ""
if dtype in _DOSSIER_INHERIT:
inherited = []
for us_key, sticky_key in _DOSSIER_INHERIT[dtype]:
v = sa.GetUserString(us_key)
if v is None or v == "": continue
# Numerische Werte ggf. konvertieren
if any(k in sticky_key for k in ("dicke", "breite", "hoehe",
"neigung", "lauf_d", "_b", "_h")):
try: v = float(v)
except Exception: pass
elif "n" == sticky_key or sticky_key.endswith("_n"):
try: v = int(float(v))
except Exception: pass
_save_sticky(sticky_key, v)
inherited.append("{}={}".format(sticky_key, v))
if inherited:
msgs.append("DOSSIER " + dtype + ": " + ", ".join(inherited))
except Exception as ex:
print("[PIPETTE] DOSSIER-Inherit:", ex)
if msgs:
print("[PIPETTE] Uebernommen: " + " | ".join(msgs))
else:
print("[PIPETTE] Keine Aenderung (Source identisch zu Defaults)")
# 7. Per-Object Custom-Hatch / Custom-Attrs: speichern als "pending"
# + one-shot Listener auf AddRhinoObject — wenn naechster Curve
# gezeichnet ist, alle Custom-Attrs auf den uebertragen.
_setup_pending_apply(doc, src)
# 6. Auto-Chain: passendes Draw-Command starten basierend auf
# Source-Typ. So hat der User direkt "die richtige Tool in der Hand".
_auto_chain(doc, src)
def _capture_source_hatch_props(doc, src_obj):
"""Wenn Source einen per-Object Custom-Hatch hat, sample dessen
Properties (Pattern/Scale/Rotation/Color)."""
try:
sa = src_obj.Attributes
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
if not fill_hid: return None
import System
hid = System.Guid(fill_hid)
hobj = doc.Objects.FindId(hid)
if hobj is None or hobj.IsDeleted: return None
hg = hobj.Geometry
ha = hobj.Attributes
if not hasattr(hg, "PatternIndex"): return None
return {
"pattern_idx": int(hg.PatternIndex),
"scale": float(hg.PatternScale),
"rotation": float(hg.PatternRotation),
"layer_idx": int(ha.LayerIndex),
"color_source": int(ha.ColorSource),
"color_argb": int(ha.ObjectColor.ToArgb()),
"plot_color_source": int(ha.PlotColorSource),
"plot_color_argb": int(ha.PlotColor.ToArgb()),
"linetype_source": int(ha.LinetypeSource),
"linetype_idx": int(ha.LinetypeIndex),
}
except Exception as ex:
print("[PIPETTE] capture-hatch:", ex)
return None
def _setup_pending_apply(doc, src_obj):
"""Speichert Source-Custom-Attrs in sticky + registriert one-shot
AddRhinoObject-Listener der die Attrs (inkl. Hatch) auf den naechsten
neuen Curve uebertraegt. Nach Apply wird Listener wieder entfernt."""
sa = src_obj.Attributes
# Custom-User-Strings sammeln (DOSSIER-Element-Typen + andere). Skip
# die Fill-Tracking-Keys weil wir den Hatch neu erstellen mit neuer ID.
skip_keys = {
"ebenen_fill_hatch_id", # zeigt auf alte Source-Hatch-ID
"ebenen_fill_owner",
}
user_strings = {}
try:
for k in sa.GetUserStringKeys():
if k in skip_keys: continue
v = sa.GetUserString(k)
if v is not None: user_strings[k] = v
except Exception as ex:
print("[PIPETTE] user-strings:", ex)
# Source-Geometrie Closed-State erfassen — wenn Source closed war,
# erzwingen wir nach dem Add auch auf der Kopie ein Close (Polyline
# bleibt sonst standardmaessig offen, hatten User-Feedback dazu).
src_closed = False
try:
import Rhino.Geometry as _rg
sg = src_obj.Geometry
if isinstance(sg, _rg.Curve) and sg.IsClosed:
src_closed = True
except Exception: pass
pending = {
"linetype_source": int(sa.LinetypeSource),
"linetype_idx": int(sa.LinetypeIndex),
"plot_weight_source": int(sa.PlotWeightSource),
"plot_weight": float(sa.PlotWeight),
"user_strings": user_strings,
"hatch_props": _capture_source_hatch_props(doc, src_obj),
"src_closed": src_closed,
}
sc.sticky["dossier_pipette_pending"] = pending
# One-shot handler — applied beim naechsten AddRhinoObject + entfernt sich
def _on_add(sender, e):
try:
obj = e.TheObject
if obj is None or obj.IsDeleted: return
import Rhino.Geometry as rg2
if not isinstance(obj.Geometry, rg2.Curve): return
_apply_pending(doc, obj, pending)
except Exception as ex:
print("[PIPETTE] one-shot apply:", ex)
finally:
try: Rhino.RhinoDoc.AddRhinoObject -= _on_add
except Exception: pass
sc.sticky.pop("dossier_pipette_pending", None)
try:
Rhino.RhinoDoc.AddRhinoObject += _on_add
except Exception as ex:
print("[PIPETTE] listener-install:", ex)
def _force_close_curve(crv):
"""Schliesst eine offene Polyline durch Anhaengen des Startpunkts.
Generische Curves: MakeClosed (nur wenn Endpunkte nahe) oder Join mit
Lueckensegment. Returns geschlossene Curve oder None bei Fehler."""
import Rhino.Geometry as rg2
if crv is None or crv.IsClosed: return None
try:
if isinstance(crv, rg2.PolylineCurve):
ok, pl = crv.TryGetPolyline()
if ok and pl is not None and pl.Count >= 2:
if pl[0].DistanceTo(pl[pl.Count - 1]) > 1e-9:
pl.Add(pl[0])
return rg2.PolylineCurve(pl)
return None
# Generic: erst MakeClosed (closed wenn Endpunkte innerhalb tol)
try:
if crv.MakeClosed(1e-6): return crv
except Exception: pass
# Fallback: Lueckensegment einfuegen + joinen
line = rg2.LineCurve(crv.PointAtEnd, crv.PointAtStart)
joined = rg2.Curve.JoinCurves([crv, line], 1e-6)
if joined and len(joined) > 0 and joined[0].IsClosed:
return joined[0]
except Exception as ex:
print("[PIPETTE] force-close:", ex)
return None
def _apply_pending(doc, new_obj, pending):
"""Wendet pending state auf das neu erzeugte Objekt an."""
import Rhino.Geometry as rg2
import System
# Close-Erzwingen wenn Source geschlossen war — Polyline-Command erzeugt
# standardmaessig offene Curves; Pipette soll den Closed-State erhalten.
if pending.get("src_closed"):
try:
crv = new_obj.Geometry
if isinstance(crv, rg2.Curve) and not crv.IsClosed:
closed = _force_close_curve(crv)
if closed is not None:
if doc.Objects.Replace(new_obj.Id, closed):
ref = doc.Objects.FindId(new_obj.Id)
if ref is not None: new_obj = ref
print("[PIPETTE] Polyline auto-geschlossen (Source war closed)")
except Exception as ex:
print("[PIPETTE] close-replace:", ex)
# Linetype + PlotWeight overrides
try:
na = new_obj.Attributes.Duplicate()
if pending["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
na.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
na.LinetypeIndex = pending["linetype_idx"]
if pending["plot_weight_source"] == int(rdoc.ObjectPlotWeightSource.PlotWeightFromObject):
na.PlotWeightSource = rdoc.ObjectPlotWeightSource.PlotWeightFromObject
na.PlotWeight = pending["plot_weight"]
# UserStrings 1:1 kopieren
for k, v in pending["user_strings"].items():
try: na.SetUserString(k, v)
except Exception: pass
doc.Objects.ModifyAttributes(new_obj, na, True)
except Exception as ex:
print("[PIPETTE] apply-attrs:", ex)
# Per-Object Custom-Hatch: nachbauen wenn Source einen hatte UND
# der neue Curve closed ist
hp = pending.get("hatch_props")
if hp is None: return
try:
crv = new_obj.Geometry
if not isinstance(crv, rg2.Curve) or not crv.IsClosed: return
tol = doc.ModelAbsoluteTolerance
hatches = rg2.Hatch.Create(crv, hp["pattern_idx"],
hp["rotation"], hp["scale"], tol)
if not hatches or len(hatches) == 0: return
ha = rdoc.ObjectAttributes()
ha.LayerIndex = hp["layer_idx"]
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
try:
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
except Exception: pass
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
ha.LinetypeIndex = hp["linetype_idx"]
ha.SetUserString("ebenen_fill_source", "object")
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
new_hid = doc.Objects.AddHatch(hatches[0], ha)
if new_hid and new_hid != System.Guid.Empty:
# Cross-Link: Curve speichert Hatch-ID
ca = new_obj.Attributes.Duplicate()
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
ca.SetUserString("ebenen_fill_source", "object")
doc.Objects.ModifyAttributes(new_obj, ca, True)
print("[PIPETTE] Per-Object Hatch uebernommen (Pattern={}, Scale={})"
.format(hp["pattern_idx"], hp["scale"]))
except Exception as ex:
print("[PIPETTE] hatch-replicate:", ex)
def _auto_chain(doc, src):
"""Startet das passende Draw-Command basierend auf Source-Typ."""
sa = src.Attributes
dtype = sa.GetUserString("dossier_element_type") or ""
geom = src.Geometry
geom_type = type(geom).__name__
# DOSSIER-BIM: triggere den Dispatcher
_DOSSIER_DRAW = {
"wand_axis": "wand",
"treppe_axis": "treppe",
"decke_outline": "decke",
"dach_outline": "dach",
"stuetze_point": "stuetze",
"traeger_axis": "traeger",
"oeffnung_point": None, # braucht parent-Wand-Kontext → skip auto-chain
"raum_outline": "raum",
}
if dtype in _DOSSIER_DRAW:
action = _DOSSIER_DRAW[dtype]
if action:
import os
_here = os.path.dirname(os.path.abspath(__file__))
wrapper = os.path.join(_here, action + ".py")
if os.path.exists(wrapper):
Rhino.RhinoApp.RunScript(
'_-RunPythonScript "{}"'.format(wrapper), False)
print("[PIPETTE] → starte DOSSIER {}".format(action))
return
# Standard-Rhino-Curves: detect Typ → entsprechendes Draw-Cmd
cmd = None
if geom_type == "LineCurve":
cmd = "_Line"
elif geom_type == "ArcCurve":
# ArcCurve mit voller Sweep = Kreis
try:
if geom.IsClosed: cmd = "_Circle"
else: cmd = "_Arc"
except Exception:
cmd = "_Arc"
elif geom_type == "PolylineCurve":
try:
ok, pl = geom.TryGetPolyline()
if ok and pl is not None and pl.IsClosed and pl.Count == 5:
# Geschlossen + 4 Segmente → vermutlich Rectangle
cmd = "_Rectangle"
else:
cmd = "_Polyline"
except Exception:
cmd = "_Polyline"
elif geom_type == "NurbsCurve":
cmd = "_Curve"
elif geom_type == "TextEntity":
cmd = "_Text"
if cmd:
Rhino.RhinoApp.RunScript(cmd, False)
print("[PIPETTE] → starte {}".format(cmd))
_run()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'raum'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("raum")
+57
View File
@@ -0,0 +1,57 @@
#! python3
# -*- coding: utf-8 -*-
# Wrapper fuer dSection: interaktiver Schnitt-Pick (2 Punkte + Blickrichtung).
# Defaults kommen aus Project-Settings.defaults; nach erfolgreicher
# Erstellung wird der neue Schnitt als aktive Zeichnungs-Ebene set.
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 layers_panel as 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)
+458
View File
@@ -0,0 +1,458 @@
#! 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 _replace_curve_endpoint(curve, which_end, new_pt):
"""Ersetze Start- (which_end=0) oder End-Punkt (which_end=1). Liefert
eine neue Curve oder None bei nicht-unterstuetztem Typ."""
if isinstance(curve, rg.LineCurve):
if which_end == 0:
return rg.LineCurve(new_pt, curve.PointAtEnd)
return rg.LineCurve(curve.PointAtStart, new_pt)
if isinstance(curve, rg.PolylineCurve):
n = curve.PointCount
pts = [curve.Point(i) for i in range(n)]
if which_end == 0: pts[0] = new_pt
else: pts[-1] = new_pt
return rg.PolylineCurve(pts)
# Fallback: generische Curve via Extend
cu = curve.DuplicateCurve()
if cu is None: return None
end_enum = rg.CurveEnd.Start if which_end == 0 else rg.CurveEnd.End
try:
return cu.Extend(end_enum,
rg.CurveExtensionStyle.Line,
[rg.Point3d(new_pt)])
except Exception:
return None
def _walls_and_curves_from_sel(doc, sel):
"""Liefert (axes, generic_curves). Axes = dedup Wand-Achsen (per wall_id),
generic_curves = offene Kurven die KEINE Wand sind. wand_volumes werden
auf ihre Achse via wall_id resolved (auto-group bringt axis+volume
automatisch beide in sel)."""
seen_walls = set()
axes = []
generic = []
# Pre-Index wand_axis by wall_id fuer schnelles Lookup
axis_by_id = {}
for o in doc.Objects:
if o.Attributes.GetUserString("dossier_element_type") == "wand_axis":
wid = o.Attributes.GetUserString("dossier_element_id") or ""
if wid: axis_by_id[wid] = o
for obj in sel:
t = obj.Attributes.GetUserString("dossier_element_type") or ""
wid = obj.Attributes.GetUserString("dossier_element_id") or ""
if t == "wand_axis" and wid and wid not in seen_walls:
axes.append(obj); seen_walls.add(wid)
elif t == "wand_volume" and wid:
wall_ids = {wid}
members_raw = obj.Attributes.GetUserString(
"dossier_wand_chain_members") or ""
if members_raw:
try:
import json as _j
for c in _j.loads(members_raw):
if c: wall_ids.add(c)
except Exception: pass
for w in wall_ids:
if w in seen_walls: continue
ax = axis_by_id.get(w)
if ax is not None:
axes.append(ax); seen_walls.add(w)
elif t == "":
g = obj.Geometry
if isinstance(g, rg.Curve) and not g.IsClosed:
generic.append(obj)
return axes, generic
def _find_nearest_other_wand_axis(doc, my_axis_obj, my_id, tol=1.0):
"""Findet die naechste andere wand_axis im Doc (innerhalb tol).
Return das axis Object oder None."""
if my_axis_obj is None: return None
g = my_axis_obj.Geometry
if not isinstance(g, rg.Curve): return None
bb = g.GetBoundingBox(True)
if not bb.IsValid: return None
best = None; best_d = tol
for obj in doc.Objects:
try:
if obj.Attributes.GetUserString("dossier_element_type") != "wand_axis":
continue
wid = obj.Attributes.GetUserString("dossier_element_id") or ""
if wid == my_id or not wid: continue
except Exception: continue
og = obj.Geometry
if not isinstance(og, rg.Curve): continue
try:
# Mindest-Distanz: Endpunkte gegeneinander UND ClosestPoint
d_min = float('inf')
for ep in (g.PointAtStart, g.PointAtEnd):
rc, t = og.ClosestPoint(ep)
if rc:
d = og.PointAt(t).DistanceTo(ep)
if d < d_min: d_min = d
for ep in (og.PointAtStart, og.PointAtEnd):
rc, t = g.ClosestPoint(ep)
if rc:
d = g.PointAt(t).DistanceTo(ep)
if d < d_min: d_min = d
if d_min < best_d:
best_d = d_min; best = obj
except Exception: continue
return best
def _t_join_attempt(doc, sel):
"""T-Join: 2 OFFENE Kurven wobei der EINE Endpunkt der einen Kurve
nahe (< 1m) auf der ANDEREN Kurve mitten landet (zwischen deren
Endpunkten). Schiebt diesen Endpunkt exakt auf die andere Kurve.
Die andere Kurve bleibt unchanged.
Auch 1-Wand-Modus: wenn nur 1 wand_axis selektiert, sucht automatisch
die naechste andere Wand und snappt diese eine.
Liefert True wenn ausgefuehrt."""
axes, generic = _walls_and_curves_from_sel(doc, sel)
if len(axes) == 2 and len(generic) == 0:
o1, o2 = axes[0], axes[1]
elif len(axes) == 1 and len(generic) == 0:
# 1-Wand-Modus: finde naechste andere wand_axis im Doc
my_id = axes[0].Attributes.GetUserString("dossier_element_id") or ""
other = _find_nearest_other_wand_axis(doc, axes[0], my_id, tol=1.0)
if other is None:
print("[SMART-JOIN] 1-Wand T-Join: keine Nachbar-Wand "
"innerhalb 1m gefunden")
return False
o1 = axes[0]; o2 = other
print("[SMART-JOIN] 1-Wand T-Join: snappe an Nachbar-Wand")
elif len(axes) == 0 and len(generic) == 2:
o1, o2 = generic[0], generic[1]
else:
return False
c1 = o1.Geometry; c2 = o2.Geometry
if not (isinstance(c1, rg.Curve) and isinstance(c2, rg.Curve)):
return False
if c1.IsClosed or c2.IsClosed: return False
tol_snap = 1.00 # 1 m Snap-Radius fuer T-Verbindung — generous damit
# auch grosse Drift (z.B. wenn User auf Outline statt
# Axis gesnappt hat = 30cm Wand-dicke) gefangen wird
end_tol = 0.05 # 5cm: wenn closest-point nahe Endpunkt → eigentlich L
candidates = []
debug_rows = []
# Pro Endpunkt der einen Kurve: ClosestPoint auf der ANDEREN Kurve
for (a_obj, ac, b_obj, bc) in ((o1, c1, o2, c2), (o2, c2, o1, c1)):
for end in (0, 1):
ep = ac.PointAtStart if end == 0 else ac.PointAtEnd
try:
rc, t = bc.ClosestPoint(ep)
if not rc:
debug_rows.append(("axis_end={}".format(end), "ClosestPoint failed"))
continue
cp = bc.PointAt(t)
d = cp.DistanceTo(ep)
ps = bc.PointAtStart; pe = bc.PointAtEnd
d_to_ps = cp.DistanceTo(ps)
d_to_pe = cp.DistanceTo(pe)
reason = None
if d < 1e-6: reason = "schon gesnappt"
elif d > tol_snap: reason = "zu weit ({:.3f} > {:.2f})".format(d, tol_snap)
elif d_to_ps < end_tol: reason = "cp nahe Endpunkt-Start ({:.3f}<{:.2f}) → L-Join Sache".format(d_to_ps, end_tol)
elif d_to_pe < end_tol: reason = "cp nahe Endpunkt-End ({:.3f}<{:.2f}) → L-Join Sache".format(d_to_pe, end_tol)
debug_rows.append(("axis_end={} d={:.3f}".format(end, d),
reason or "candidate"))
if reason is None:
candidates.append((d, a_obj, ac, end, cp))
except Exception as ex:
debug_rows.append(("axis_end={}".format(end), "exc: {}".format(ex)))
# Diagnostic alles ausgeben
for tag, msg in debug_rows:
print("[SMART-JOIN] T-Join check {}: {}".format(tag, msg))
if not candidates: return False
# Naechster Endpunkt → der wird gesnappt
candidates.sort(key=lambda x: x[0])
_d, a_obj, ac, end, cp = candidates[0]
new_c = _replace_curve_endpoint(ac, end, cp)
if new_c is None: return False
ur = doc.BeginUndoRecord("DOSSIER T-Join")
try:
ok = doc.Objects.Replace(a_obj.Id, new_c)
return bool(ok)
finally:
doc.EndUndoRecord(ur)
def _l_join_attempt(doc, sel):
"""Wenn genau 2 OFFENE Kurven (Wand-Achsen oder generische Lines)
selektiert sind, deren End-Tangenten sich in einem Punkt schneiden
beide Kurven extend/shorten zu diesem Punkt (= L-Form). True wenn
ausgefuehrt."""
axes, generic = _walls_and_curves_from_sel(doc, sel)
# Erlaubte Konfigs: 2 Wand-Achsen ODER 2 generische Kurven (keine mix)
if len(axes) == 2 and len(generic) == 0:
o1, o2 = axes[0], axes[1]
elif len(axes) == 0 and len(generic) == 2:
o1, o2 = generic[0], generic[1]
else:
return False
c1 = o1.Geometry; c2 = o2.Geometry
if not (isinstance(c1, rg.Curve) and isinstance(c2, rg.Curve)):
return False
if c1.IsClosed or c2.IsClosed: return False
tol = max(doc.ModelAbsoluteTolerance, 1e-6)
# Closest endpoint pair (a_end, b_end ∈ {0=start, 1=end})
pairs = [
(c1.PointAtStart, c2.PointAtStart, 0, 0),
(c1.PointAtStart, c2.PointAtEnd, 0, 1),
(c1.PointAtEnd, c2.PointAtStart, 1, 0),
(c1.PointAtEnd, c2.PointAtEnd, 1, 1),
]
pairs.sort(key=lambda p: p[0].DistanceTo(p[1]))
p1, p2, e1, e2 = pairs[0]
if p1.DistanceTo(p2) < tol:
return False # bereits verbunden
def _out_dir(c, end):
return -c.TangentAtStart if end == 0 else c.TangentAtEnd
d1 = _out_dir(c1, e1)
d2 = _out_dir(c2, e2)
# Parallel-Check (Cross-Produkt-Laenge in XY)
cross_z = d1.X * d2.Y - d1.Y * d2.X
if abs(cross_z) < 1e-9: return False # parallel
# Unendliche Linien-Intersection
line1 = rg.Line(p1, p1 + d1)
line2 = rg.Line(p2, p2 + d2)
rc, t_a, t_b = rg.Intersect.Intersection.LineLine(line1, line2, tol, False)
if not rc: return False
ipt = line1.PointAt(t_a)
if line2.PointAt(t_b).DistanceTo(ipt) > 0.01:
return False # Schiefe Linien in 3D
nc1 = _replace_curve_endpoint(c1, e1, ipt)
nc2 = _replace_curve_endpoint(c2, e2, ipt)
if nc1 is None or nc2 is None: return False
ur = doc.BeginUndoRecord("DOSSIER L-Join")
try:
ok1 = doc.Objects.Replace(o1.Id, nc1)
ok2 = doc.Objects.Replace(o2.Id, nc2)
return bool(ok1 and ok2)
finally:
doc.EndUndoRecord(ur)
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
# Info-Hint (T-Join unterstuetzt 1-Wand-Modus, L-Join braucht 2)
n_wand_axes = sum(1 for o in sel
if (o.Attributes.GetUserString("dossier_element_type")
or "") == "wand_axis")
if n_wand_axes == 0 and any(
(o.Attributes.GetUserString("dossier_element_type") or "")
.startswith("wand_") for o in sel):
print("[SMART-JOIN] keine Wand-Achse in Selection — selektiere die "
"Wand-Linie oder das Wand-Volumen.")
# T-Join: Endpunkt der einen Curve trifft mitten auf die andere → snap.
# L-Join: beide Endpunkte werden zum Schnittpunkt der verlaengerten Linien
# gezogen. T zuerst probieren (= spezifischer), dann L als Fallback.
if len(sel) >= 2:
# Diagnostic: was sieht smart_join in der Selection?
axes_dbg, generic_dbg = _walls_and_curves_from_sel(doc, sel)
type_counts = {}
for o in sel:
try:
t = o.Attributes.GetUserString("dossier_element_type") or "<none>"
wid_raw = o.Attributes.GetUserString("dossier_element_id") or ""
geom_kind = type(o.Geometry).__name__
key = "{}|{}|wid={}".format(t, geom_kind,
"yes" if wid_raw else "no")
type_counts[key] = type_counts.get(key, 0) + 1
except Exception: pass
print("[SMART-JOIN] sel-detect: {} Wand-Achsen, {} generische Curves "
"(sel total: {})".format(len(axes_dbg), len(generic_dbg), len(sel)))
for k, n in type_counts.items():
print("[SMART-JOIN] {} × {}".format(n, k))
try:
if _t_join_attempt(doc, sel):
doc.Views.Redraw()
print("[SMART-JOIN] T-Join: Endpunkt auf Achse gesnappt")
return
else:
print("[SMART-JOIN] T-Join: kein passender Kandidat (zu weit "
"weg oder am Endpunkt → L-Join Territory)")
except Exception as ex:
print("[SMART-JOIN] T-Join error:", ex)
try:
if _l_join_attempt(doc, sel):
doc.Views.Redraw()
print("[SMART-JOIN] L-Join: 2 Curves zu L verbunden")
return
else:
print("[SMART-JOIN] L-Join: konnte nicht ausfuehren (parallel, "
"schon verbunden, oder Geometrie ungueltig)")
except Exception as ex:
print("[SMART-JOIN] L-Join error:", ex)
# Safety: wenn Wand-Achsen selektiert sind, NIE auf Standard-_Join fallen
# — das wuerde mehrere Achsen zu einer Curve zusammenkleben und die Wand-
# Verknuepfung zerstoeren (Source-Duplikat-Listener kapert die alte ID).
has_wand_axis = any(
obj.Attributes.GetUserString("dossier_element_type") == "wand_axis"
for obj in sel)
if has_wand_axis:
print("[SMART-JOIN] Wand-Achsen selektiert: T-Join/L-Join hat nicht "
"gegriffen (zu viele Selektionen oder zu weit weg). Bitte "
"GENAU 2 Waende selektieren die sich verbinden sollen, dann "
"erneut Cmd+J.")
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 styles 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 failed:", ex)
continue
if not result: continue
# C) Pre-Check Overlap: wenn result-Anzahl gleich input-Anzahl
# ist, gab's keinen tatsaechlichen Overlap → Gruppe nicht
# anfassen.
if len(result) >= len(objs):
continue
# Tatsaechlich gemerged → replace
attrs_template = objs[0].Attributes.Duplicate()
# Fill-Key clearen damit _apply_ebene_fill nicht "schon gefuellt"
# zurueckgibt
try:
attrs_template.SetUserString("ebenen_fill_hatch_id", "")
except Exception: pass
any_had_fill = bool(key[4][0]) # fill_key[0] = had-fill bool
new_ids = []
for crv in result:
nid = doc.Objects.AddCurve(crv, attrs_template)
if nid: new_ids.append(nid)
for o in objs:
try: doc.Objects.Delete(o.Id, True)
except Exception: pass
# Fill nachziehen wenn Inputs welche hatten
if any_had_fill and _g is not None:
for nid in new_ids:
try:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_g._apply_ebene_fill(doc, nobj)
except Exception as fex:
print("[SMART-JOIN] fill-apply:", fex)
n_merged_total += (len(objs) - len(result))
n_groups_ops += 1
finally:
doc.EndUndoRecord(ur)
if n_groups_ops == 0:
print("[SMART-JOIN] Nichts zu mergen — keine Curves overlappen "
"(oder verschiedene Attribute/Layer)")
else:
doc.Views.Redraw()
print("[SMART-JOIN] {} Gruppe(n) bearbeitet, {} Curve(s) zu Union vereint"
.format(n_groups_ops, n_merged_total))
_run()
+267
View File
@@ -0,0 +1,267 @@
#! python3
# -*- coding: utf-8 -*-
# Smart-Split: User zeichnet eine Splitlinie/Polylinie waehrend des Befehls
# (mehrere Klicks, Enter beendet die Eingabe). Alle Curves die die Linie
# schneidet werden gesplittet.
# - Offene Curves: bei den Schnittpunkten in offene Segmente.
# - GESCHLOSSENE Curves: in mehrere CLOSED Sub-Regionen via
# Curve.CreateBooleanRegions (funktioniert auch bei multi-segment
# Polylinien-Cuttern). Per-Object-Hatch wird auf alle Regionen repliziert.
# DOSSIER-Source-Typen (Wand-Achse etc.) bleiben geschuetzt.
import scriptcontext as sc
import Rhino
import Rhino.Input.Custom as ric
import Rhino.Geometry as rg
import Rhino.DocObjects as rdoc
from Rhino.Input import GetResult
# Was Smart-Split NIE anfasst:
# - oeffnung_point / stuetze_point: Punkte, nicht teilbar
# - schnitt_axis: Schnitt-Linien sollen bleiben, sonst kaputte Schnitte
# - treppe_axis: Treppen-State (Lauflinie, Schrittmass-Lock, Wendel-Sweep)
# waere bei einem Split inkonsistent
# Alles andere (wand/traeger/decke/dach/raum/aussparung) DARF gesplittet werden:
# der Add-Listener in elemente.py erkennt die Duplikat-IDs der neuen Stuecke
# und vergibt jedem Stueck ein frisches Element-ID + Regen → BIM-Volumen
# baut sich pro neuem Stueck neu auf.
_PROTECTED_TYPES = {
"treppe_axis",
"oeffnung_point", "stuetze_point", "schnitt_axis",
}
def _capture_hatch_props(doc, src_obj):
try:
sa = src_obj.Attributes
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
if not fill_hid: return None
import System
hid = System.Guid(fill_hid)
hobj = doc.Objects.FindId(hid)
if hobj is None or hobj.IsDeleted: return None
hg = hobj.Geometry
ha = hobj.Attributes
if not hasattr(hg, "PatternIndex"): return None
return {
"pattern_idx": int(hg.PatternIndex),
"scale": float(hg.PatternScale),
"rotation": float(hg.PatternRotation),
"layer_idx": int(ha.LayerIndex),
"color_source": int(ha.ColorSource),
"color_argb": int(ha.ObjectColor.ToArgb()),
"plot_color_source": int(ha.PlotColorSource),
"plot_color_argb": int(ha.PlotColor.ToArgb()),
"linetype_source": int(ha.LinetypeSource),
"linetype_idx": int(ha.LinetypeIndex),
"fill_source": sa.GetUserString("ebenen_fill_source") or "object",
}
except Exception as ex:
print("[SMART-SPLIT] capture-hatch:", ex)
return None
def _replicate_hatch(doc, new_obj, hp):
if hp is None: return
import System
try:
crv = new_obj.Geometry
if not isinstance(crv, rg.Curve) or not crv.IsClosed: return
tol = doc.ModelAbsoluteTolerance
hatches = rg.Hatch.Create(crv, hp["pattern_idx"], hp["rotation"],
hp["scale"], tol)
if not hatches or len(hatches) == 0: return
ha = rdoc.ObjectAttributes()
ha.LayerIndex = hp["layer_idx"]
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
try:
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
except Exception: pass
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
ha.LinetypeIndex = hp["linetype_idx"]
ha.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
new_hid = doc.Objects.AddHatch(hatches[0], ha)
if new_hid and new_hid != System.Guid.Empty:
ca = new_obj.Attributes.Duplicate()
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
ca.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
doc.Objects.ModifyAttributes(new_obj, ca, True)
except Exception as ex:
print("[SMART-SPLIT] hatch-replicate:", ex)
def _collect_polyline_cutter(prompt_first, prompt_more):
"""Sammelt n Punkte fuer den Cutter. Enter beendet (min. 2 Punkte).
ESC bricht ab. Returnt Polyline oder None."""
pts = []
while True:
gp = ric.GetPoint()
if not pts:
gp.SetCommandPrompt(prompt_first)
else:
gp.SetCommandPrompt(prompt_more + " (Enter zum Splitten, ESC = abbrechen)")
gp.SetBasePoint(pts[-1], True)
gp.DrawLineFromPoint(pts[-1], True)
gp.AcceptNothing(True)
res = gp.Get()
if res == GetResult.Nothing:
# Enter gedrueckt
if len(pts) >= 2: return rg.Polyline(pts)
print("[SMART-SPLIT] Mindestens 2 Punkte noetig"); return None
if res != GetResult.Point: return None
pts.append(gp.Point())
def _split_closed_with_cutter(closed_crv, cutter_crv, doc):
"""Splittet closed curve mit beliebigem cutter (Linie oder Polylinie) in
closed Sub-Regionen via Curve.CreateBooleanRegions."""
tol = doc.ModelAbsoluteTolerance
try:
# WorldXY-Plane als Default (DOSSIER ist 2D Plan-Workflow)
plane = rg.Plane.WorldXY
regions = rg.Curve.CreateBooleanRegions(
[closed_crv, cutter_crv], plane, False, tol)
if regions is None or regions.RegionCount == 0:
return None
out = []
for i in range(regions.RegionCount):
rcurves = list(regions.RegionCurves(i))
if not rcurves: continue
if len(rcurves) == 1:
if rcurves[0].IsClosed:
out.append(rcurves[0])
else:
# einzelne offene curve — sollte nicht passieren bei
# Boolean-Regions, aber defensiv
joined = rg.Curve.JoinCurves([rcurves[0]], tol)
if joined and len(joined) > 0 and joined[0].IsClosed:
out.append(joined[0])
else:
joined = rg.Curve.JoinCurves(rcurves, tol)
if joined:
for j in joined:
if j.IsClosed: out.append(j)
return out if out else None
except Exception as ex:
print("[SMART-SPLIT] closed-split:", ex)
return None
def _run():
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
# Polylinie als Cutter sammeln
poly = _collect_polyline_cutter(
"Splitlinie Startpunkt",
"Naechster Punkt")
if poly is None or poly.Count < 2:
return
cutter = rg.PolylineCurve(poly)
tol = doc.ModelAbsoluteTolerance
pre_sel = [o for o in doc.Objects.GetSelectedObjects(False, False)
if o is not None and not o.IsDeleted]
if pre_sel:
source = pre_sel
mode_label = "selektierte ({})".format(len(pre_sel))
else:
s = rdoc.ObjectEnumeratorSettings()
s.HiddenObjects = False; s.LockedObjects = False
source = list(doc.Objects.GetObjectList(s))
mode_label = "alle sichtbaren"
candidates_open = []
candidates_closed = []
for obj in source:
if obj is None or obj.IsDeleted: continue
try:
t = obj.Attributes.GetUserString("dossier_element_type") or ""
if t in _PROTECTED_TYPES: continue
except Exception: pass
g = obj.Geometry
if not isinstance(g, rg.Curve): continue
try:
ints = rg.Intersect.Intersection.CurveCurve(cutter, g, tol, tol)
except Exception:
continue
if not ints or ints.Count == 0: continue
if g.IsClosed:
candidates_closed.append((obj, g))
else:
params = []
for i in range(ints.Count):
ev = ints[i]
if ev.IsPoint:
params.append(ev.ParameterB)
else:
params.append(ev.ParameterB); params.append(ev.ParameterB2)
if params:
params = sorted(set(round(p, 6) for p in params))
candidates_open.append((obj, g, params))
if not candidates_open and not candidates_closed:
print("[SMART-SPLIT] Cutter schneidet nichts ({})".format(mode_label))
return
ur = doc.BeginUndoRecord("DOSSIER Smart-Split")
n_open = 0; n_closed = 0
try:
# Closed: Boolean-Regions → CLOSED Sub-Regionen + Fill replicate
for obj, crv in candidates_closed:
try:
regions = _split_closed_with_cutter(crv, cutter, doc)
if not regions or len(regions) <= 1: continue
hatch_props = _capture_hatch_props(doc, obj)
attrs = obj.Attributes.Duplicate()
try: attrs.SetUserString("ebenen_fill_hatch_id", "")
except Exception: pass
new_ids = []
for r in regions:
nid = doc.Objects.AddCurve(r, attrs)
if nid: new_ids.append(nid)
doc.Objects.Delete(obj.Id, True)
if hatch_props is not None:
for nid in new_ids:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_replicate_hatch(doc, nobj, hatch_props)
else:
try:
import styles as _gmod
for nid in new_ids:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_gmod._apply_ebene_fill(doc, nobj)
except Exception: pass
n_closed += 1
except Exception as ex:
print("[SMART-SPLIT] closed-fail:", ex)
# Open: split bei Params
for obj, crv, params in candidates_open:
try:
pieces = crv.Split(params)
if not pieces or len(pieces) <= 1: continue
attrs = obj.Attributes.Duplicate()
for p in pieces:
doc.Objects.AddCurve(p, attrs)
doc.Objects.Delete(obj.Id, True)
n_open += 1
except Exception as ex:
print("[SMART-SPLIT] open-fail:", ex)
finally:
doc.EndUndoRecord(ur)
doc.Views.Redraw()
print("[SMART-SPLIT] {} closed-Regionen + {} offene Curves gesplittet "
"({} Cutter-Punkte, {})"
.format(n_closed, n_open, poly.Count, mode_label))
_run()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'stempel'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("stempel")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'stuetze'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("stuetze")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'symbol'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("symbol")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'traeger'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("traeger")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'treppe'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("treppe")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'tuer'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("tuer")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'wand'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("wand")
+97
View File
@@ -0,0 +1,97 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
dossier_dispatch.py
Universal-Wrapper fuer DOSSIER-Bridge-Commands via Rhino-Alias.
Aufruf vom Alias:
_-RunPythonScript "/.../dossier_dispatch.py" <action>
oder via Rhino.Input.RhinoGet wir lesen den letzten String-Parameter
aus der Command-Line.
Aktionen mappen auf ElementeBridge._cmd_create_* via einer kleinen
Dispatch-Tabelle. Bridge-Referenz wird in sc.sticky vom panel_factory
abgelegt (siehe elemente.py _bridge_factory).
"""
import sys
import scriptcontext as sc
_ACTIONS = {
"wand": ("_cmd_create_wall", ()),
"tuer": ("_cmd_create_oeffnung", ("tuer",)),
"fenster": ("_cmd_create_oeffnung", ("fenster",)),
"decke": ("_cmd_create_decke", ()),
"aussparung":("_cmd_create_aussparung",()),
"dach": ("_cmd_create_dach", ()),
"treppe": ("_cmd_create_treppe", ()),
"stuetze": ("_cmd_create_stuetze", ()),
"traeger": ("_cmd_create_traeger", ()),
"raum": ("_cmd_create_raum", ()),
"stempel": ("_cmd_create_stempel", ()),
"symbol": ("_cmd_create_symbol", ()),
}
_PRETTY = {
"wand": "DOSSIER Wand",
"tuer": "DOSSIER Tuer",
"fenster": "DOSSIER Fenster",
"decke": "DOSSIER Decke",
"aussparung": "DOSSIER Aussparung",
"dach": "DOSSIER Dach",
"treppe": "DOSSIER Treppe",
"stuetze": "DOSSIER Stuetze",
"traeger": "DOSSIER Traeger",
"raum": "DOSSIER Raum",
"stempel": "DOSSIER Stempel",
"symbol": "DOSSIER Symbol",
}
def dispatch(action):
"""Public entry — von per-action Wrapper-Scripts aufgerufen."""
try:
import Rhino
Rhino.RhinoApp.SetCommandPrompt(_PRETTY.get(action, "DOSSIER " + action.capitalize()))
except Exception: pass
bridge = sc.sticky.get("dossier_bridge_elemente")
if bridge is None:
print("[DOSSIER-ALIAS] Elemente-Bridge nicht aktiv (Panel oeffnen)")
return
spec = _ACTIONS.get(action)
if spec is None:
print("[DOSSIER-ALIAS] Unbekannte Aktion:", action)
return
method_name, args = spec
method = getattr(bridge, method_name, None)
if method is None:
print("[DOSSIER-ALIAS] Bridge-Method fehlt:", method_name)
return
try:
method({}, *args)
except Exception as ex:
print("[DOSSIER-ALIAS]", action, "->", method_name, ":", ex)
# Backwards-Compat (alter Name).
_dispatch = dispatch
def _read_action_from_argv():
# sys.argv enthaelt bei _-RunPythonScript "path" arg1 arg2 ... die
# Args nach dem Skript-Pfad. argv[0] = Skript-Pfad.
if len(sys.argv) >= 2:
return str(sys.argv[1]).strip().lower()
return None
if __name__ == "__main__":
a = _read_action_from_argv()
if a:
_dispatch(a)
else:
print("[DOSSIER-ALIAS] Keine Aktion uebergeben. Erwartet:",
", ".join(sorted(_ACTIONS.keys())))
+72
View File
@@ -0,0 +1,72 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
dossier_view_mode.py
Setzt Display-Mode (+ optional Standard-Ansicht) im aktiven Viewport.
Aufruf:
_-RunPythonScript "/.../dossier_view_mode.py" <mode>
mode: plan | persp3d | material | raytracing
"""
import sys
import Rhino
_MODES = {
"plan": {"display": "Dossier Plan", "view": "Top", "label": "DOSSIER Plan-Mode"},
"persp3d": {"display": "Dossier 3D", "view": "Perspective","label": "DOSSIER 3D-Mode"},
"material": {"display": "Dossier Material", "view": None, "label": "DOSSIER Material-Mode"},
"raytracing": {"display": "Dossier Raytracing", "view": None, "label": "DOSSIER Raytracing"},
}
def _apply(mode_name):
spec = _MODES.get(mode_name)
if spec is None:
print("[VIEW-MODE] Unbekannt:", mode_name)
return
try: Rhino.RhinoApp.SetCommandPrompt(spec.get("label", "DOSSIER View"))
except Exception: pass
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
print("[VIEW-MODE] Kein aktives Doc")
return
view = doc.Views.ActiveView
if view is None:
print("[VIEW-MODE] Kein aktiver Viewport")
return
# Standard-View setzen (Top / Perspective) falls definiert
vw_name = spec["view"]
if vw_name:
try:
view.ActiveViewport.SetProjection(
Rhino.Display.DefinedViewportProjection.Top
if vw_name == "Top"
else Rhino.Display.DefinedViewportProjection.Perspective,
vw_name, True)
except Exception as ex:
print("[VIEW-MODE] view-set:", ex)
# Display-Mode setzen via Description-Lookup
dm_name = spec["display"]
try:
all_dm = Rhino.Display.DisplayModeDescription.GetDisplayModes()
target = None
for d in all_dm:
if d.EnglishName == dm_name or d.LocalName == dm_name:
target = d; break
if target is None:
print("[VIEW-MODE] Display-Mode not found:", dm_name)
return
view.ActiveViewport.DisplayMode = target
view.Redraw()
except Exception as ex:
print("[VIEW-MODE] display-mode:", ex)
if __name__ == "__main__":
if len(sys.argv) >= 2:
_apply(str(sys.argv[1]).strip().lower())
else:
print("[VIEW-MODE] Erwartet Mode-Name:", ", ".join(_MODES.keys()))
+469
View File
@@ -0,0 +1,469 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
aliases/loader.py
Liest shortcuts_default.json + User-Overrides aus dossier_settings.json,
merged und wendet via Rhino.ApplicationSettings.CommandAliasList /
ShortcutKeySettings an. Wird einmal beim Rhino-Start aus startup.py
aufgerufen (idempotent SetMacro ueberschreibt).
User-Override-Format in dossier_settings.json:
"shortcuts_user": {
"<action_id>": "<trigger_string>" // leer = Default
}
"""
import os
import json
import Rhino
_HERE = os.path.dirname(os.path.abspath(__file__))
_quit_xml_pairs = [] # gefuellt in apply_all(), genutzt vom Closing-Hook
_DEFAULTS_PATH = os.path.join(_HERE, "shortcuts_default.json")
_SETTINGS_PATHS = [
os.path.expanduser("~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json"),
os.path.expanduser("~/Library/Application Support/RhinoPanel/dossier_settings.json"), # legacy
]
def _read_defaults():
try:
with open(_DEFAULTS_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
out = {}
for k, v in data.items():
if k.startswith("_"): continue
if not isinstance(v, dict): continue
out[k] = v
return out
except Exception as ex:
print("[ALIASES] 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("[ALIASES] 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 unchanged (Case-preserved)."""
if key_part in _SPECIAL_KEY_NAMES:
return _SPECIAL_KEY_NAMES[key_part]
return key_part
def _xml_key_from_trigger(trigger):
"""'Cmd+Shift+F3''CommandShiftF3' (Mac Rhino XML-Schema).
Cmd/Ctrl 'Command', Shift 'Shift', Alt/Option 'Option'.
Sonderzeichen ('-', '/', etc.) werden auf Enum-Namen gemapped."""
t = trigger.replace(" ", "")
parts = t.split("+") if "+" in t[1:] else [t]
# Edge-Case: trigger endet auf literal '+' oder '-' → letztes Element ist Key
# 'Cmd+-' → ['Cmd', '', '-'] via split. Fix: re-split last token wenn leer
parts = [p for p in parts if p != ""]
# Sonderfall trigger == 'Cmd+-' → split('+') = ['Cmd', '-'], OK
# Sonderfall trigger == 'Cmd++' → split('+') = ['Cmd', '', ''] → key = '+'
if "Cmd++" in trigger or "Ctrl++" in trigger or "Shift++" in trigger:
parts = trigger.replace(" ", "").rstrip("+").split("+") + ["+"]
if not parts: return None
key_part = _normalize_key_part(parts[-1])
mods = set(p.lower() for p in parts[:-1])
has_cmd = ("cmd" in mods) or ("ctrl" in mods) or ("command" in mods)
has_shift = "shift" in mods
has_alt = ("alt" in mods) or ("option" in mods) or ("opt" in mods)
prefix = ""
if has_cmd: prefix += "Command"
if has_shift: prefix += "Shift"
if has_alt: prefix += "Option"
return prefix + key_part
def _entry_in_xml(xml_key, expected_macro):
"""True wenn <entry key='<xml_key>'>expected_macro</entry> bereits im
Mac Rhino settings-XML existiert."""
import os
import re
paths = [
os.path.expanduser("~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"),
]
_esc = lambda s: s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
pat = re.compile(
r'<entry\s+key="' + re.escape(xml_key) + r'"\s*>([^<]*)</entry>')
for path in paths:
if not os.path.exists(path): continue
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
m = pat.search(content)
if m and m.group(1) == _esc(expected_macro):
return True
except Exception: pass
return False
def _xml_persist_shortcut(xml_key, macro, verbose=False):
"""Schreibt <entry key="<xml_key>"><macro></entry> direkt in Mac Rhino's
settings-Scheme__Default.xml unter <child key='ShortcutKeys'>. String-
basiert damit die Original-Formatierung 1:1 erhalten bleibt."""
import os
import re
paths = [
os.path.expanduser("~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"),
]
n_written = 0
_esc = lambda s: s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
for path in paths:
if not os.path.exists(path): continue
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
new_entry = '<entry key="{}">{}</entry>'.format(xml_key, _esc(macro))
# Existing entry? Loeschen (mit umgebendem Whitespace+Newline)
# und neu hinzufuegen mit sauberem Format. Vermeidet
# kaputt-formatierte Entries.
pat = re.compile(
r'<entry\s+key="' + re.escape(xml_key) + r'"\s*(/>|>[^<]*</entry>)')
m = pat.search(content)
if m:
# Check Line-Kontext: nur diese Entry auf Zeile + unchanged?
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("[ALIASES] 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("[ALIASES] ShortcutKeys-section fehlt")
continue
sec_end = content.find('</child>', sec_start)
if sec_end < 0:
if verbose: print("[ALIASES] 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("[ALIASES] XML {} '{}'".format(action, xml_key))
except Exception as ex:
print("[ALIASES] 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("[ALIASES] 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("[ALIASES] 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("[ALIASES] 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("[ALIASES] 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("[ALIASES] WARN F-Key {} ({}) konnte weder "
"API noch XML set 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("[ALIASES] {} ({}): 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("[ALIASES] Unbekannter Type:", spec_type); n_skipped += 1
except Exception as ex:
print("[ALIASES] Apply", action_id, "->", trigger, ":", ex)
n_skipped += 1
# Quit-Hook installieren falls XML-only Shortcuts set wurden — diese
# ueberlebt sonst Rhino's Auto-Save beim Quit nicht.
if _quit_xml_pairs:
_install_quit_xml_save(list(_quit_xml_pairs))
print("[ALIASES] {} XML-only Shortcuts werden bei Quit "
"re-persistiert (closing hook installed)"
.format(len(_quit_xml_pairs)))
return n_alias, n_fkey, n_cmd, n_skipped
if __name__ == "__main__":
a, f, c, s = apply_all()
print("[ALIASES] OK: {} alias, {} fkey, {} cmd, {} skipped"
.format(a, f, c, s))
+66
View File
@@ -0,0 +1,66 @@
{
"_meta": {
"version": 2,
"description": "DOSSIER Default Shortcuts. Schema: F1-F12 = 2D-Werkzeuge (Single-Tastendruck). Shift+F* = Views/Panels. Cmd+F* = BIM-Objekte. F8/F9 bleiben Rhino-Default (Ortho/Snap). 2D-Tools auch als Alias n1-n0 (Fallback fuer typen). 2-Letter-Aliases (st/tg/ra/sy/sp/dh/au) fuer seltenere BIM. User-Overrides leben in dossier_settings.json unter 'shortcuts_user' = {action_id: trigger_string}. Macro-Platzhalter {ALIASDIR} wird zur Laufzeit ersetzt."
},
"wand": { "type": "fkey", "trigger": "Cmd+F1", "label": "DOSSIER Wand erstellen", "macro": "dWall" },
"tuer": { "type": "fkey", "trigger": "Cmd+F2", "label": "DOSSIER Tuer erstellen", "macro": "dDoor" },
"fenster": { "type": "fkey", "trigger": "Cmd+F3", "label": "DOSSIER Fenster erstellen", "macro": "dWindow" },
"decke": { "type": "fkey", "trigger": "Cmd+F4", "label": "DOSSIER Decke erstellen", "macro": "dSlab" },
"treppe": { "type": "fkey", "trigger": "Cmd+F5", "label": "DOSSIER Treppe erstellen", "macro": "dStair" },
"stuetze": { "type": "fkey", "trigger": "Cmd+F6", "label": "DOSSIER Stuetze erstellen", "macro": "dColumn" },
"traeger": { "type": "fkey", "trigger": "Cmd+F7", "label": "DOSSIER Traeger erstellen", "macro": "dBeam" },
"raum": { "type": "fkey", "trigger": "Cmd+F10", "label": "DOSSIER Raum erstellen", "macro": "dRoom" },
"symbol": { "type": "fkey", "trigger": "Cmd+F11", "label": "DOSSIER Symbol erstellen", "macro": "dSymbol" },
"stempel": { "type": "fkey", "trigger": "Cmd+F12", "label": "DOSSIER Stempel erstellen", "macro": "dTag" },
"dach": { "type": "alias", "trigger": "dh", "label": "DOSSIER Dach (Alias)", "macro": "dRoof" },
"aussparung": { "type": "alias", "trigger": "au", "label": "DOSSIER Aussparung (Alias)", "macro": "dVoid" },
"text": { "type": "fkey", "trigger": "F1", "label": "Text", "macro": "_Text" },
"line": { "type": "fkey", "trigger": "F2", "label": "Linie", "macro": "_Line" },
"arc": { "type": "fkey", "trigger": "F3", "label": "Kreisbogen", "macro": "_Arc" },
"rectangle": { "type": "fkey", "trigger": "F4", "label": "Rechteck", "macro": "_Rectangle" },
"polyline": { "type": "fkey", "trigger": "F5", "label": "Polylinie", "macro": "_Polyline" },
"curve": { "type": "fkey", "trigger": "F6", "label": "Spline / Kurve", "macro": "_Curve" },
"hatch": { "type": "fkey", "trigger": "F7", "label": "Schraffur", "macro": "_Hatch" },
"polygon": { "type": "fkey", "trigger": "F10", "label": "Polygon", "macro": "_Polygon" },
"ellipse": { "type": "fkey", "trigger": "F11", "label": "Ellipse", "macro": "_Ellipse" },
"circle": { "type": "fkey", "trigger": "F12", "label": "Kreis", "macro": "_Circle" },
"view_plan": { "type": "fkey", "trigger": "Cmd+K", "label": "Plan-Mode (Top + Dossier Plan)", "macro": "dPlan" },
"view_3d": { "type": "fkey", "trigger": "Cmd+L", "label": "3D-Mode (Perspective + Dossier 3D)", "macro": "d3D" },
"zoom_ext": { "type": "fkey", "trigger": "Cmd+U", "label": "Zoom Extents", "macro": "_Zoom _All _Extents" },
"zoom_sel": { "type": "fkey", "trigger": "Cmd+Shift+U", "label": "Zoom Selected", "macro": "_Zoom _Selected" },
"mod_group": { "type": "fkey", "trigger": "Cmd+G", "label": "Gruppieren (Group)", "macro": "_Group" },
"geschoss_up": { "type": "alias", "trigger": "gu", "label": "Geschoss hoch (Alias)", "macro": "dLevelUp" },
"geschoss_down": { "type": "fkey", "trigger": "Cmd+B", "label": "Geschoss tief", "macro": "dLevelDown" },
"view_material": { "type": "alias", "trigger": "ma", "label": "Material-Mode (Alias)", "macro": "dMaterial" },
"panel_layer": { "type": "alias", "trigger": "la", "label": "Layer-Panel (Alias)", "macro": "_Layer" },
"panel_elemente": { "type": "alias", "trigger": "el", "label": "DOSSIER Elemente-Panel (Alias)", "macro": "-_ShowPanel \"DOSSIER Elemente\"" },
"mod_mirror": { "type": "fkey", "trigger": "Cmd+I", "label": "Spiegeln (Mirror)", "macro": "_Mirror" },
"mod_copy": { "type": "fkey", "trigger": "Cmd+D", "label": "Kopieren (Copy = Duplicate)", "macro": "_Copy" },
"mod_rotate": { "type": "fkey", "trigger": "Cmd+R", "label": "Drehen (Rotate)", "macro": "_Rotate" },
"mod_trim": { "type": "fkey", "trigger": "Cmd+T", "label": "Trim (Schneiden)", "macro": "_Trim" },
"mod_join": { "type": "fkey", "trigger": "Cmd+J", "label": "Verbinden (Smart-Join: Regionen → Union, sonst Join)", "macro": "dJoin" },
"mod_explode": { "type": "fkey", "trigger": "Cmd+E", "label": "Trennen (Explode)", "macro": "_Explode" },
"mod_fillet": { "type": "fkey", "trigger": "Cmd+Shift+V", "label": "Verrunden (Fillet)", "macro": "_Fillet" },
"mod_move": { "type": "fkey", "trigger": "Cmd+M", "label": "Verschieben (Move)", "macro": "_Move" },
"mod_offset": { "type": "fkey", "trigger": "Cmd+Shift+P", "label": "Parallele (OffsetCrv)", "macro": "_OffsetCrv" },
"mod_split": { "type": "fkey", "trigger": "Cmd+X", "label": "Smart-Split (Splitlinie zeichnen — ueberschreibt Cut)", "macro": "dSplit" },
"mod_chamfer": { "type": "fkey", "trigger": "Cmd+Shift+C", "label": "Abfasen (Chamfer)", "macro": "_Chamfer" },
"mod_pipette": { "type": "fkey", "trigger": "Cmd+Y", "label": "Pipette (Einstellungen uebernehmen)", "macro": "dPipette" },
"cheatsheet": { "type": "fkey", "trigger": "Cmd+-", "label": "DOSSIER Shortcuts-Cheatsheet", "macro": "dKeys" },
"text_alias": { "type": "alias", "trigger": "n1", "label": "Text (Alias)", "macro": "_Text" },
"line_alias": { "type": "alias", "trigger": "n2", "label": "Linie (Alias)", "macro": "_Line" },
"arc_alias": { "type": "alias", "trigger": "n3", "label": "Kreisbogen (Alias)", "macro": "_Arc" },
"rectangle_alias": { "type": "alias", "trigger": "n4", "label": "Rechteck (Alias)", "macro": "_Rectangle" },
"polyline_alias": { "type": "alias", "trigger": "n5", "label": "Polylinie (Alias)", "macro": "_Polyline" },
"curve_alias": { "type": "alias", "trigger": "n6", "label": "Kurve (Alias)", "macro": "_Curve" },
"hatch_alias": { "type": "alias", "trigger": "n7", "label": "Schraffur (Alias)", "macro": "_Hatch" },
"polygon_alias": { "type": "alias", "trigger": "n8", "label": "Polygon (Alias)", "macro": "_Polygon" },
"ellipse_alias": { "type": "alias", "trigger": "n9", "label": "Ellipse (Alias)", "macro": "_Ellipse" },
"circle_alias": { "type": "alias", "trigger": "n0", "label": "Kreis (Alias)", "macro": "_Circle" }
}
+42
View File
@@ -0,0 +1,42 @@
#! python3
# -*- coding: utf-8 -*-
# Geschoss runter (zum naechsttieferen Eintrag in der Zeichnungsebenen-Liste)
import json
import scriptcontext as sc
import Rhino
def _go(delta):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
print("[GESCHOSS-NAV] kein Doc"); return
bridge = sc.sticky.get("ebenen_bridge_ref")
if bridge is None:
print("[GESCHOSS-NAV] Ebenen-Bridge nicht aktiv (Panel oeffnen)"); return
try:
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or ""
zs = json.loads(zraw) if zraw else []
if not isinstance(zs, list) or not zs:
print("[GESCHOSS-NAV] keine Zeichnungsebenen"); return
cur_id = doc.Strings.GetValue("dossier_active_id") or ""
idx = -1
for i, z in enumerate(zs):
if isinstance(z, dict) and z.get("id") == cur_id:
idx = i; break
if idx < 0:
idx = len(zs) # nichts aktiv → starten unten
new_idx = max(0, min(len(zs) - 1, idx + delta))
if new_idx == idx:
print("[GESCHOSS-NAV] schon am {}".format(
"untersten" if delta > 0 else "obersten")); return
target = zs[new_idx]
if not isinstance(target, dict) or not target.get("id"):
print("[GESCHOSS-NAV] Zielebene ungueltig"); return
print("[GESCHOSS-NAV] wechsle zu '{}'".format(target.get("name") or target["id"]))
bridge._set_active_zeichnungsebene(target)
except Exception as ex:
print("[GESCHOSS-NAV]", ex)
# delta=+1 = nach unten (naechster Eintrag in der Liste)
_go(+1)
+43
View File
@@ -0,0 +1,43 @@
#! python3
# -*- coding: utf-8 -*-
# Geschoss hoch (zum naechstoberen Eintrag in der Zeichnungsebenen-Liste)
import json
import scriptcontext as sc
import Rhino
def _go(delta):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
print("[GESCHOSS-NAV] kein Doc"); return
bridge = sc.sticky.get("ebenen_bridge_ref")
if bridge is None:
print("[GESCHOSS-NAV] Ebenen-Bridge nicht aktiv (Panel oeffnen)"); return
try:
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or ""
zs = json.loads(zraw) if zraw else []
if not isinstance(zs, list) or not zs:
print("[GESCHOSS-NAV] keine Zeichnungsebenen"); return
cur_id = doc.Strings.GetValue("dossier_active_id") or ""
idx = -1
for i, z in enumerate(zs):
if isinstance(z, dict) and z.get("id") == cur_id:
idx = i; break
if idx < 0:
idx = 0 # nichts aktiv → starten oben
new_idx = max(0, min(len(zs) - 1, idx + delta))
if new_idx == idx:
print("[GESCHOSS-NAV] schon am {}".format(
"obersten" if delta < 0 else "untersten")); return
target = zs[new_idx]
if not isinstance(target, dict) or not target.get("id"):
print("[GESCHOSS-NAV] Zielebene ungueltig"); return
print("[GESCHOSS-NAV] wechsle zu '{}'".format(target.get("name") or target["id"]))
bridge._set_active_zeichnungsebene(target)
except Exception as ex:
print("[GESCHOSS-NAV]", ex)
# delta=-1 = nach oben (vorheriger Eintrag in der Liste, weil Listen
# typischerweise oberste Ebene oben sind)
_go(-1)
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer View-Mode 'material'.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_view_mode
dossier_view_mode._apply("material")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer View-Mode 'persp3d'.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_view_mode
dossier_view_mode._apply("persp3d")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer View-Mode 'plan'.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_view_mode
dossier_view_mode._apply("plan")
+41 -39
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
"""
ausschnitte.py
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
@@ -147,9 +149,9 @@ def _apply_camera(vp, cam):
Rhino.RhinoApp.RunScript(
"_-Zoom _Factor {:.6f} _Enter".format(factor), False)
except Exception as ex:
print("[AUSSCHNITTE] Frustum-Apply:", ex)
print("[VIEWPORTS] Frustum-Apply:", ex)
except Exception as ex:
print("[AUSSCHNITTE] Camera-Apply:", ex)
print("[VIEWPORTS] Camera-Apply:", ex)
def _capture_layers(doc):
@@ -241,7 +243,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
Liefert True bei Erfolg."""
snap = next((s for s in _load_snapshots(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] apply_to_detail: snap nicht gefunden", snap_id)
print("[VIEWPORTS] apply_to_detail: snap not found", snap_id)
return False
# Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive)
page_view = None
@@ -255,14 +257,14 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
except Exception:
continue
except Exception as ex:
print("[AUSSCHNITTE] page-view-suche:", ex)
print("[VIEWPORTS] page-view-suche:", ex)
# Detail muss aktiv sein, damit Kamera-Aenderungen anschlagen
was_active = False
try: was_active = detail.IsActive
except Exception: pass
if page_view is not None and not was_active:
try: page_view.SetActiveDetail(detail.Id)
except Exception as ex: print("[AUSSCHNITTE] SetActiveDetail:", ex)
except Exception as ex: print("[VIEWPORTS] SetActiveDetail:", ex)
# Kamera + Layer + Name
vp = detail.Viewport
_apply_camera(vp, snap.get("camera"))
@@ -272,7 +274,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
if new_name and vp.Name != new_name:
vp.Name = new_name
except Exception as ex:
print("[AUSSCHNITTE] Detail-Rename:", ex)
print("[VIEWPORTS] Detail-Rename:", ex)
# Massstab
ratio = _parse_scale(snap.get("scale", ""))
if ratio is not None:
@@ -298,7 +300,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
(page_view or doc.Views).Redraw()
except Exception:
doc.Views.Redraw()
print("[AUSSCHNITTE] '{}' auf Detail {} angewendet".format(snap.get("name"), detail.Id))
print("[VIEWPORTS] '{}' auf Detail {} applied".format(snap.get("name"), detail.Id))
return True
@@ -448,7 +450,7 @@ class AusschnittBridge(panel_base.BaseBridge):
snaps.insert(idx + 1, copy)
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' dupliziert".format(src.get("name")))
print("[VIEWPORTS] '{}' dupliziert".format(src.get("name")))
def _set_field(self, snap_id, field, value):
if not snap_id: return
@@ -464,7 +466,7 @@ class AusschnittBridge(panel_base.BaseBridge):
def _capture(self, doc, name, existing_id=None, prior_scale=""):
view = doc.Views.ActiveView
if view is None:
print("[AUSSCHNITTE] Keine aktive View")
print("[VIEWPORTS] Keine aktive View")
return None
vp = view.ActiveViewport
# Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel-
@@ -481,7 +483,7 @@ class AusschnittBridge(panel_base.BaseBridge):
else:
scale_str = "1:{:.1f}".format(ratio)
except Exception as ex:
print("[AUSSCHNITTE] Live-Skala lesen:", ex)
print("[VIEWPORTS] Live-Skala lesen:", ex)
# Fallback: wenn kein Massstab gepinnt war, die aus dem Frustum
# berechnete Live-Skala speichern. So bleibt das Massstab-Dropdown
# nach Restore konsistent (auch wenn der eigentliche Zoom-Restore
@@ -493,7 +495,7 @@ class AusschnittBridge(panel_base.BaseBridge):
if live is not None and live > 0:
scale_str = "1:{:.0f}".format(live) if live >= 10 else "1:{:.1f}".format(live)
except Exception as ex:
print("[AUSSCHNITTE] Live-Skala (Fallback):", ex)
print("[VIEWPORTS] Live-Skala (Fallback):", ex)
if not scale_str and prior_scale:
scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben
# Darstellungs-Override aus dem aktuellen Doc-Setting uebernehmen.
@@ -521,7 +523,7 @@ class AusschnittBridge(panel_base.BaseBridge):
snaps.append(snap)
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' gespeichert".format(name))
print("[VIEWPORTS] '{}' gespeichert".format(name))
def _update(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
@@ -538,7 +540,7 @@ class AusschnittBridge(panel_base.BaseBridge):
break
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' aktualisiert".format(target.get("name")))
print("[VIEWPORTS] '{}' aktualisiert".format(target.get("name")))
def _restore(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
@@ -554,16 +556,16 @@ class AusschnittBridge(panel_base.BaseBridge):
kombi = (snap.get("layerCombination") or "").strip()
if kombi:
try:
import rhinopanel
import layers_panel as rhinopanel
rhinopanel.apply_layer_preset_by_name(doc, kombi)
except Exception as ex:
print("[AUSSCHNITTE] kombi-apply '{}':".format(kombi), ex)
print("[VIEWPORTS] kombi-apply '{}':".format(kombi), ex)
_apply_layers_global(doc, snap.get("layers", []))
else:
_apply_layers_global(doc, snap.get("layers", []))
# Eigene Sichtbarkeit → active_comb_name clearen
try:
import rhinopanel
import layers_panel as rhinopanel
rhinopanel.set_active_comb_name(doc, None)
rhinopanel._notify_oberleiste_combs()
except Exception: pass
@@ -582,8 +584,8 @@ class AusschnittBridge(panel_base.BaseBridge):
if b is not None: b._send_state(force=True)
except Exception: pass
except Exception as ex:
print("[AUSSCHNITTE] darstellung apply:", ex)
# Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat.
print("[VIEWPORTS] darstellung apply:", ex)
# Overrides: nur anwenden wenn das Snap "applyOverrides" set hat.
# Sonst bleibt der aktuelle User-Override-State unangetastet.
if snap.get("applyOverrides"):
try:
@@ -598,7 +600,7 @@ class AusschnittBridge(panel_base.BaseBridge):
b._send_state(force=True)
except Exception: pass
except Exception as ex:
print("[AUSSCHNITTE] overrides-apply:", ex)
print("[VIEWPORTS] overrides-apply:", ex)
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
@@ -608,8 +610,8 @@ class AusschnittBridge(panel_base.BaseBridge):
if new_name and vp.Name != new_name:
vp.Name = new_name
except Exception as ex:
print("[AUSSCHNITTE] Rename:", ex)
# Gespeicherten Massstab anwenden (z.B. "1:50") — falls vorhanden und
print("[VIEWPORTS] Rename:", ex)
# Gespeicherten Massstab anwenden (z.B. "1:50") — falls present und
# Viewport parallel ist (in Perspective ignoriert massstab._apply_scale).
try:
scale_str = (snap.get("scale") or "").strip()
@@ -619,7 +621,7 @@ class AusschnittBridge(panel_base.BaseBridge):
_, model_v = ratio # (page=1, model=N) -> N
import massstab
massstab._apply_scale(doc, vp, float(model_v))
print("[AUSSCHNITTE] Massstab gesetzt auf 1:{} (applied={})".format(
print("[VIEWPORTS] Massstab set auf 1:{} (applied={})".format(
model_v, massstab.get_applied_scale_ratio()))
# Andere Panels (Massstab, Oberleiste) sofort ueber den
# neuen appliedScale informieren — sonst zeigt das Dropdown
@@ -628,15 +630,15 @@ class AusschnittBridge(panel_base.BaseBridge):
for key in ("massstab_bridge", "oberleiste_bridge"):
try:
b = sc.sticky.get(key)
print("[AUSSCHNITTE] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING"))
print("[VIEWPORTS] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING"))
if b is not None:
b._send_state(force=True)
except Exception as e:
print("[AUSSCHNITTE] force-send {} failed: {}".format(key, e))
print("[VIEWPORTS] force-send {} failed: {}".format(key, e))
except Exception as ex:
print("[AUSSCHNITTE] Massstab-Restore:", ex)
print("[VIEWPORTS] Massstab-Restore:", ex)
view.Redraw()
print("[AUSSCHNITTE] '{}' wiederhergestellt".format(snap.get("name")))
print("[VIEWPORTS] '{}' wiederhergestellt".format(snap.get("name")))
def _apply_to_detail(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
@@ -651,9 +653,9 @@ class AusschnittBridge(panel_base.BaseBridge):
detail = d
break
except Exception as ex:
print("[AUSSCHNITTE] Active-Detail-Suche:", ex)
print("[VIEWPORTS] Active-Detail-Suche:", ex)
if detail is None:
print("[AUSSCHNITTE] Kein Detail ausgewaehlt — bitte:")
print("[VIEWPORTS] Kein Detail ausgewaehlt — bitte:")
print(" 1) ins Layout wechseln")
print(" 2) Detail-Rahmen einmal anklicken (so dass er hervorgehoben ist)")
print(" 3) erneut 'Auf Detail anwenden' waehlen")
@@ -666,7 +668,7 @@ class AusschnittBridge(panel_base.BaseBridge):
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] Snap nicht gefunden:", snap_id)
print("[VIEWPORTS] Snap not found:", snap_id)
return
snap_by_id = {}
for ls in (snap.get("layers") or []):
@@ -712,7 +714,7 @@ class AusschnittBridge(panel_base.BaseBridge):
target["layers"] = new_list
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
print("[VIEWPORTS] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
def _save_preset(self, name, layers):
if not name: return
@@ -736,7 +738,7 @@ class AusschnittBridge(panel_base.BaseBridge):
presets.append({"name": name, "layers": clean})
self._store_presets(doc, presets)
self._send_list()
print("[AUSSCHNITTE] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
print("[VIEWPORTS] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
def _delete_preset(self, name):
if not name: return
@@ -771,7 +773,7 @@ class AusschnittBridge(panel_base.BaseBridge):
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id)
print("[VIEWPORTS] open_settings: snap not found", snap_id)
return
outer = self
bridge_holder = {"form": None, "id": snap_id}
@@ -783,22 +785,22 @@ class AusschnittBridge(panel_base.BaseBridge):
# Listen fuer Dropdowns
display_modes = []
try:
import oberleiste
import toolbar as oberleiste
display_modes = oberleiste._list_display_modes()
except Exception as ex:
print("[AUSSCHNITTE] display_modes:", ex)
print("[VIEWPORTS] display_modes:", ex)
overrides_presets = []
try:
import overrides
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
except Exception as ex:
print("[AUSSCHNITTE] overrides_presets:", ex)
print("[VIEWPORTS] overrides_presets:", ex)
layer_kombis = []
try:
import rhinopanel
import layers_panel as rhinopanel
layer_kombis = rhinopanel.list_layer_preset_names(d)
except Exception as ex:
print("[AUSSCHNITTE] layer_kombis:", ex)
print("[VIEWPORTS] layer_kombis:", ex)
cam = sn.get("camera") or {}
return {
"snap": {
@@ -824,7 +826,7 @@ class AusschnittBridge(panel_base.BaseBridge):
sid = bridge_holder["id"]
target = next((s for s in snaps if s.get("id") == sid), None)
if target is None:
print("[AUSSCHNITTE] persist settings: snap weg"); return
print("[VIEWPORTS] persist settings: snap weg"); return
# Massstab
sc_val = (settings.get("scale") or "").strip()
target["scale"] = sc_val
@@ -846,7 +848,7 @@ class AusschnittBridge(panel_base.BaseBridge):
target["darstellung"] = darst if darst in ("einfach", "standard", "detail") else ""
outer._store(d, snaps)
outer._send_list()
print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name")))
print("[VIEWPORTS] Settings fuer '{}' aktualisiert".format(target.get("name")))
class _AusschnittSettingsBridge(panel_base.BaseBridge):
def __init__(self):
+79
View File
@@ -0,0 +1,79 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
begin_cmd_hook.py
Hook auf Rhino.Commands.Command.BeginCommand. Wenn der User ein Drawing-
Command startet (Line, Polyline, Rectangle, Circle etc.), oeffnen wir
automatisch das DOSSIER-Gestaltung-Panel und bringen es in den Vordergrund.
Idempotent Re-Install nach _reset_panels deregistriert alten Handler.
"""
import Rhino
import scriptcontext as sc
import System
# Commands bei denen wir Gestaltung-Panel fokussieren.
# CommandEnglishName ohne Underscore-Prefix.
_DRAWING_COMMANDS = {
"Line", "Polyline", "Curve", "InterpCrv",
"Arc", "Circle", "Ellipse",
"Rectangle", "Polygon",
"Hatch", "Text",
"Point", "Points",
"InfiniteLine",
}
_GESTALTUNG_PANEL_GUID = "4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829"
_HANDLER_KEY = "_dossier_begin_cmd_handler"
_VERBOSE_KEY = "_dossier_begin_cmd_verbose"
def _on_begin_command(sender, e):
try:
cmd = getattr(e, "CommandEnglishName", "") or ""
if sc.sticky.get(_VERBOSE_KEY):
print("[CMD-HOOK] 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("[CMD-HOOK] Gestaltung-Panel opened/focused")
except Exception as ex:
print("[CMD-HOOK] OpenPanel:", ex)
try:
Rhino.RhinoApp.RunScript(
'-_ShowPanel "DOSSIER Gestaltung"', False)
except Exception: pass
except Exception as ex:
print("[CMD-HOOK] 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("[CMD-HOOK] Hook installed (verbose={})".format(bool(verbose)))
except Exception as ex:
print("[CMD-HOOK] install:", ex)
def set_verbose(flag):
sc.sticky[_VERBOSE_KEY] = bool(flag)
if __name__ == "__main__":
install(verbose=True)
+2
View File
@@ -1,4 +1,6 @@
#! python 3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
clean.py
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
+3 -1
View File
@@ -1,4 +1,6 @@
#! python 3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
clean_layers.py
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
@@ -48,4 +50,4 @@ else:
print("[clean_layers] Nichts geloescht (schon sauber?)")
if skip:
print("[clean_layers] Uebersprungen (Objekte drauf): {}".format(", ".join(skip)))
print("[clean_layers] Panel-Sticky zurueckgesetzt")
print("[clean_layers] Panel-Sticky zurueckset")
+161
View File
@@ -0,0 +1,161 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
curve_vertex_dots.py
Display-only Vertex-Dots fuer GENERISCHE Curves (Polylinen, Linien,
Rectangles, NurbsCurves etc). Zeigt gruene Punkte an allen Vertices
selektierter Curves hilft beim Visuell-Finden von Grip-Positionen
wenn die Curve eine Fuellung (Hatch) hat und schwer per Klick auf
einen einzelnen Vertex zu treffen ist.
Display-only kein eigener Drag-Handler. User editiert Vertices via
Rhino's native _Grips (Punkte sichtbar machen + Standard-Drag) oder
direktes Object-Snapping waehrend Drag.
Skipt dossier-managed Curves (wand_axis, treppe_axis, schnitt_axis,
wand_outline, wand_centerline, raum_polylinie etc) die haben ihre
eigenen Conduits oder duerfen nicht via Vertex editiert werden.
"""
import Rhino
import Rhino.Display as rd
import Rhino.Geometry as rg
import scriptcontext as sc
import System.Drawing as SD
# --- Konstanten ------------------------------------------------------------
_MARKER_RADIUS_PX = 6
_MARKER_FILL = SD.Color.FromArgb(200, 95, 168, 150) # accent-gruen
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
# Dossier-managed Element-Types die NICHT mit generic dots versehen werden
# (= haben eigene Conduits oder sind nicht editierbar via Vertex-Click).
_SKIP_TYPES = {
"wand_axis", "wand_centerline", "wand_outline", "wand_volume",
"treppe_axis", "treppe_outline", "treppe_volume",
"schnitt_axis", "schnitt_outline",
"raum_polylinie", "raum_stempel",
"ausschnitt_polylinie",
"decke_polylinie", "decke_volume",
"dach_polylinie", "dach_volume",
}
# --- Helpers --------------------------------------------------------------
def _is_dossier_managed(obj):
"""True wenn obj ein dossier-managed Element ist (= Skip)."""
if obj is None or obj.IsDeleted: return True
try:
t = obj.Attributes.GetUserString("dossier_element_type") or ""
return t in _SKIP_TYPES
except Exception:
return False
def _curve_vertices(curve):
"""Liefert Liste von rg.Point3d fuer alle relevanten Vertices der
Curve. Verschiedene Curve-Types haben verschiedene Vertices:
- LineCurve: 2 Endpunkte
- PolylineCurve: alle Polyline-Punkte (deduplizert wenn closed)
- PolyCurve: rekursiv Segmente
- NurbsCurve/sonst: Start + End (control points nicht zu viele)"""
pts = []
if curve is None: return pts
try:
if isinstance(curve, rg.PolylineCurve):
ok, pline = curve.TryGetPolyline()
if ok and pline is not None:
n = pline.Count
# Deduplizieren wenn closed (letzter Punkt = erster)
last = n
try:
if (n >= 2
and pline[0].DistanceTo(pline[n - 1]) < 1e-6):
last = n - 1
except Exception: pass
for i in range(last):
pts.append(rg.Point3d(pline[i]))
return pts
if isinstance(curve, rg.LineCurve):
pts.append(curve.PointAtStart)
pts.append(curve.PointAtEnd)
return pts
if isinstance(curve, rg.PolyCurve):
for i in range(curve.SegmentCount):
seg = curve.SegmentCurve(i)
if seg is None: continue
# Nur Start jedes Segments (End ist Start des naechsten)
pts.append(seg.PointAtStart)
# Letztes Segment-End anhaengen
try:
pts.append(curve.PointAtEnd)
except Exception: pass
return pts
# Generic Curve: nur Start + End
try:
pts.append(curve.PointAtStart)
pts.append(curve.PointAtEnd)
except Exception: pass
except Exception:
pass
return pts
# --- Conduit -------------------------------------------------------------
class _VertexDotConduit(rd.DisplayConduit):
"""Zeichnet bei jeder selektierten generischen Curve gruene Punkte
an allen Vertices."""
def DrawForeground(self, e):
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
try:
sel = list(doc.Objects.GetSelectedObjects(False, False))
except Exception: return
seen_curve_ids = set()
for obj in sel:
if _is_dossier_managed(obj): continue
try:
cid = str(obj.Id)
except Exception: continue
if cid in seen_curve_ids: continue
seen_curve_ids.add(cid)
geom = obj.Geometry
if not isinstance(geom, rg.Curve): continue
for pt in _curve_vertices(geom):
try:
e.Display.DrawPoint(
pt, rd.PointStyle.RoundControlPoint,
_MARKER_RADIUS_PX, _MARKER_FILL)
except Exception:
try: e.Display.DrawDot(
pt, "·", _MARKER_FILL, _MARKER_BORDER)
except Exception: pass
except Exception as ex:
print("[CURVE-DOTS] DrawForeground:", ex)
# --- Install -------------------------------------------------------------
_STICKY_CONDUIT = "_dossier_curve_vertex_dots_conduit"
def install_curve_vertex_dots():
"""Idempotent: alten Conduit disable, neuen installieren."""
try:
old = sc.sticky.get(_STICKY_CONDUIT)
if old is not None:
try: old.Enabled = False
except Exception: pass
conduit = _VertexDotConduit()
conduit.Enabled = True
sc.sticky[_STICKY_CONDUIT] = conduit
print("[CURVE-DOTS] Vertex dot conduit active")
except Exception as ex:
print("[CURVE-DOTS] install:", ex)
+22 -7
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
"""
dimensionen.py
DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild.
@@ -181,7 +183,7 @@ def _apply_xform(doc, objs, xform):
if doc.Objects.Transform(obj.Id, xform, True):
n += 1
except Exception as ex:
print("[DIMENSIONEN] Transform-Fehler:", ex)
print("[DIMENSIONS] Transform-Fehler:", ex)
return n
@@ -199,14 +201,14 @@ class _UndoRecord(object):
try:
self.serial = self.doc.BeginUndoRecord(self.label)
except Exception as ex:
print("[DIMENSIONEN] BeginUndoRecord:", ex)
print("[DIMENSIONS] BeginUndoRecord:", ex)
self.serial = 0
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.serial:
try: self.doc.EndUndoRecord(self.serial)
except Exception as ex:
print("[DIMENSIONEN] EndUndoRecord:", ex)
print("[DIMENSIONS] EndUndoRecord:", ex)
return False # exceptions propagieren
@@ -225,7 +227,7 @@ def _scale_around_point(doc, objs, plane, ref_world, sx, sy, sz):
ausgerichtet an plane."""
if sx == 1 and sy == 1 and sz == 1: return
if sx <= 0 or sy <= 0 or sz <= 0:
print("[DIMENSIONEN] Ungueltige Skalierungsfaktoren:", sx, sy, sz)
print("[DIMENSIONS] Ungueltige Skalierungsfaktoren:", sx, sy, sz)
return
p = rg.Plane(plane)
p.Origin = ref_world
@@ -584,15 +586,28 @@ def _install_listeners(bridge):
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
# Tick zwischen den einzelnen Deletes. CommandEnd refresht.
if sc.sticky.get("_dossier_bulk_op_active"): return
# Waehrend Gumball/Move/Rotate: nicht pollen. Geometrie ist gerade
# in Transit (Live-Replace pro Frame), Werte wuerden mit ~5/s
# zwischen Frames flickern. CommandEnd triggert finalen _send_state.
if sc.sticky.get("_dossier_user_transform_active"): return
b = sc.sticky.get("dimensionen_bridge")
if b is not None:
try: b.tick_idle()
except Exception as ex: print("[DIMENSIONEN] idle:", ex)
except Exception as ex: print("[DIMENSIONS] idle:", ex)
def on_select(s, e):
# Swisstopo-Import feuert tausende Selection-Events → bail.
if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get("_dossier_bulk_op_active"): return
# Waehrend elemente.py's Partnership-Cascade (Klick auf Wand/Treppe
# → 30+ Partner selektiert in einem Rutsch): NICHT pro Event ein
# _send_state feuern. Sonst rauscht das Dimensionen-Panel mit 30+
# Re-Renders durch und die Werte/Auswahl-Anzeige flickert wild.
# Der Idle-Tick holt die finale Selektion eh ~5/s nach.
if sc.sticky.get("_elemente_select_busy"): return
# Waehrend User-Transform (Gumball/Move/Rotate): kein Re-Send, sonst
# rauscht Replace-Storm durch und der Frontend-State zappelt.
if sc.sticky.get("_dossier_user_transform_active"): return
b = sc.sticky.get("dimensionen_bridge")
if b is not None:
try: b._send_state(force=True)
@@ -604,9 +619,9 @@ def _install_listeners(bridge):
Rhino.RhinoDoc.DeselectObjects += on_select
Rhino.RhinoDoc.DeselectAllObjects += on_select
except Exception as ex:
print("[DIMENSIONEN] select-events:", ex)
print("[DIMENSIONS] select-events:", ex)
sc.sticky[flag] = True
print("[DIMENSIONEN] Listener aktiv (Idle + SelectObjects)")
print("[DIMENSIONS] Listener active (Idle + SelectObjects)")
def _bridge_factory():
+6727 -513
View File
File diff suppressed because it is too large Load Diff
+2
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
"""
elemente_properties.py
Properties-Satellite-Window. Zeigt die Property-Forms (WallProperties,
+118 -1
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
"""
elemente_uebersicht.py
BIM-artiger Project Browser: alle Smart-Elemente in einem Tree
@@ -29,6 +31,7 @@ _KIND_MAP = {
"stuetze_point": "stuetze",
"traeger_axis": "traeger",
"raum_outline": "raum",
"stempel": "stempel",
"decke_aussparung_outline": "aussparung",
"oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest
}
@@ -114,7 +117,39 @@ def _build_overview(doc):
out_geschosse.append({
"id": "__keingeschoss__", "name": "(kein Geschoss)", "okff": None,
})
return {"geschosse": out_geschosse, "items": items}
# SIA-416 Bilanz pro Geschoss: aggregiert alle raum_outline-Flaechen
# nach raum_sia-Klassifikation. Räume ohne SIA-Tag landen in "ohne".
# NF = HNF + NNF (Nutzflaeche). Wird im Frontend als Tabelle gerendert.
sia_bilanz = {} # {geschossId: {hnf, nnf, vf, ff, ohne, nf, total, count}}
for obj in doc.Objects:
meta = _elm._read_meta(obj)
if meta is None: continue
if meta.get("type") != "raum_outline": continue
try:
area, _, _ = _elm._raum_amp(obj.Geometry)
except Exception: continue
if not area or area <= 0: continue
g_id = meta.get("geschoss") or "__keingeschoss__"
sia = (meta.get("raum_sia") or "").lower()
if sia not in ("hnf", "nnf", "vf", "ff", "gf", "agf"):
sia = "ohne"
b = sia_bilanz.setdefault(g_id, {
"hnf": 0.0, "nnf": 0.0, "vf": 0.0, "ff": 0.0,
"gf": 0.0, "agf": 0.0,
"ohne": 0.0, "count": 0,
})
b[sia] += float(area)
b["count"] += 1
# NF/NGF/Total ableiten
for b in sia_bilanz.values():
b["nf"] = b["hnf"] + b["nnf"]
b["ngf"] = b["nf"] + b["vf"] + b["ff"]
b["total"] = (b["hnf"] + b["nnf"] + b["vf"] + b["ff"]
+ b["gf"] + b["agf"] + b["ohne"])
return {"geschosse": out_geschosse, "items": items,
"siaBilanz": sia_bilanz}
class ElementeUebersichtBridge(panel_base.BaseBridge):
@@ -173,6 +208,88 @@ class ElementeUebersichtBridge(panel_base.BaseBridge):
print("[UEBERSICHT] zoom:", ex)
except Exception as ex:
print("[UEBERSICHT] zoom find:", ex)
elif t == "EXPORT_BILANZ":
self._export_bilanz()
def _export_bilanz(self):
"""Exportiert SIA-416 Bilanz als CSV (Excel-kompatibel: Semikolon-
Separator + UTF-8 BOM + Komma als Dezimaltrenner). Wide-Format:
eine Spalte pro Geschoss + Total-Spalte, Zeilen pro Kategorie.
"""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
# Geschoss-Liste (geordnet) + Total am Ende
geschosse = _elm._load_geschosse(doc) or []
gs_list = [g for g in geschosse
if isinstance(g, dict) and g.get("isGeschoss")]
# Bilanz pro Geschoss + Total via compute_sia_bilanz
per_gid = {} # gid → bilanz dict
for g in gs_list:
per_gid[g["id"]] = _elm.compute_sia_bilanz(
doc, "geschoss:" + g["id"])
total = _elm.compute_sia_bilanz(doc, "total")
# SaveFileDialog
try:
from Rhino.UI import SaveFileDialog
sfd = SaveFileDialog()
sfd.DefaultExt = "csv"
sfd.Filter = "CSV (*.csv)|*.csv"
sfd.FileName = "sia_bilanz.csv"
ok = False
try: ok = sfd.ShowSaveDialog()
except Exception:
try: ok = sfd.ShowDialog()
except Exception: ok = False
if not ok:
print("[UEBERSICHT] Bilanz-Export abgebrochen"); return
path = sfd.FileName
except Exception as ex:
print("[UEBERSICHT] SaveFileDialog:", ex); return
# Zeilen-Definition: (Label, Bilanz-Key, ist_personen?)
rows = [
("HNF (m²)", "hnf", False),
("NNF (m²)", "nnf", False),
("NF (m²)", "nf", False),
("VF (m²)", "vf", False),
("FF (m²)", "ff", False),
("NGF (m²)", "ngf", False),
("GF (m²)", "gf", False),
("AGF (m²)", "agf", False),
("Räume", "count", True),
("Personen", "personen", True),
]
def _fmt(val, is_count):
if val is None: return ""
if is_count: return str(int(val))
return "{:.2f}".format(float(val)).replace(".", ",")
def _esc(s):
s = str(s)
if ";" in s or '"' in s or "\n" in s:
return '"' + s.replace('"', '""') + '"'
return s
try:
import io
with io.open(path, "w", encoding="utf-8-sig", newline="") as f:
# Header — Kategorie + Geschoss-Namen + Total
header = ["Kategorie"]
for g in gs_list: header.append(_esc(g.get("name") or "?"))
header.append("Total")
f.write(";".join(header) + "\n")
for label, key, is_count in rows:
line = [_esc(label)]
for g in gs_list:
b = per_gid.get(g["id"], {})
line.append(_fmt(b.get(key, 0), is_count))
line.append(_fmt(total.get(key, 0), is_count))
f.write(";".join(line) + "\n")
print("[UEBERSICHT] SIA-Bilanz exportiert: {} ({} Geschosse + Total)".format(
path, len(gs_list)))
except Exception as ex:
print("[UEBERSICHT] CSV schreiben:", ex)
def open_as_window():
+3 -1
View File
@@ -1,4 +1,6 @@
#! python 3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
inspect_section.py
Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log,
@@ -54,7 +56,7 @@ for n in dir(layer):
except Exception as ex:
print(" layer.{} -> err: {}".format(n, ex))
# layer.SectionStyle dumpen wenn vorhanden
# layer.SectionStyle dumpen wenn present
try:
if hasattr(layer, "SectionStyle"):
dump("layer.SectionStyle", layer.SectionStyle)
+2
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
"""
kamera.py
Kamera-Panel: liest/setzt Viewport-Kamera (Position, Target, Projektion,
+41 -35
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
"""
layer_builder.py
Layer-Struktur:
@@ -80,7 +82,7 @@ def _find_hatch_pattern_index(doc, name):
if hp.Name and hp.Name.strip().lower() == target:
return i
except Exception as ex:
print("[EBENEN] hatch lookup:", ex)
print("[LAYERS] hatch lookup:", ex)
return -1
@@ -100,7 +102,7 @@ def _find_linetype_index(doc, name):
def _try_set(obj, prop_names, value):
"""Versucht den Wert auf das erste vorhandene Property zu setzen.
"""Versucht den Wert auf das erste presente Property zu setzen.
Liefert den Property-Namen bei Erfolg, sonst None."""
if isinstance(prop_names, str):
prop_names = (prop_names,)
@@ -152,11 +154,14 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
try:
SS = Rhino.DocObjects.SectionStyle
except Exception as ex:
print("[EBENEN] SectionStyle-Klasse nicht da:", ex); return
print("[LAYERS] SectionStyle-Klasse nicht da:", ex); return
pat = (section_cfg.get("hatchPattern") or "None").strip()
show = bool(section_cfg.get("boundaryShow", True))
diag = "[SS:{}]".format(layer.Name if layer else "?")
# DEBUG: zeigt was an section_cfg ankommt (zur Diagnose des Hatch-Bugs)
print(diag, "section_cfg.hatchPattern='{}' scale={} rot={}".format(
pat, section_cfg.get("hatchScale"), section_cfg.get("hatchRotation")))
# Wenn weder Hatch noch Boundary → Custom-Style entfernen
if pat == "None" and not show:
@@ -165,7 +170,7 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
layer.RemoveCustomSectionStyle()
print(diag, "removed (kein Hatch + kein Boundary)")
except Exception as ex:
print(diag, "remove fehlgeschlagen:", ex)
print(diag, "remove failed:", ex)
return
style = SS()
@@ -194,17 +199,20 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
rot_deg = float(section_cfg.get("hatchRotation") or 0)
_try_set(style, ("HatchRotation", "HatchAngle"), math.radians(rot_deg))
# Hatch-Color: explizit ColorFromObject setzen damit der eigene Wert greift
# Hatch-Color: explizit setzen — wenn User keine Override-Farbe angegeben
# hat, nehmen wir die Layer-Farbe als Default (sonst rendert Rhino sonst
# schwarz). Section-Style hat keine ByLayer-Option, also Farbwert
# explizit reinkopieren.
hatch_color = section_cfg.get("hatchColor")
if hatch_color:
col = _color(hatch_color)
set_color = _try_set(style, ("HatchColor", "FillColor"), col)
# Source auf "FromObject" — sonst nutzt Rhino den Layer-Color
src_from_object = _enum_int(
(("DocObjects", "ObjectColorSource"), "ColorFromObject"))
if src_from_object is not None:
_try_set(style, ("HatchColorSource", "FillColorSource"), src_from_object)
print(diag, "HatchColor via {}".format(set_color))
elif layer_color is not None:
col = _color(layer_color) if isinstance(layer_color, str) else layer_color
else:
col = None
if col is not None:
set_color = _try_set(style, ("HatchPatternColor", "HatchColor", "FillColor"), col)
print(diag, "HatchColor via {} (default=layer)".format(set_color))
# Background (viewport=0/transparent vs object=1)
bg = section_cfg.get("background")
@@ -226,20 +234,18 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
print(diag, "BoundaryVisible={} via {}".format(show, set_show))
if show:
# Boundary-Color: setze Color + Source auf FromObject
# Boundary-Color: User-Override oder Layer-Farbe als Default
bc = section_cfg.get("boundaryColor")
if bc:
col = _color(bc)
bcol = _color(bc)
elif layer_color is not None:
bcol = _color(layer_color) if isinstance(layer_color, str) else layer_color
else:
bcol = None
if bcol is not None:
set_to = _try_set(style,
("BoundaryColor", "OutlineColor", "EdgeColor"), col)
src_from_object = _enum_int(
(("DocObjects", "ObjectColorSource"), "ColorFromObject"))
if src_from_object is not None:
_try_set(style,
("BoundaryColorSource", "OutlineColorSource",
"EdgeColorSource"),
src_from_object)
print(diag, "BoundaryColor={} via {}".format(bc, set_to))
("BoundaryColor", "OutlineColor", "EdgeColor"), bcol)
print(diag, "BoundaryColor via {} (default=layer)".format(set_to))
# Width-Scale auf PlotWeight uebertragen (RW8 hat keine WidthScale direkt;
# alternative Property-Namen probieren)
@@ -335,7 +341,7 @@ def _build_ebene_layer(doc, parent_id, e, diag_prefix=""):
_apply_section_style(doc, doc.Layers[sub_idx],
e.get("section"), e.get("color"))
except Exception as ex:
print("[EBENEN] section-style apply ({}{}): {}".format(
print("[LAYERS] section-style apply ({}{}): {}".format(
diag_prefix, sub_name, ex))
return sub_idx
@@ -378,7 +384,7 @@ def build_layers(doc, zeichnungsebenen, ebenen):
diag_prefix=z_name + "/")
doc.Views.Redraw()
n_total = len(walk_ebenen(ebenen))
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert (inkl. {} Sub)".format(
print("[LAYERS] {} drawing levels x {} layers updated (incl. {} sub)".format(
len(zeichnungsebenen), len(ebenen), max(0, n_total - len(ebenen))))
@@ -427,7 +433,7 @@ def set_ebene_locked(doc, code, locked):
def delete_ebene(doc, code, move_to=None):
"""
Loescht alle Sublayer mit dem gegebenen Code in allen Zeichnungsebenen.
Falls move_to gesetzt: verschiebt vorher alle Objekte zum Sublayer
Falls move_to set: verschiebt vorher alle Objekte zum Sublayer
mit move_to-Code unter dem selben Parent. Sonst: loescht Objekte mit.
"""
if not code:
@@ -481,10 +487,10 @@ def delete_ebene(doc, code, move_to=None):
if doc.Layers.Delete(from_idx, True):
deleted_layers += 1
except Exception as ex:
print("[EBENEN] Layer-Delete:", ex)
print("[LAYERS] Layer-Delete:", ex)
doc.Views.Redraw()
print("[EBENEN] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format(
print("[LAYERS] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format(
code, deleted_layers, moved, deleted_objs))
@@ -526,7 +532,7 @@ def update_clipping_plane(doc, active_z, enabled):
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
# IMMER vorhandene Plane loeschen — bei Re-Enable wollen wir frische
# IMMER presente Plane loeschen — bei Re-Enable wollen wir frische
# vp_ids (alte koennten leer/falsch sein, dann clippt das Replace zwar
# die Geometrie aber keinen Viewport).
if existing is not None:
@@ -534,15 +540,15 @@ def update_clipping_plane(doc, active_z, enabled):
doc.Objects.Delete(existing.Id, True)
print("[CLIP] alte Plane geloescht")
except Exception as ex:
print("[CLIP] Delete fehlgeschlagen:", ex)
print("[CLIP] Delete failed:", ex)
if (not enabled) or (not is_geschoss):
print("[CLIP] disabled — fertig (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
print("[CLIP] disabled — done (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
doc.Views.Redraw()
return
# dict.get(k, default) liefert default NUR wenn Key fehlt — bei
# Key-vorhanden-aber-None gibt's None zurueck. float(None) crasht.
# Key-present-aber-None gibt's None zurueck. float(None) crasht.
# Daher explizit None-faangen:
okff_raw = active_z.get("okff")
sh_raw = active_z.get("schnitthoehe")
@@ -650,7 +656,7 @@ def cleanup_default_layers(doc):
except Exception:
pass
if deleted:
print("[EBENEN] Default-Layer entfernt: {}".format(", ".join(deleted)))
print("[LAYERS] Default layer removed: {}".format(", ".join(deleted)))
def _find_sublayer_by_code_recursive(doc, parent_id, code):
@@ -676,14 +682,14 @@ def set_active_sublayer(doc, zeichnungsebene_id, code):
7101_Strassen liegt zwei Ebenen tief)."""
parent_idx = _find_top_by_id(doc, zeichnungsebene_id)
if parent_idx < 0:
print("[EBENEN] Parent-Layer fuer Zeichnungsebene {} nicht gefunden".format(zeichnungsebene_id))
print("[LAYERS] Parent-Layer fuer Zeichnungsebene {} not found".format(zeichnungsebene_id))
return
parent_id = doc.Layers[parent_idx].Id
sub_idx = _find_sublayer_by_code_recursive(doc, parent_id, code)
if sub_idx >= 0:
doc.Layers.SetCurrentLayerIndex(sub_idx, True)
else:
print("[EBENEN] Sublayer mit Code {} unter Parent {} nicht gefunden".format(code, doc.Layers[parent_idx].Name))
print("[LAYERS] Sublayer with code {} under parent {} not found".format(code, doc.Layers[parent_idx].Name))
def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_mode, e_mode):
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -1,9 +1,11 @@
#! python 3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
layouts.py
LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken.
Phase 1 Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail angewendet,
Phase 1 Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail applied,
Re-Sync per Knopf. Live-Link und Masterlayouts kommen spaeter.
"""
import os
@@ -387,7 +389,7 @@ class LayoutsBridge(panel_base.BaseBridge):
try:
page = doc.Views.AddPageView(name, w, h)
if page is None:
print("[LAYOUTS] AddPageView fehlgeschlagen"); return
print("[LAYOUTS] AddPageView failed"); return
print("[LAYOUTS] '{}' angelegt ({}x{})".format(name, w, h))
except Exception as ex:
print("[LAYOUTS] AddPageView Fehler:", ex)
@@ -412,7 +414,7 @@ class LayoutsBridge(panel_base.BaseBridge):
done = True
print("[LAYOUTS] SetPageSize -> {}x{}".format(w, h))
except Exception as ex:
print("[LAYOUTS] SetPageSize fehlgeschlagen:", ex)
print("[LAYOUTS] SetPageSize failed:", ex)
# 2) Fallback: Properties (haengt von Rhino-Version ab)
if not done:
try:
@@ -421,7 +423,7 @@ class LayoutsBridge(panel_base.BaseBridge):
done = True
print("[LAYOUTS] PageWidth/Height-Properties -> {}x{}".format(w, h))
except Exception as ex:
print("[LAYOUTS] Property-Setter fehlgeschlagen:", ex)
print("[LAYOUTS] Property-Setter failed:", ex)
if not done:
print("[LAYOUTS] Konnte Seiten-Groesse nicht setzen — bitte ueber Rhinos Layout-Dialog aendern")
try: page.Redraw()
@@ -513,7 +515,7 @@ class LayoutsBridge(panel_base.BaseBridge):
pdf.Write(path)
print("[LAYOUTS] PDF geschrieben: {} ({} Seite(n))".format(path, n_added))
except Exception as ex:
print("[LAYOUTS] PDF-Export fehlgeschlagen:", ex)
print("[LAYOUTS] PDF-Export failed:", ex)
finally:
# Vorherige View wieder aktivieren
if prev_view is not None:
@@ -536,7 +538,7 @@ class LayoutsBridge(panel_base.BaseBridge):
if doc.Path:
base = os.path.splitext(os.path.basename(doc.Path))[0] + "_Layouts"
dlg.FileName = "{}.pdf".format(base)
# Default-Folder — neben der .3dm wenn vorhanden
# Default-Folder — neben der .3dm wenn present
if doc.Path:
try: dlg.Directory = System.Uri(os.path.dirname(doc.Path))
except Exception: pass
+566 -65
View File
@@ -1,5 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
library.py Dossier-Library (Phase A: lokal, read-only)
@@ -82,28 +84,28 @@ def _write_seed_manifest(path):
"type": "material", "version": 1,
"name": "Beton — Sichtbeton",
"tags": ["beton", "tragwerk", "roh"],
"data": {"color": "#a8a39b", "hatch": "Solid", "scale": 1.0},
"data": {"color": "#a8a39b"},
},
{
"id": "mat-mauerwerk-backstein-v1",
"type": "material", "version": 1,
"name": "Mauerwerk — Backstein",
"tags": ["mauerwerk", "stein"],
"data": {"color": "#a45a3c", "hatch": "Solid", "scale": 1.0},
"data": {"color": "#a45a3c"},
},
{
"id": "mat-daemmung-mineralwolle-v1",
"type": "material", "version": 1,
"name": "Daemmung — Mineralwolle",
"tags": ["daemmung", "weich"],
"data": {"color": "#e8d36b", "hatch": "Solid", "scale": 1.0},
"data": {"color": "#e8d36b"},
},
{
"id": "mat-holz-fichte-v1",
"type": "material", "version": 1,
"name": "Holz — Fichte",
"tags": ["holz", "ausbau"],
"data": {"color": "#c8a06a", "hatch": "Solid", "scale": 1.0},
"data": {"color": "#c8a06a"},
},
{
"id": "sym-nordpfeil-01",
@@ -148,6 +150,459 @@ def load_manifest():
return _empty_manifest()
def save_manifest(manifest):
"""Schreibt das Manifest zurueck zur library.json. Items werden
normalisiert. Returns True/False."""
ensure_library()
path = os.path.join(library_root(), _MANIFEST_FN)
try:
if not isinstance(manifest, dict): manifest = _empty_manifest()
manifest.setdefault("schemaVersion", _SCHEMA_VERSION)
manifest.setdefault("name", "Dossier-Library")
items = manifest.get("items") or []
manifest["items"] = [_normalize_item(x) for x in items
if _normalize_item(x) is not None]
with open(path, "w") as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
return True
except Exception as ex:
print("[LIBRARY] save_manifest:", ex)
return False
def update_item(item_id, patch):
"""Patcht ein Item im Manifest. Returns (ok, new_manifest)."""
m = load_manifest()
items = m.get("items", [])
found = False
for it in items:
if it.get("id") == item_id:
for k, v in (patch or {}).items():
it[k] = v
found = True
break
if not found: return False, m
ok = save_manifest(m)
return ok, load_manifest()
def delete_item(item_id):
"""Loescht ein Item aus dem Manifest. Asset-Files bleiben auf Disk
(User koennte sie noch wollen)."""
m = load_manifest()
items = m.get("items", [])
new_items = [it for it in items if it.get("id") != item_id]
if len(new_items) == len(items): return False, m
m["items"] = new_items
ok = save_manifest(m)
return ok, load_manifest()
def add_item(item):
"""Fuegt ein neues Item zum Manifest hinzu. Returns (ok, new_manifest)."""
norm = _normalize_item(item)
if norm is None: return False, load_manifest()
m = load_manifest()
items = m.get("items", [])
# Dedupe per id
items = [it for it in items if it.get("id") != norm["id"]]
items.append(norm)
m["items"] = items
ok = save_manifest(m)
return ok, load_manifest()
def _previews_dir():
"""Pfad zum previews/-Subfolder. Wird angelegt falls fehlt."""
p = os.path.join(library_root(), "previews")
if not os.path.isdir(p):
try: os.makedirs(p)
except Exception: pass
return p
def _preview_rel_for(asset_rel_or_id):
"""Erzeugt einen relativen Preview-Pfad fuer ein Asset (oder Item-ID).
Liefert z.B. 'previews/<name>.png'. Wir benutzen den Stamm des
.3dm-Files damit Asset + Preview gleichen Namen haben (debuggbar)."""
stem = asset_rel_or_id
if "/" in stem: stem = stem.split("/")[-1]
if "\\" in stem: stem = stem.split("\\")[-1]
if stem.lower().endswith(".3dm"): stem = stem[:-4]
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in stem)
return "previews/" + safe + ".png"
def _capture_thumbnail_of_objects(target_objects, png_abs_path, size=128):
"""Captured einen Top-View der gegebenen Objekte als PNG. Hided
temporaer alle anderen Objekte damit der Background sauber ist.
Returns True/False."""
if Rhino is None: return False
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None or not target_objects: return False
try:
from System.Drawing import Size
import scriptcontext as sc
except Exception:
return False
# IDs der Ziel-Objekte
target_ids = set()
for o in target_objects:
try:
if not o.IsDeleted: target_ids.add(str(o.Id))
except Exception: pass
if not target_ids: return False
# Andere Objekte temporaer ausblenden
hidden_by_us = []
try:
for o in list(doc.Objects):
try:
if o.IsDeleted: continue
if str(o.Id) in target_ids: continue
if o.IsHidden: continue
if not o.IsNormal: continue # bereits hidden/locked → skip
doc.Objects.Hide(o.Id, True)
hidden_by_us.append(o.Id)
except Exception: pass
except Exception: pass
capture_ok = False
try:
view = doc.Views.ActiveView
if view is None:
return False
vp = view.ActiveViewport
# Viewport-State sichern damit User nichts verliert
saved_target = vp.CameraTarget
saved_loc = vp.CameraLocation
saved_proj = vp.IsParallelProjection
try:
# Auf Top-Parallel wechseln + Zoom auf Ziel
vp.SetProjection(Rhino.Display.DefinedViewportProjection.Top, "Top", True)
try:
bbox = Rhino.Geometry.BoundingBox.Empty
for o in target_objects:
g = o.Geometry
if g is None: continue
try:
bb = g.GetBoundingBox(True)
if bb.IsValid: bbox.Union(bb)
except Exception: pass
if bbox.IsValid:
# Etwas Padding
bbox.Inflate(bbox.Diagonal.Length * 0.1)
vp.ZoomBoundingBox(bbox)
except Exception as ex:
print("[LIBRARY] thumbnail zoom:", ex)
view.Redraw()
# Capture
try:
bmp = view.CaptureToBitmap(Size(int(size), int(size)))
if bmp is not None:
# Sicherstellen dass Verzeichnis da ist
try:
d = os.path.dirname(png_abs_path)
if d and not os.path.isdir(d): os.makedirs(d)
except Exception: pass
bmp.Save(png_abs_path)
capture_ok = True
except Exception as ex:
print("[LIBRARY] thumbnail capture:", ex)
finally:
# Viewport wiederherstellen
try:
vp.SetCameraLocation(saved_loc, False)
vp.SetCameraTarget(saved_target, True)
if saved_proj:
vp.IsParallelProjection = True
else:
vp.IsParallelProjection = False
except Exception: pass
finally:
# Hidden Objekte wieder einblenden
for gid in hidden_by_us:
try: doc.Objects.Show(gid, True)
except Exception: pass
try: doc.Views.Redraw()
except Exception: pass
return capture_ok
def read_preview_data_uri(rel_path):
"""Liest die PNG-Vorschau als data:image/png;base64-URI fuer
direktes Einsetzen in <img src='...'>. Liefert None wenn Datei fehlt."""
if not rel_path: return None
abs_p = os.path.join(library_root(), rel_path)
if not os.path.isfile(abs_p): return None
try:
import base64
with open(abs_p, "rb") as f:
data = f.read()
return "data:image/png;base64," + base64.b64encode(data).decode("ascii")
except Exception as ex:
print("[LIBRARY] read_preview:", ex)
return None
def convert_to_3dm_via_import(src_path, target_name):
"""Konvertiert eine beliebige CAD-Datei (.dwg/.obj/.fbx/.dae/.stl/...)
nach .3dm. Strategie: Rhinos _-Import in den aktiven Doc, dann die
NEU hinzugekommenen Objekte als File3dm in library/assets/<name>.3dm
speichern + aus dem Doc wieder loeschen. Returns relativer Pfad oder
None.
WARNUNG: User-Doc wird kurz veraendert (Objects in/out) wir
delete'n alles wieder am Ende. Setzt einen sticky-Flag damit unsere
eigenen Listener nichts cascaden."""
if not src_path or not os.path.isfile(src_path): return None
if Rhino is None: return None
import scriptcontext as sc
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return None
ensure_library()
assets_dir = os.path.join(library_root(), "assets")
if not os.path.isdir(assets_dir):
try: os.makedirs(assets_dir)
except Exception: pass
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in target_name)
if not safe.endswith(".3dm"): safe += ".3dm"
target = os.path.join(assets_dir, safe)
if os.path.isfile(target):
stem, ext = os.path.splitext(safe)
n = 2
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
n += 1
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
# Listener stilllegen: unsere Add-/Delete-Cascade soll bei diesem
# temporaeren Import nicht greifen (Objekte haben keine DOSSIER-
# UserStrings, kommen aber trotzdem durch unsere Schnellfilter).
sc.sticky["dossier_library_import_busy"] = True
sc.sticky["dossier_swisstopo_busy"] = True # blockt schon viele Listener
# Snapshot der existierenden Object-IDs
before_ids = set()
try:
for o in doc.Objects:
try:
if not o.IsDeleted: before_ids.add(str(o.Id))
except Exception: pass
except Exception: pass
# User-Selection sichern damit wir sie am Ende restoren
sel_before_ids = []
try:
for o in doc.Objects.GetSelectedObjects(False, False):
sel_before_ids.append(o.Id)
except Exception: pass
new_objs = []
try:
try: doc.Objects.UnselectAll()
except Exception: pass
# _-Import dash-prefix = scripted, kein UI-Dialog. Pfad in Quotes
# damit Spaces nicht splitten. _Enter beendet die Optionen.
cmd = '_-Import "' + src_path + '" _Enter _Enter'
try:
Rhino.RhinoApp.RunScript(cmd, False)
except Exception as ex:
print("[LIBRARY] convert_to_3dm RunScript:", ex)
# Sammle die NEU hinzugekommenen Objekte
try:
for o in doc.Objects:
try:
if o.IsDeleted: continue
if str(o.Id) not in before_ids:
new_objs.append(o)
except Exception: pass
except Exception: pass
if not new_objs:
print("[LIBRARY] convert_to_3dm: Import lieferte keine Objekte")
return None
# In File3dm packen
try:
from Rhino.FileIO import File3dm
import Rhino.Geometry as rg
bbox = rg.BoundingBox.Empty
geoms_attrs = []
for o in new_objs:
g = o.Geometry
if g is None: continue
try:
bb = g.GetBoundingBox(True)
if bb.IsValid: bbox.Union(bb)
except Exception: pass
geoms_attrs.append((g, o.Attributes))
if bbox.IsValid:
offset = rg.Transform.Translation(
-bbox.Min.X, -bbox.Min.Y, -bbox.Min.Z)
else:
offset = rg.Transform.Identity
f3 = File3dm()
for g, a in geoms_attrs:
try:
g2 = g.Duplicate()
try: g2.Transform(offset)
except Exception: pass
try: f3.Objects.Add(g2, a)
except Exception:
if isinstance(g2, rg.Brep): f3.Objects.AddBrep(g2)
elif isinstance(g2, rg.Curve): f3.Objects.AddCurve(g2)
elif isinstance(g2, rg.Mesh): f3.Objects.AddMesh(g2)
except Exception as ex:
print("[LIBRARY] convert add geom:", ex)
try: f3.Write(target, 8)
except Exception as ex:
print("[LIBRARY] convert_to_3dm Write:", ex)
return None
except Exception as ex:
print("[LIBRARY] convert_to_3dm File3dm:", ex)
return None
finally:
# Thumbnail-Capture BEVOR die Objekte geloescht werden.
try:
rel_for_preview = os.path.relpath(target, library_root())
preview_rel = _preview_rel_for(rel_for_preview)
preview_abs = os.path.join(library_root(), preview_rel)
_capture_thumbnail_of_objects(new_objs, preview_abs)
except Exception as ex:
print("[LIBRARY] convert thumbnail:", ex)
# Cleanup: importierte Objekte wieder loeschen
for o in new_objs:
try: doc.Objects.Delete(o.Id, True)
except Exception: pass
# Restore Selection
try:
for gid in sel_before_ids:
try: doc.Objects.Select(gid, True)
except Exception: pass
except Exception: pass
rel = os.path.relpath(target, library_root())
print("[LIBRARY] convert_to_3dm OK: {}{}".format(src_path, rel))
return rel
finally:
sc.sticky["dossier_library_import_busy"] = False
sc.sticky["dossier_swisstopo_busy"] = False
try: doc.Views.Redraw()
except Exception: pass
def save_selection_to_asset(doc, target_name):
"""Speichert die aktuelle Selection aus dem Doc als eigene .3dm-Datei
in library/assets/<target_name>.3dm. Returns relativer Pfad oder None.
Geometry wird relativ zum BoundingBox-Min platziert damit der Block-
Origin am Ursprung sitzt sinnvoll fuer Symbol-Insert."""
if doc is None or not target_name: return None
try:
sel = list(doc.Objects.GetSelectedObjects(False, False))
except Exception:
sel = []
if not sel:
print("[LIBRARY] save_selection: keine Auswahl")
return None
if Rhino is None: return None
ensure_library()
assets_dir = os.path.join(library_root(), "assets")
if not os.path.isdir(assets_dir):
try: os.makedirs(assets_dir)
except Exception: pass
# Safe filename
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in target_name)
if not safe.endswith(".3dm"): safe += ".3dm"
target = os.path.join(assets_dir, safe)
if os.path.isfile(target):
stem, ext = os.path.splitext(safe)
n = 2
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
n += 1
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
# File3dm aufbauen + Selection rein
try:
from Rhino.FileIO import File3dm, File3dmWriteOptions
import Rhino.Geometry as rg
# BoundingBox sammeln um geometrie an Ursprung zu verschieben
bbox = rg.BoundingBox.Empty
geoms = []
for o in sel:
g = o.Geometry
if g is None: continue
try:
bb = g.GetBoundingBox(True)
if bb.IsValid: bbox.Union(bb)
except Exception: pass
geoms.append((g, o.Attributes))
if not geoms:
return None
if not bbox.IsValid:
origin = rg.Point3d(0, 0, 0)
else:
origin = bbox.Min
offset = rg.Transform.Translation(-origin.X, -origin.Y, -origin.Z)
f3 = File3dm()
for g, a in geoms:
try:
g2 = g.Duplicate()
try: g2.Transform(offset)
except Exception: pass
# Generischer Add fuer alle GeometryBase
try: f3.Objects.Add(g2, a)
except Exception:
# Fallback: typ-spezifisch
if isinstance(g2, rg.Brep): f3.Objects.AddBrep(g2)
elif isinstance(g2, rg.Curve): f3.Objects.AddCurve(g2)
elif isinstance(g2, rg.Mesh): f3.Objects.AddMesh(g2)
elif isinstance(g2, rg.Point): f3.Objects.AddPoint(g2.Location)
except Exception as ex:
print("[LIBRARY] save_selection add:", ex)
# Write
try:
opts = File3dmWriteOptions()
opts.Version = 8
f3.Write(target, opts)
except Exception:
try: f3.Write(target, 8)
except Exception as ex:
print("[LIBRARY] save_selection write:", ex)
return None
rel = os.path.relpath(target, library_root())
# Thumbnail aus den (noch selektierten) Objekten capturen
try:
preview_rel = _preview_rel_for(rel)
preview_abs = os.path.join(library_root(), preview_rel)
_capture_thumbnail_of_objects(sel, preview_abs)
except Exception as ex:
print("[LIBRARY] save_selection thumbnail:", ex)
print("[LIBRARY] save_selection: {} objs → {}".format(len(geoms), rel))
return rel
except Exception as ex:
print("[LIBRARY] save_selection:", ex)
return None
def copy_to_assets(src_path, target_name=None):
"""Kopiert eine Datei in library/assets/. target_name optional (sonst
Original-Name). Returns relativer Pfad ('assets/foo.3dm') oder None."""
if not src_path or not os.path.isfile(src_path):
return None
ensure_library()
assets_dir = os.path.join(library_root(), "assets")
if not os.path.isdir(assets_dir):
try: os.makedirs(assets_dir)
except Exception: pass
base = target_name or os.path.basename(src_path)
target = os.path.join(assets_dir, base)
# Konflikt-Resolution: bei doppeltem Namen Nummer dran
if os.path.isfile(target):
stem, ext = os.path.splitext(base)
n = 2
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
n += 1
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
try:
import shutil
shutil.copy2(src_path, target)
rel = os.path.relpath(target, library_root())
return rel
except Exception as ex:
print("[LIBRARY] copy_to_assets:", ex)
return None
def _empty_manifest():
return {"schemaVersion": _SCHEMA_VERSION,
"name": "Dossier-Library", "items": []}
@@ -156,6 +611,14 @@ def _empty_manifest():
def _normalize_item(it):
if not isinstance(it, dict): return None
if not it.get("id") or not it.get("type"): return None
# 2D/3D-Files: ein Item kann beide haben (Objekt mit Plan-Darstellung 2D
# + perspektivischem 3D-Modell) oder nur eines. Legacy 'files'-Feld wird
# als files2d interpretiert (Symbole = nur 2D historisch).
files_legacy = list(it.get("files") or [])
files2d = list(it.get("files2d") or [])
files3d = list(it.get("files3d") or [])
if not files2d and not files3d and files_legacy:
files2d = files_legacy
out = {
"id": str(it["id"]),
"type": str(it["type"]),
@@ -164,9 +627,10 @@ def _normalize_item(it):
"tags": list(it.get("tags") or []),
"preview": it.get("preview"),
"data": it.get("data") or {},
# files: relative Pfade (zur library_root()) auf .3dm-Fragmente.
# Symbol/Object-Import liest die ueber File3dm.Read.
"files": list(it.get("files") or []),
"files2d": files2d,
"files3d": files3d,
# legacy "files" mitgeben fuer Backwards-Kompatibilitaet
"files": files_legacy or files2d,
}
return out
@@ -192,13 +656,15 @@ def import_material(doc, item):
new_mat = {
"name": item.get("name") or "Unbenannt",
"color": data.get("color", "#888888"),
"hatch": data.get("hatch", "Solid"),
"scale": float(data.get("scale", 1.0) or 1.0),
"source": "library",
"libraryId": item.get("id"),
}
# PBR + Textur-Felder, falls Library-Item welche hat
for k in ("roughness", "reflection", "transparency", "iorN",
"uvScaleM", "textures"):
if k in data: new_mat[k] = data[k]
# Lazy-Import um Zyklen zu vermeiden
import rhinopanel
import layers_panel as rhinopanel
settings = rhinopanel.load_project_settings(doc)
mats = list(settings.get("materials", []))
for m in mats:
@@ -218,20 +684,21 @@ def _lib_asset_path(rel_path):
return os.path.join(library_root(), rel_path)
def _block_name_for(item):
def _block_name_for(item, variant=""):
"""Stabiler Block-Name fuer InstanceDefinition. Format:
'dossier_lib_<libraryId>' Dedup ueber Lib-ID, nicht Item-Name (sonst
Konflikt bei Umbenennen)."""
'dossier_lib_<libraryId>[_<variant>]'. variant '2d'/'3d' fuer Pair-Items."""
lid = item.get("id") or ""
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in lid)
return "dossier_lib_" + safe
name = "dossier_lib_" + safe
if variant: name += "_" + variant
return name
def _read_3dm_geometry(abs_path):
"""Liest alle Top-Level-Objekte aus einer .3dm-Datei. Returns Liste von
(GeometryBase, ObjectAttributes). Bei Fehler leere Liste."""
if not abs_path or not os.path.isfile(abs_path):
print("[LIBRARY] _read_3dm: Datei nicht gefunden:", abs_path)
print("[LIBRARY] _read_3dm: Datei not found:", abs_path)
return []
try:
from Rhino.FileIO import File3dm
@@ -254,24 +721,24 @@ def _read_3dm_geometry(abs_path):
return []
def _ensure_block_definition(doc, item, geometry_attrs):
def _ensure_block_definition(doc, item, geometry_attrs, variant=""):
"""Erstellt InstanceDefinition fuer dieses Library-Item wenn noch nicht
da. Returns (idx, was_created). idx<0 bei Fehler."""
da. variant unterscheidet '2d'/'3d'. Returns (idx, was_created)."""
if Rhino is None: return -1, False
name = _block_name_for(item)
name = _block_name_for(item, variant=variant)
try:
existing = doc.InstanceDefinitions.Find(name)
except Exception:
existing = None
if existing is not None:
return existing.Index, False
# Geometry + Attributes separat sammeln
geoms = [g for g, _ in geometry_attrs if g is not None]
attrs = [a for _, a in geometry_attrs]
if not geoms:
return -1, False
base_pt = Rhino.Geometry.Point3d(0, 0, 0)
desc = "Dossier-Library: " + (item.get("name") or "")
desc_suffix = " ({})".format(variant.upper()) if variant else ""
desc = "Dossier-Library: " + (item.get("name") or "") + desc_suffix
try:
idx = doc.InstanceDefinitions.Add(name, desc, base_pt, geoms, attrs)
return idx, True
@@ -280,52 +747,83 @@ def _ensure_block_definition(doc, item, geometry_attrs):
return -1, False
def import_symbol(doc, item):
"""Importiert ein Symbol-Item (= 2D-Block) in das Doc. Liest die
.3dm-Datei(en) aus item.files, erstellt eine InstanceDefinition mit
stabilem Namen, fuegt eine Instanz am Ursprung ein.
Returns (ok, message)."""
return _import_block_like(doc, item, kind="symbol")
def import_object(doc, item):
"""Importiert ein Object-Item (= 3D-Block, BIM-Element) in das Doc.
Gleiche Pipeline wie import_symbol Symbol/Object unterscheiden sich
nur in der UI-Kategorisierung."""
return _import_block_like(doc, item, kind="object")
def _import_block_like(doc, item, kind):
if doc is None: return False, "Kein aktives Dokument"
files = item.get("files") or []
if not files:
return False, "Item hat keine .3dm-Files: " + str(item.get("id"))
# Alle Files zusammen in eine Block-Definition packen.
def _build_variant_block(doc, item, variant_files, variant_label):
"""Liest .3dm-Files und legt eine InstanceDefinition (variant) an.
Returns Block-Index oder -1."""
if not variant_files: return -1
all_geom = []
for f in files:
for f in variant_files:
abs_p = _lib_asset_path(f)
all_geom.extend(_read_3dm_geometry(abs_p))
if not all_geom:
return False, "Keine importierbare Geometrie in {}".format(files)
idx, created = _ensure_block_definition(doc, item, all_geom)
if idx < 0:
return False, "InstanceDefinition konnte nicht erstellt werden"
# Instanz am Ursprung einfuegen — User kann danach verschieben.
return -1
idx, _ = _ensure_block_definition(doc, item, all_geom, variant=variant_label)
return idx
def _place_instance(doc, block_idx, point, layer_idx=-1):
"""Platziert eine InstanceObject am gegebenen Punkt, optional auf
spezifischem Layer. Returns Guid oder None."""
if block_idx < 0: return None
try:
xform = Rhino.Geometry.Transform.Identity
inst_id = doc.Objects.AddInstanceObject(idx, xform)
if inst_id == System_Guid_Empty():
return False, "AddInstanceObject fehlgeschlagen"
try: doc.Views.Redraw()
except Exception: pass
msg = ("Block '{}' importiert + am Ursprung eingefuegt".format(
item.get("name") or "")
if created else
"Block bereits vorhanden — neue Instanz eingefuegt")
return True, msg
xform = Rhino.Geometry.Transform.Translation(
point.X, point.Y, point.Z)
attrs = Rhino.DocObjects.ObjectAttributes()
if layer_idx >= 0:
attrs.LayerIndex = layer_idx
gid = doc.Objects.AddInstanceObject(block_idx, xform, attrs)
return gid
except Exception as ex:
print("[LIBRARY] AddInstanceObject:", ex)
return False, str(ex)
print("[LIBRARY] _place_instance:", ex)
return None
def import_symbol(doc, item, at_point=None, layer2d=-1, layer3d=-1):
return _import_block_like(doc, item, at_point=at_point,
layer2d=layer2d, layer3d=layer3d)
def import_object(doc, item, at_point=None, layer2d=-1, layer3d=-1):
return _import_block_like(doc, item, at_point=at_point,
layer2d=layer2d, layer3d=layer3d)
def _import_block_like(doc, item, at_point=None, layer2d=-1, layer3d=-1):
"""Platziert ein Library-Item im Doc. Item kann files2d und/oder files3d
haben beide Varianten werden geladen + an gleichem Punkt platziert auf
ihren respektiven Layern.
at_point: Point3d (default = Origin).
layer2d/3d: optionale Layer-Indizes (default = aktiver Layer)."""
if doc is None: return False, "Kein aktives Dokument"
files2d = item.get("files2d") or []
files3d = item.get("files3d") or []
if not files2d and not files3d:
# Legacy fallback
legacy = item.get("files") or []
if legacy: files2d = legacy
if not files2d and not files3d:
return False, "Item hat keine .3dm-Files: " + str(item.get("id"))
if at_point is None:
at_point = Rhino.Geometry.Point3d(0, 0, 0)
placed = []
if files2d:
idx2 = _build_variant_block(doc, item, files2d, "2d")
if idx2 >= 0:
gid = _place_instance(doc, idx2, at_point, layer_idx=layer2d)
if gid is not None and gid != System_Guid_Empty():
placed.append("2D")
if files3d:
idx3 = _build_variant_block(doc, item, files3d, "3d")
if idx3 >= 0:
gid = _place_instance(doc, idx3, at_point, layer_idx=layer3d)
if gid is not None and gid != System_Guid_Empty():
placed.append("3D")
if not placed:
return False, "Konnte keinen Block platzieren (Files fehlen?)"
try: doc.Views.Redraw()
except Exception: pass
return True, "{} eingefuegt ({})".format(
item.get("name") or "", "+".join(placed))
def System_Guid_Empty():
@@ -337,16 +835,19 @@ def System_Guid_Empty():
return None
def import_item(doc, item_id):
def import_item(doc, item_id, at_point=None, layer2d=-1, layer3d=-1):
"""Type-dispatching Import. material → Project-Settings-Liste.
symbol/object InstanceDefinition im Doc via File3dm.Read."""
symbol/object InstanceDefinition im Doc via File3dm.Read.
at_point + layer2d/3d nur fuer symbol/object."""
item = find_item(item_id)
if item is None: return False, "Item nicht gefunden: " + str(item_id)
if item is None: return False, "Item not found: " + str(item_id)
t = item.get("type")
if t == "material":
return import_material(doc, item)
if t == "symbol":
return import_symbol(doc, item)
return import_symbol(doc, item, at_point=at_point,
layer2d=layer2d, layer3d=layer3d)
if t == "object":
return import_object(doc, item)
return import_object(doc, item, at_point=at_point,
layer2d=layer2d, layer3d=layer3d)
return False, "Unbekannter Typ: '{}'".format(t)
+5 -3
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
"""
mass_style.py
Globale Mass-Stil-Presets fuer Dossier speichert pro Dokument benannte
@@ -12,7 +14,7 @@ Persistiert als JSON in doc.Strings["dossier_mass_styles"] (Liste) und
doc.Strings["dossier_mass_style_active"] (aktive ID).
Ein Mass-Style wird als globale Vorgabe gelesen. Per-Element-Override
(z.B. raum_rundung UserString am einzelnen Raum) hat Vorrang wenn gesetzt.
(z.B. raum_rundung UserString am einzelnen Raum) hat Vorrang wenn set.
"""
import json
import uuid
@@ -86,7 +88,7 @@ def list_presets(doc):
# Erst-Initialisierung: Default-Liste schreiben
items = [_normalize(p) for p in _DEFAULT_PRESETS]
_save_all(doc, items)
# Default-Aktiv setzen falls noch nichts gesetzt
# Default-Aktiv setzen falls noch nichts set
if not doc.Strings.GetValue(_KEY_ACTIVE):
doc.Strings.SetString(_KEY_ACTIVE, items[0]["id"])
return items
@@ -173,7 +175,7 @@ def delete_preset(doc, preset_id):
def raum_rundung_default(doc):
"""Default-Rundung fuer Raum-Stempel wenn keine per-Raum-Override
gesetzt ist."""
set ist."""
p = get_active(doc)
return p["raumRundung"] if p else "0.1"
+2
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
"""
masse_settings.py
Satellite-Fenster fuer das Bearbeiten der Masse-Presets
+63 -52
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
"""
massstab.py
MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports.
@@ -32,11 +34,11 @@ PANEL_GUID_STR = "5c8e4f3f-6d0e-4f1a-a3d4-e5f607182941"
# Wir legen sie in einer Config-Datei im Home des Users ab.
_DOC_DPI_KEY = "dossier_dpi" # Legacy, fuer Migration
# Pro Viewport-Name der zuletzt vom User explizit gesetzte Massstab
# Pro Viewport-Name der zuletzt vom User explizit sete Massstab
# (Dropdown/Input/100%-Button/Ausschnitt-Restore). NICHT der Live-Zoom — der
# drifted bei Pan/Zoom. Wird von Ausschnitten beim Speichern als "der
# eingestellte Massstab" gelesen. Per-doc persistiert in doc.Strings als
# JSON-Dict, damit ein Wechsel zurueck auf einen frueher gesetzten Viewport
# JSON-Dict, damit ein Wechsel zurueck auf einen frueher seten Viewport
# den korrekten Wert wieder rausgibt — auch nach Restart.
_user_set_scales = {} # {viewport_name: float}
_user_set_scales_loaded = False # lazy load aus doc.Strings beim ersten Zugriff
@@ -102,7 +104,7 @@ def _detect_dpi():
try:
from System.Diagnostics import Process, ProcessStartInfo
except Exception as ex:
print("[MASSSTAB] auto-detect: .NET Process nicht verfuegbar:", ex)
print("[SCALE] auto-detect: .NET Process not available:", ex)
return None
if not os.path.isfile("/usr/bin/osascript"):
# Vermutlich nicht macOS -> nichts zu detecten
@@ -135,10 +137,10 @@ def _detect_dpi():
if not finished:
try: p.Kill()
except Exception: pass
print("[MASSSTAB] auto-detect: osascript timeout")
print("[SCALE] auto-detect: osascript timeout")
return None
if p.ExitCode != 0:
print("[MASSSTAB] auto-detect osascript ExitCode={}:".format(p.ExitCode), err)
print("[SCALE] auto-detect osascript ExitCode={}:".format(p.ExitCode), err)
return None
import json as _json
data = _json.loads((out or "").strip())
@@ -152,15 +154,15 @@ def _detect_dpi():
return None
dpi = px * 25.4 / mm
if dpi < 30.0 or dpi > 600.0:
print("[MASSSTAB] auto-detect: DPI {:.1f} ausserhalb 30..600 -> ignoriert".format(dpi))
print("[SCALE] auto-detect: DPI {:.1f} ausserhalb 30..600 -> ignoriert".format(dpi))
return None
print("[MASSSTAB] DPI auto-detected: {:.1f} physisch (Bildschirm {:.0f}x{:.0f}px / {:.1f}x{:.1f}mm, logisch {:.0f}x{:.0f})".format(
print("[SCALE] DPI auto-detected: {:.1f} physical (screen {:.0f}x{:.0f}px / ... logical {:.0f}x{:.0f})".format(
dpi, px, float(data.get("py") or 0),
mm, float(data.get("mh") or 0),
lpx, float(data.get("lpy") or 0)))
return dpi
except Exception as ex:
print("[MASSSTAB] auto-detect fehlgeschlagen:", ex)
print("[SCALE] auto-detect failed:", ex)
return None
finally:
if script_path:
@@ -186,7 +188,7 @@ def _read_config():
if isinstance(data, dict):
cfg = data
except Exception as ex:
print("[MASSSTAB] config lesen:", ex)
print("[SCALE] config lesen:", ex)
_config_cache = cfg
return cfg
@@ -202,7 +204,7 @@ def _write_config(cfg):
_config_cache = cfg # Cache mit dem geschriebenen Stand aktualisieren
return True
except Exception as ex:
print("[MASSSTAB] config schreiben:", ex)
print("[SCALE] config schreiben:", ex)
return False
@@ -264,7 +266,7 @@ def _set_dpi(doc, value, source="manual"):
cfg["dpi_source"] = source
if not _write_config(cfg):
return False
print("[MASSSTAB] DPI={:.1f} ({}) -> {}".format(v, source, _CONFIG_PATH))
print("[SCALE] DPI={:.1f} ({}) -> {}".format(v, source, _CONFIG_PATH))
return True
@@ -346,7 +348,7 @@ def _compute_scale(doc, vp):
pass
# appliedScale pro Viewport. Map ist gefuettert durch _apply_scale und
# Ausschnitt-Restore — wenn ein anderer Viewport aktiv ist als beim letzten
# Setzen, kommt entweder dessen frueher gesetzter Wert oder None zurueck.
# Setzen, kommt entweder dessen frueher seter Wert oder None zurueck.
# Niemals auf die Live-Skala mappen — das Dropdown soll STATISCH sein.
# Wichtig: nur bei Parallelprojektion zurueckgeben. In Perspective ist ein
# Massstab konzeptionell unsinnig — selbst wenn der gleiche Viewport vorher
@@ -419,9 +421,9 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
layer.PlotWeight = new
n_layer += 1
except Exception as ex:
print("[MASSSTAB] LW scale layer '{}': {}".format(layer.Name, ex))
print("[SCALE] LW scale layer '{}': {}".format(layer.Name, ex))
except Exception as ex:
print("[MASSSTAB] LW scale layers:", ex)
print("[SCALE] LW scale layers:", ex)
# -- Objekte -------------------------------------------------------------
try:
@@ -449,11 +451,11 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
except Exception:
pass
except Exception as ex:
print("[MASSSTAB] LW scale objects:", ex)
print("[SCALE] LW scale objects:", ex)
try: doc.Views.Redraw()
except Exception: pass
print("[MASSSTAB] PlotWeight-Skalierung x{:.1f}: {} Layer, {} Objekte angepasst".format(
print("[SCALE] PlotWeight-Skalierung x{:.1f}: {} Layer, {} Objekte angepasst".format(
factor, n_layer, n_obj))
# Diagnose: zeige die ersten paar Layer mit ihren echten PlotWeights
try:
@@ -461,7 +463,7 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
for layer in doc.Layers:
if layer.IsDeleted or not layer.PlotWeight: continue
stored = layer.GetUserString(_LW_ORIG_KEY) or "-"
print("[MASSSTAB] Layer '{}' PlotWeight={:.3f}mm (orig={})".format(
print("[SCALE] Layer '{}' PlotWeight={:.3f}mm (orig={})".format(
layer.Name, float(layer.PlotWeight), stored))
shown += 1
if shown >= 5: break
@@ -477,7 +479,7 @@ def write_plotweight(doc, target, value):
Print-Mode-aware. value = "echter" Wert in mm wie er auf Papier landet.
Speichert value als Original-UserString. Wenn Print-Mode aktiv ist wird
PlotWeight = value * scale gesetzt damit die Anzeige direkt skaliert.
PlotWeight = value * scale set damit die Anzeige direkt skaliert.
Aufrufer ist verantwortlich fuer ModifyAttributes / Doc-Refresh."""
if target is None: return
@@ -497,7 +499,7 @@ def write_plotweight(doc, target, value):
try:
target.PlotWeight = v * factor
except Exception as ex:
print("[MASSSTAB] write_plotweight set:", ex)
print("[SCALE] write_plotweight set:", ex)
def apply_scaled_hatches(doc, scale_n):
@@ -570,11 +572,11 @@ def apply_scaled_hatches(doc, scale_n):
if doc.Objects.Replace(hid, new_g):
n_scaled += 1
except Exception as ex:
print("[MASSSTAB] hatch set PatternScale:", ex)
print("[SCALE] hatch set PatternScale:", ex)
except Exception as ex:
print("[MASSSTAB] hatch iter:", ex)
print("[SCALE] hatch iter:", ex)
if n_scaled or hatch_ids:
print("[MASSSTAB] Hatch-Skalierung: {} gefunden, {} mit Faktor x{:.2f} angepasst".format(
print("[SCALE] Hatch-Skalierung: {} gefunden, {} mit Faktor x{:.2f} angepasst".format(
len(hatch_ids), n_scaled, factor))
try: doc.Views.Redraw()
except Exception: pass
@@ -597,7 +599,7 @@ def post_create_hatch_scale(doc, hatch_obj, user_scale):
a.SetUserString(_HATCH_ORIG_KEY, "{:.6f}".format(u))
doc.Objects.ModifyAttributes(hatch_obj, a, True)
except Exception as ex:
print("[MASSSTAB] post_create_hatch_scale orig:", ex)
print("[SCALE] post_create_hatch_scale orig:", ex)
# Mit aktuellem Massstab skalieren (sqrt-Formel /10, siehe apply_scaled_hatches)
scale_n = _read_user_scale(doc, default=1.0)
if not scale_n or scale_n <= 0: scale_n = 1.0
@@ -610,7 +612,7 @@ def post_create_hatch_scale(doc, hatch_obj, user_scale):
new_g.PatternScale = u * factor
doc.Objects.Replace(h2.Id, new_g)
except Exception as ex:
print("[MASSSTAB] post_create_hatch_scale rescale:", ex)
print("[SCALE] post_create_hatch_scale rescale:", ex)
def read_plotweight(target):
@@ -654,7 +656,7 @@ def _set_lineweights_enabled(doc, enabled):
try:
doc.Strings.SetString(_LW_KEY, flag)
except Exception as ex:
print("[MASSSTAB] _set_lineweights_enabled persist:", ex)
print("[SCALE] _set_lineweights_enabled persist:", ex)
# Print-Display togglen — primaerer Befehl auf Mac Rhino
on_off = "_On" if enabled else "_Off"
yes_no = "_Yes" if enabled else "_No"
@@ -673,17 +675,17 @@ def _set_lineweights_enabled(doc, enabled):
scale_n = _read_user_scale(doc, default=1.0)
_apply_scaled_lineweights(doc, enabled, scale_n)
except Exception as ex:
print("[MASSSTAB] PlotWeight-Scale:", ex)
print("[SCALE] PlotWeight-Scale:", ex)
try:
for v in doc.Views: v.Redraw()
except Exception: pass
print("[MASSSTAB] Print-Display:", "AN (Strichstaerken sichtbar)" if enabled else "AUS")
print("[SCALE] Print-Display:", "AN (Strichstaerken sichtbar)" if enabled else "AUS")
return True
def _read_user_scale(doc, default=1.0):
"""Persistierter eingestellter Massstab oder default. Setze default=None
um "nie gesetzt" zu erkennen."""
um "nie set" zu erkennen."""
if doc is None: return default
try:
raw = doc.Strings.GetValue(_DOC_USER_SCALE_KEY)
@@ -700,7 +702,7 @@ def _write_user_scale(doc, ratio):
try:
doc.Strings.SetString(_DOC_USER_SCALE_KEY, "{:.6f}".format(float(ratio)))
except Exception as ex:
print("[MASSSTAB] _write_user_scale:", ex)
print("[SCALE] _write_user_scale:", ex)
def _ensure_user_scales_loaded(doc):
@@ -721,7 +723,7 @@ def _ensure_user_scales_loaded(doc):
except Exception:
pass
except Exception as ex:
print("[MASSSTAB] _ensure_user_scales_loaded:", ex)
print("[SCALE] _ensure_user_scales_loaded:", ex)
_user_set_scales_loaded = True
@@ -731,7 +733,7 @@ def _write_user_scales(doc):
doc.Strings.SetString(_DOC_USER_SCALES_KEY,
json.dumps(_user_set_scales, ensure_ascii=False))
except Exception as ex:
print("[MASSSTAB] _write_user_scales:", ex)
print("[SCALE] _write_user_scales:", ex)
def _get_applied_scale_for_vp(doc, vp_name):
@@ -776,7 +778,7 @@ def _rescale_doc_patterns(doc, factor):
doc.Objects.Replace(obj.Id, g2)
n_h += 1
except Exception as ex:
print("[MASSSTAB] hatch rescale:", ex)
print("[SCALE] hatch rescale:", ex)
# Per-Objekt Linetype-Scale (Rhino 8 Attribut)
try:
a = obj.Attributes
@@ -784,7 +786,7 @@ def _rescale_doc_patterns(doc, factor):
if hasattr(a, prop):
cur = getattr(a, prop)
if cur and cur > 0 and abs(cur - 1.0) > 1e-9:
# Nur Objekte mit explizit gesetzter Skala anfassen
# Nur Objekte mit explizit seter Skala anfassen
# (Default=1.0 ueberlassen wir dem globalen Multiplikator).
new_a = a.Duplicate()
setattr(new_a, prop, cur * factor)
@@ -794,7 +796,7 @@ def _rescale_doc_patterns(doc, factor):
except Exception:
pass
except Exception as ex:
print("[MASSSTAB] _rescale_doc_patterns:", ex)
print("[SCALE] _rescale_doc_patterns:", ex)
# Globale Linetype-Pattern-Length-Skala (Rhino-doc-Setting) versuchen.
# Property-Namen variieren je nach Version — wir probieren.
@@ -813,7 +815,7 @@ def _rescale_doc_patterns(doc, factor):
pass
try: doc.Views.Redraw()
except Exception: pass
print("[MASSSTAB] Rescale x{:.4f}: {} Hatches, {} per-obj Linetypes{}".format(
print("[SCALE] Rescale x{:.4f}: {} Hatches, {} per-obj Linetypes{}".format(
factor, n_h, n_l, ", global Linetype-Scale" if set_global else ""))
@@ -831,7 +833,7 @@ def _apply_scale(doc, vp, ratio):
if vp is None or doc is None: return False
try:
if not vp.IsParallelProjection:
print("[MASSSTAB] Viewport ist nicht parallel — Skala nicht setzbar")
print("[SCALE] Viewport ist nicht parallel — Skala nicht setzbar")
return False
except Exception:
return False
@@ -854,7 +856,7 @@ def _apply_scale(doc, vp, ratio):
# factor > 1 zoomt rein (kleineres Frustum). factor = cur_w / new_w.
factor = cur_w / new_frustum_u
if factor <= 0 or not (factor < 1e9 and factor > 1e-9):
print("[MASSSTAB] _apply_scale: ungueltiger Faktor", factor)
print("[SCALE] _apply_scale: ungueltiger Faktor", factor)
return False
applied = False
# Verschiedene API-Signaturen je nach Rhino-Version durchprobieren.
@@ -873,7 +875,7 @@ def _apply_scale(doc, vp, ratio):
Rhino.RhinoApp.RunScript("_-Zoom _Factor {:.6f} _Enter".format(factor), False)
applied = True
except Exception as ex3:
print("[MASSSTAB] _apply_scale alle Varianten fehlgeschlagen:",
print("[SCALE] _apply_scale alle Varianten failed:",
ex1, ex2, ex3)
if not applied:
return False
@@ -882,28 +884,37 @@ def _apply_scale(doc, vp, ratio):
if _get_lineweights_enabled(doc):
_apply_scaled_lineweights(doc, True, float(ratio))
except Exception as ex:
print("[MASSSTAB] LW-Rescale:", ex)
print("[SCALE] LW-Rescale:", ex)
# Hatches mit sqrt(N) skalieren — moderate Anpassung.
try:
apply_scaled_hatches(doc, float(ratio))
except Exception as ex:
print("[MASSSTAB] Hatch-Rescale:", ex)
# Neuen Wert persistieren — sowohl per-Viewport (fuer das Dropdown,
# damit jeder Viewport seinen eigenen Massstab behaelt) als auch als
# globaler "letzter Wert" (Legacy-Key; wird von Plotweight/Hatch-Rescale
# doc-weit benutzt — dort ist nur EIN Faktor sinnvoll).
print("[SCALE] Hatch-Rescale:", ex)
# Neuen Wert ZUERST persistieren — sowohl per-Viewport (fuer das
# Dropdown, damit jeder Viewport seinen eigenen Massstab behaelt) als
# auch als globaler "letzter Wert". WICHTIG: vor dem Raumstempel-
# Regen weil _resolve_raum_text_height_m get_applied_scale_ratio()
# liest — sonst regennt mit ALTER Skala.
_write_user_scale(doc, ratio)
try:
_set_applied_scale_for_vp(doc, vp.Name, float(ratio))
except Exception as ex:
print("[MASSSTAB] per-vp scale write:", ex)
print("[SCALE] per-vp scale write:", ex)
# Raumstempel im masstab-Modus regennen mit der NEUEN Skala.
try:
import elemente as _el
n_regen = _el.regen_masstab_raeume(doc)
if n_regen > 0:
print("[SCALE] {} masstab-Raum/Raeume regenned".format(n_regen))
except Exception as ex:
print("[SCALE] Raumstempel-Regen:", ex)
try: doc.Views.Redraw()
except Exception: pass
print("[MASSSTAB] Skala 1:{:.2f} gesetzt (Faktor {:.4f}, soll-frustum {:.4f} {})".format(
print("[SCALE] Skala 1:{:.2f} set (Faktor {:.4f}, soll-frustum {:.4f} {})".format(
ratio, factor, new_frustum_u, str(doc.ModelUnitSystem)))
return True
except Exception as ex:
print("[MASSSTAB] _apply_scale:", ex)
print("[SCALE] _apply_scale:", ex)
return False
@@ -913,7 +924,7 @@ def _zoom_extents(doc, vp, selected_only=False):
if selected_only:
objs = list(doc.Objects.GetSelectedObjects(False, False))
if not objs:
print("[MASSSTAB] Keine Selektion fuer Zoom-Selection")
print("[SCALE] Keine Selektion fuer Zoom-Selection")
return False
bbox = Rhino.Geometry.BoundingBox.Empty
for o in objs:
@@ -946,7 +957,7 @@ def _zoom_extents(doc, vp, selected_only=False):
except Exception: pass
return True
except Exception as ex:
print("[MASSSTAB] _zoom_extents:", ex)
print("[SCALE] _zoom_extents:", ex)
return False
@@ -961,7 +972,7 @@ class MassstabBridge(panel_base.BaseBridge):
def _on_ready(self):
# Einmalige Bootstrap-Detection falls noch keine DPI in der Config.
try: _bootstrap_dpi()
except Exception as ex: print("[MASSSTAB] bootstrap:", ex)
except Exception as ex: print("[SCALE] bootstrap:", ex)
self._send_state(force=True)
def handle(self, data):
@@ -1003,7 +1014,7 @@ class MassstabBridge(panel_base.BaseBridge):
elif t == "DETECT_DPI":
v = _force_redetect_dpi()
if v is None:
print("[MASSSTAB] Auto-Detect: keine Bildschirminfo verfuegbar")
print("[SCALE] Auto-Detect: keine Bildschirminfo verfuegbar")
self._send_state(force=True)
elif t == "SET_LINEWEIGHTS":
doc, _ = _active_vp()
@@ -1050,7 +1061,7 @@ def _install_listeners(bridge):
Rhino.RhinoApp.Idle += on_idle
Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change
sc.sticky[flag] = True
print("[MASSSTAB] Listener aktiv (Idle-Poll + Doc-Change)")
print("[SCALE] Listener active (Idle-Poll + Doc-Change)")
def get_current_scale_ratio():
+2
View File
@@ -1,5 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
OSM-Importer fuer Dossier holt OpenStreetMap-Daten via Overpass-API als
Polylinien (Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege).
+6 -4
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
"""
overrides.py
Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides /
@@ -255,8 +257,8 @@ def delete_rule_template(name):
def set_active_preset(doc, name):
"""Aktiviert ein gespeichertes Preset: kopiert dessen Rules ins Doc-Config
und markiert es als activePreset. Wenn name leer/None: aktives Preset
geclear-t, Rules bleiben unveraendert (User waehlt "kein Preset"). Bei
aktivem enabled-Flag wird sofort neu angewendet. True bei Erfolg."""
geclear-t, Rules bleiben unchanged (User waehlt "kein Preset"). Bei
aktivem enabled-Flag wird sofort neu applied. True bei Erfolg."""
if doc is None: return False
cfg = load_config(doc)
if name:
@@ -674,7 +676,7 @@ def _apply_to_object(doc, obj, overrides):
def apply_all(doc):
"""Wendet alle aktiven Regeln auf alle Objekte im Doc an.
Objekte die NICHT (mehr) matchen werden auf Originale zurueckgesetzt."""
Objekte die NICHT (mehr) matchen werden auf Originale zurueckset."""
if doc is None: return 0, 0
cfg = load_config(doc)
if not cfg.get("enabled"): return 0, 0
@@ -863,4 +865,4 @@ def install_listeners():
return
sc.sticky["overrides_listeners"] = True
print("[OVERRIDES] Live-Update Listener aktiv (Add/Replace/LayerTable)")
print("[OVERRIDES] Live-Update Listener active (Add/Replace/LayerTable)")
+3 -1
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
"""
overrides_panel.py
OVERRIDES-Panel: Rule-Editor fuer grafische Overrides.
@@ -197,7 +199,7 @@ class OverridesBridge(panel_base.BaseBridge):
overrides.set_active_preset(doc, name)
else:
# Append-Mode: bestehende + Preset-Rules. activePreset wird
# in update_rules auf None gesetzt — passt, weil's eine
# in update_rules auf None set — passt, weil's eine
# Mischung ist, kein einzelnes Preset mehr.
rules = overrides.load_preset(name)
if rules is not None:
+120 -60
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
"""
panel_base.py
Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView.
@@ -18,6 +20,43 @@ import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
_DIST = os.path.join(_HERE, "..", "dist", "index.html")
_SETTINGS_PATH = os.path.expanduser(
"~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json")
_MODE_LOG_TAG = {
"ebenen": "LAYERS",
"zeichnungsebenen": "DRAWING-LEVELS",
"oberleiste": "TOOLBAR",
"gestaltung": "STYLES",
"werkzeuge": "TOOLS",
"dimensionen": "DIMENSIONS",
"ausschnitte": "VIEWPORTS",
"massstab": "SCALE",
"overrides": "OVERRIDES",
"layouts": "LAYOUTS",
"elemente": "ELEMENTS",
"kamera": "CAMERA",
"layer_combinations": "LAYER-COMBINATIONS",
"dossier_settings": "DOSSIER-SETTINGS",
"project_settings": "PROJECT-SETTINGS",
}
def _tag(mode):
return _MODE_LOG_TAG.get(mode, mode.upper())
def _read_lang():
"""Liest die UI-Sprache aus dossier_settings.json. Default: 'de'."""
try:
if os.path.isfile(_SETTINGS_PATH):
with open(_SETTINGS_PATH, "rb") as f:
d = json.loads(f.read().decode("utf-8"))
lang = d.get("lang", "de")
return lang if lang in ("de", "en") else "de"
except Exception:
pass
return "de"
# --- Timing-Instrumentierung ------------------------------------------------
@@ -45,16 +84,16 @@ def print_startup_summary():
print("[STARTUP] ===== SUMMARY =====")
print("[STARTUP] Wall-time (first to last mark): {:.1f} ms".format(total_wall))
print("[STARTUP] Sum of measured work: {:.1f} ms".format(total_work))
# Top-10 nach Dauer
# Top-10 by duration
top = sorted(_TIMINGS, key=lambda x: -x[2])[:10]
print("[STARTUP] --- Top-10 nach Dauer ---")
print("[STARTUP] --- Top-10 by duration ---")
for phase, label, ms in top:
print("[STARTUP] {:7.1f} ms {} / {}".format(ms, phase, label))
# Aggregat nach Phase
# Aggregate by phase
by_phase = {}
for phase, _, ms in _TIMINGS:
by_phase[phase] = by_phase.get(phase, 0.0) + ms
print("[STARTUP] --- Aggregat nach Phase ---")
print("[STARTUP] --- Aggregate by phase ---")
for phase, ms in sorted(by_phase.items(), key=lambda x: -x[1]):
print("[STARTUP] {:7.1f} ms {}".format(ms, phase))
@@ -96,7 +135,7 @@ def migrate_to_dossier(doc):
new = "dossier_" + suffix
try:
if doc.Strings.GetValue(new):
continue # Dossier-Variante vorhanden -> nicht ueberschreiben
continue # Dossier-Variante present -> nicht ueberschreiben
for prefix in _LEGACY_PREFIXES:
old_v = doc.Strings.GetValue(prefix + suffix)
if old_v:
@@ -190,7 +229,7 @@ class BaseBridge(object):
try:
data = json.loads(raw_str)
except Exception as ex:
print("[{}] JSON-Fehler: {}".format(self._mode.upper(), ex))
print("[{}] JSON-Fehler: {}".format(_tag(self._mode), ex))
return
if not isinstance(data, dict):
return
@@ -206,8 +245,8 @@ class BaseBridge(object):
self.handle(json.loads(full))
except Exception as ex:
import traceback
print("[{}] Chunk-Reassembly: {}".format(self._mode.upper(), ex))
print("[{}] Traceback:\n{}".format(self._mode.upper(), traceback.format_exc()))
print("[{}] Chunk-Reassembly: {}".format(_tag(self._mode), ex))
print("[{}] Traceback:\n{}".format(_tag(self._mode), traceback.format_exc()))
else:
self.handle(data)
@@ -265,10 +304,12 @@ def _build_inline_template():
html = f.read().decode("utf-8")
placeholder_script = '<script>' + _MODE_SCRIPT_PLACEHOLDER + '</script>'
_no_select = '<style>*{-webkit-user-select:none!important;user-select:none!important;}</style>'
_no_ctx = '<script>document.addEventListener("contextmenu",function(e){e.preventDefault();},true);</script>'
if "</head>" in html:
html = html.replace("</head>", placeholder_script + "</head>")
html = html.replace("</head>", placeholder_script + _no_select + _no_ctx + "</head>")
else:
html = placeholder_script + html
html = placeholder_script + _no_select + _no_ctx + html
def inline_css(m):
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
@@ -308,17 +349,18 @@ def load_inline(wv, mode, params=None):
if _INLINE_TEMPLATE is None or _INLINE_TEMPLATE[0] != cur_mtime:
sig, tmpl = _build_inline_template()
if tmpl is None:
print("[{}] dist nicht gefunden".format(mode.upper()))
print("[{}] dist not found".format(_tag(mode)))
return
_INLINE_TEMPLATE = (sig, tmpl)
# Per-Mount: nur das Mode-Script-Snippet bauen
parts = ['window.PANEL_MODE="{}";'.format(mode)]
parts = ['window.PANEL_MODE="{}";'.format(mode),
'window.DOSSIER_LANG="{}";'.format(_read_lang())]
if params is not None:
try:
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
except Exception as ex:
print("[{}] PANEL_PARAMS serialize: {}".format(mode.upper(), ex))
print("[{}] PANEL_PARAMS serialize: {}".format(_tag(mode), ex))
mode_script = ''.join(parts)
html = _INLINE_TEMPLATE[1].replace(_MODE_SCRIPT_PLACEHOLDER, mode_script)
t_loadhtml = time.time()
@@ -339,10 +381,10 @@ def attach_webview(panel, bridge, mode):
try:
bridge.handle_raw(title[10:])
except Exception as ex:
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
print("[{}] Message-Fehler: {}".format(_tag(mode), ex))
finally:
try:
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
wv.ExecuteScript("document.title='{}';".format(_tag(mode)))
except Exception:
pass
@@ -351,13 +393,22 @@ def attach_webview(panel, bridge, mode):
wv.ExecuteScript("window.RHINO_MODE=true;")
except Exception:
pass
try:
wv.ExecuteScript(
"var _ds=document.createElement('style');"
"_ds.textContent='*{-webkit-user-select:none!important;user-select:none!important;}';"
"document.head.appendChild(_ds);"
"document.addEventListener('contextmenu',function(e){e.preventDefault();},true);"
)
except Exception:
pass
def on_idle(s, e):
Rhino.RhinoApp.Idle -= on_idle
try:
load_inline(wv, mode)
except Exception as ex:
print("[{}] Inline-Fehler: {}".format(mode.upper(), ex))
print("[{}] Inline-Fehler: {}".format(_tag(mode), ex))
wv.DocumentTitleChanged += on_title
wv.DocumentLoaded += on_loaded
@@ -409,7 +460,7 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560),
if on_save is not None:
try: on_save(p)
except Exception as ex:
print("[{}] on_save: {}".format(mode.upper(), ex))
print("[{}] on_save: {}".format(_tag(mode), ex))
try: form.Close()
except Exception: pass
elif t == "CANCEL":
@@ -428,16 +479,24 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560),
try:
bridge.handle_raw(title_str[10:])
except Exception as ex:
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
print("[{}] Message-Fehler: {}".format(_tag(mode), ex))
finally:
try:
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
wv.ExecuteScript("document.title='{}';".format(_tag(mode)))
except Exception:
pass
def on_loaded(s, e):
try: wv.ExecuteScript("window.RHINO_MODE=true;")
except Exception: pass
try:
wv.ExecuteScript(
"var _ds=document.createElement('style');"
"_ds.textContent='*{-webkit-user-select:none!important;user-select:none!important;}';"
"document.head.appendChild(_ds);"
"document.addEventListener('contextmenu',function(e){e.preventDefault();},true);"
)
except Exception: pass
wv.DocumentTitleChanged += on_title_
wv.DocumentLoaded += on_loaded
@@ -449,7 +508,7 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560),
try:
load_inline(wv, mode, params=params)
except Exception as ex:
print("[{}] Inline-Fehler: {}".format(mode.upper(), ex))
print("[{}] Inline-Fehler: {}".format(_tag(mode), ex))
return form
@@ -545,7 +604,7 @@ def _try_load_png_white(png_path, size):
g.Dispose()
return target
except Exception as ex:
print("[panel_base] PNG-load failed:", ex)
print("[CORE] PNG-load failed:", ex)
return None
@@ -591,7 +650,7 @@ def _try_load_svg_white(svg_path, size):
g.Dispose()
return target
except Exception as ex:
print("[panel_base] SVG-load failed:", ex)
print("[CORE] SVG-load failed:", ex)
return None
@@ -680,10 +739,10 @@ def make_panel_icon(name_or_letter, bg_hex):
if os.path.isfile(png_path):
icon_bmp = _try_load_png_white(png_path, size - 8)
if icon_bmp is not None: chosen_path = png_path
else: print("[panel_base] PNG geladen aber Bitmap None:",
else: print("[CORE] PNG loaded but Bitmap is 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")
@@ -697,31 +756,32 @@ def make_panel_icon(name_or_letter, bg_hex):
size - 2*pad, size - 2*pad)
used_svg = True
used_material = True # → kein Letter-Fallback
print("[panel_base] Icon-Pfad: {}{}".format(
print("[CORE] Icon path: {}{}".format(
name_or_letter, chosen_path))
except Exception as ex:
print("[panel_base] Icon-Composite Fehler:", ex)
print("[CORE] Icon composite error:", ex)
except Exception as ex:
print("[panel_base] Icon-Pfad-Check:", ex)
print("[CORE] Icon path check error:", ex)
# 1) Material-Icon-Font (wenn keine SVG vorhanden)
# 1) Material-Icon-Font (wenn keine SVG present)
mat_cp = _MATERIAL_CODEPOINTS.get(name_or_letter)
if not used_svg and mat_cp is not None:
font_family_name = _try_material_font()
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,
drawing.Colors.White)
used_material = True
except Exception as ex:
print("[panel_base] Material-Render Fehler:", ex)
print("[CORE] Material render error:", ex)
used_material = False
# 2) Fallback: Buchstabe (erstes Zeichen bzw. eingegebener Buchstabe)
@@ -749,7 +809,7 @@ def make_panel_icon(name_or_letter, bg_hex):
tag, safe, bg_hex.lstrip("#")))
bmp.Save(path, drawing.ImageFormat.Png)
except Exception as ex:
print("[panel_base] Icon-Save:", ex)
print("[CORE] Icon save error:", ex)
path = None
# WICHTIG: Mac Rhinos RegisterPanel meldet "expected Icon, got Icon"
# wenn wir Eto.Drawing.Icon uebergeben — die API erwartet
@@ -759,35 +819,35 @@ def make_panel_icon(name_or_letter, bg_hex):
try:
import System.Drawing as _sd
ic = _sd.Icon(path)
print("[panel_base] Icon erzeugt via System.Drawing.Icon(path) [{}]".format(tag))
print("[CORE] Icon created via System.Drawing.Icon(path) [{}]".format(tag))
return ic
except Exception as ex:
print("[panel_base] System.Drawing.Icon(path) fehlgeschlagen:", ex)
print("[CORE] System.Drawing.Icon(path) failed:", ex)
# System.Drawing.Bitmap als Fallback (manche RegisterPanel-Overloads akzeptieren Bitmap)
try:
import System.Drawing as _sd
bmp_sd = _sd.Bitmap(path)
print("[panel_base] Icon erzeugt via System.Drawing.Bitmap(path) [{}]".format(tag))
print("[CORE] Icon created via System.Drawing.Bitmap(path) [{}]".format(tag))
return bmp_sd
except Exception as ex:
print("[panel_base] System.Drawing.Bitmap(path) fehlgeschlagen:", ex)
print("[CORE] System.Drawing.Bitmap(path) failed:", ex)
# Eto.Drawing.Icon als letzter Versuch — falls Rhino-Version anders ist
try:
ic = drawing.Icon(path)
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(path) [{}]".format(tag))
print("[CORE] Icon erzeugt via Eto.Drawing.Icon(path) [{}]".format(tag))
return ic
except Exception as ex:
print("[panel_base] Eto.Drawing.Icon(path) fehlgeschlagen:", ex)
print("[CORE] Eto.Drawing.Icon(path) failed:", ex)
# Bitmap-Fallback (in-memory) — wenn alles vorherige fehlschlaegt
try:
ic = drawing.Icon(1.0, bmp)
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(scale, bmp) [{}]".format(tag))
print("[CORE] Icon erzeugt via Eto.Drawing.Icon(scale, bmp) [{}]".format(tag))
return ic
except Exception: pass
print("[panel_base] Icon Fallback: Eto.Bitmap zurueck ({})".format(tag))
print("[CORE] Icon Fallback: Eto.Bitmap zurueck ({})".format(tag))
return bmp
except Exception as ex:
print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex)
print("[CORE] Icon-Erstellung failed:", ex)
return None
@@ -801,7 +861,7 @@ def find_plugin():
if p is not None:
return p
except Exception as ex:
print("[panel_base] Plugin-Suche:", ex)
print("[CORE] Plugin-Suche:", ex)
return None
@@ -822,7 +882,7 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
t_reg = time.time()
plugin = find_plugin()
if plugin is None:
print("[{}] Plugin nicht gefunden".format(mode.upper()))
print("[{}] Plugin not found".format(_tag(mode)))
return
try:
type_name = "DynPanel_" + mode
@@ -836,7 +896,7 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
try:
panel.MinimumSize = drawing.Size(int(min_size[0]), int(min_size[1]))
except Exception as ex:
print("[{}] MinimumSize konnte nicht gesetzt werden: {}".format(mode.upper(), ex))
print("[{}] MinimumSize konnte nicht set werden: {}".format(_tag(mode), ex))
# Auf einigen Eto-Versionen gibt es zusaetzlich Size/ClientSize
for attr in ("Size", "ClientSize"):
try:
@@ -855,14 +915,14 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
try:
icon = make_panel_icon(icon_spec[0], icon_spec[1])
except Exception as ex:
print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex))
print("[{}] Icon-Erstellung uebersprungen: {}".format(_tag(mode), ex))
icon = None
_t_mark("icon", mode, t_icon)
registered = False
registered_with_icon = False
# Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels
# akzeptieren auf manchen Versionen nur System.Drawing.Icon, das auf
# Mac nicht verfuegbar ist - die Registrierung ohne Icon ist OK).
# Mac not available ist - die Registrierung ohne Icon ist OK).
attempts = [(icon, True)] if icon is not None else []
attempts.append((None, False))
for arg, with_icon in attempts:
@@ -871,25 +931,25 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
registered = True
registered_with_icon = with_icon
if with_icon:
print("[{}] Panel mit Icon registriert ({})".format(
mode.upper(), type(arg).__name__))
print("[{}] Panel registered with icon ({})".format(
_tag(mode), type(arg).__name__))
break
except Exception as ex:
if with_icon:
print("[{}] RegisterPanel mit Icon fehlgeschlagen: {}".format(
mode.upper(), ex))
print("[{}] RegisterPanel mit Icon failed: {}".format(
_tag(mode), ex))
else:
print("[{}] RegisterPanel fehlgeschlagen: {}".format(
mode.upper(), ex))
print("[{}] RegisterPanel failed: {}".format(
_tag(mode), ex))
if registered and not registered_with_icon and icon is not None:
print("[{}] Panel ohne Icon registriert (Fallback)".format(mode.upper()))
print("[{}] Panel ohne Icon registriert (Fallback)".format(_tag(mode)))
if not registered:
return
sc.sticky[sticky_reg] = True
sc.sticky[sticky_guid] = System.Guid(guid_str)
print("[{}] Panel registriert".format(mode.upper()))
print("[{}] Panel registered".format(_tag(mode)))
except Exception as ex:
print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex))
print("[{}] Registrierung failed: {}".format(_tag(mode), ex))
return
_t_mark("register", mode, t_reg)
@@ -898,7 +958,7 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
guid = sc.sticky.get(sticky_guid, System.Guid(guid_str))
RhinoUI.Panels.OpenPanel(guid)
_t_mark("OpenPanel", mode, t_open)
print("[{}] Panel geoeffnet".format(mode.upper()))
print("[{}] Panel opened".format(_tag(mode)))
except Exception as ex:
print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex))
print("[{}] OpenPanel failed: {}".format(_tag(mode), ex))
_t_mark("register_and_open", mode, t_outer)
+12 -10
View File
@@ -1,5 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
schnitt_grips.py
Endpoint-Grips fuer Schnitt/Ansicht-Symbole im Plan.
@@ -98,7 +100,7 @@ def _update_linePts(doc, schnitt_id, new_p1, new_p2):
doc.Strings.SetString("dossier_zeichnungsebenen",
json.dumps(z_list, ensure_ascii=False))
except Exception as ex:
print("[SCHNITT_GRIPS] persist linePts:", ex)
print("[SECTION-GRIPS] persist linePts:", ex)
return False
# Symbol regenerieren — Layer aus altem Symbol uebernehmen
@@ -131,7 +133,7 @@ def _update_linePts(doc, schnitt_id, new_p1, new_p2):
if i == 0 and gid and gid != System.Guid.Empty:
first_new_id = gid
except Exception as ex:
print("[SCHNITT_GRIPS] add new symbol curve:", ex)
print("[SECTION-GRIPS] add new symbol curve:", ex)
# Neue Hauptlinie selektieren — damit der Conduit die Marker
# gleich wieder zeigt (sonst muesste der User nochmal klicken).
@@ -148,19 +150,19 @@ def _update_linePts(doc, schnitt_id, new_p1, new_p2):
if active_id == schnitt_id:
schnitte.activate_schnitt(doc, target, skip_view=True)
except Exception as ex:
print("[SCHNITT_GRIPS] re-activate:", ex)
print("[SECTION-GRIPS] re-activate:", ex)
# Panel-Broadcast (linePts haben sich geaendert, Ebenen-Panel will
# ggf. mit-rendern)
try:
import rhinopanel
import layers_panel as rhinopanel
rhinopanel._broadcast_state(doc)
except Exception: pass
try: doc.Views.Redraw()
except Exception: pass
return True
except Exception as ex:
print("[SCHNITT_GRIPS] update endpoint:", ex)
print("[SECTION-GRIPS] update endpoint:", ex)
return False
@@ -234,7 +236,7 @@ class _SchnittEndpointConduit(rd.DisplayConduit):
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
except Exception: pass
except Exception as ex:
print("[SCHNITT_GRIPS] DrawForeground:", ex)
print("[SECTION-GRIPS] DrawForeground:", ex)
# --- MouseCallback --------------------------------------------------------
@@ -289,7 +291,7 @@ class _SchnittMouseHandler(Rhino.UI.MouseCallback):
sid, z, kind, anchor_pt = hit
self._start_drag(view.Document, sid, z, kind, anchor_pt)
except Exception as ex:
print("[SCHNITT_GRIPS] OnMouseDown:", ex)
print("[SECTION-GRIPS] OnMouseDown:", ex)
def _start_drag(self, doc, schnitt_id, z, kind, anchor_pt):
if doc is None: return
@@ -368,7 +370,7 @@ class _SchnittMouseHandler(Rhino.UI.MouseCallback):
confirmed = bool(_update_linePts(
doc, schnitt_id, new_p1, new_p2))
except Exception as ex:
print("[SCHNITT_GRIPS] _start_drag:", ex)
print("[SECTION-GRIPS] _start_drag:", ex)
finally:
if not confirmed:
for pid in hidden_clip_ids:
@@ -404,6 +406,6 @@ def install_handlers():
handler.Enabled = True
sc.sticky[_STICKY_CONDUIT] = conduit
sc.sticky[_STICKY_HANDLER] = handler
print("[SCHNITT_GRIPS] Endpoint-Conduit + Mouse-Handler aktiv")
print("[SECTION-GRIPS] Endpoint conduit + mouse handler active")
except Exception as ex:
print("[SCHNITT_GRIPS] install:", ex)
print("[SECTION-GRIPS] install:", ex)
+41 -28
View File
@@ -1,5 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
schnitte.py
Schnitte + Ansichten als Zeichnungsebenen-Typ.
@@ -94,7 +96,7 @@ def _collect_viewport_ids(doc):
def find_schnitt_clip_objects(doc):
"""Findet alle Clipping-Plane-Objekte die zu einem aktiven Schnitt
gehoeren (UserString _KEY_SCHNITT_CLIP gesetzt)."""
gehoeren (UserString _KEY_SCHNITT_CLIP set)."""
out = []
try:
for obj in doc.Objects:
@@ -125,9 +127,9 @@ def clear_schnitt_clipping(doc):
if doc.Objects.Delete(obj.Id, True):
n += 1
except Exception as ex:
print("[SCHNITT] clear: {}".format(ex))
print("[SECTION] clear: {}".format(ex))
if n:
print("[SCHNITT] {} Clipping-Plane(s) entfernt".format(n))
print("[SECTION] {} Clipping-Plane(s) entfernt".format(n))
def _add_clipping_plane(doc, plane, du, dv, vp_ids, role):
@@ -135,7 +137,7 @@ def _add_clipping_plane(doc, plane, du, dv, vp_ids, role):
try:
gid = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
if gid is None or gid == System.Guid.Empty:
print("[SCHNITT] AddClippingPlane lieferte Empty Guid")
print("[SECTION] AddClippingPlane lieferte Empty Guid")
return None
obj = doc.Objects.FindId(gid)
if obj is None: return None
@@ -149,7 +151,7 @@ def _add_clipping_plane(doc, plane, du, dv, vp_ids, role):
doc.Objects.ModifyAttributes(obj, attrs, True)
return obj
except Exception as ex:
print("[SCHNITT] AddClippingPlane Fehler ({}):".format(role), ex)
print("[SECTION] AddClippingPlane Fehler ({}):".format(role), ex)
return None
@@ -179,13 +181,13 @@ def activate_schnitt(doc, z, skip_view=False):
if z is None: return
pts = z.get("linePts") or []
if len(pts) < 2:
print("[SCHNITT] '{}' hat keine linePts".format(z.get("name")))
print("[SECTION] '{}' hat keine linePts".format(z.get("name")))
return
try:
p1 = rg.Point3d(float(pts[0][0]), float(pts[0][1]), 0)
p2 = rg.Point3d(float(pts[1][0]), float(pts[1][1]), 0)
except Exception as ex:
print("[SCHNITT] linePts ungueltig:", ex)
print("[SECTION] linePts ungueltig:", ex)
return
dir_sign = 1 if int(z.get("dirSign", 1) or 1) >= 0 else -1
depth_back = max(0.5, float(z.get("depthBack", 8.0) or 8.0))
@@ -197,7 +199,7 @@ def activate_schnitt(doc, z, skip_view=False):
line_dir, view_dir, mid = _line_vectors(p1, p2, dir_sign)
if line_dir is None:
print("[SCHNITT] '{}' hat zu kurze Linie".format(z.get("name")))
print("[SECTION] '{}' hat zu kurze Linie".format(z.get("name")))
return
line_len = p1.DistanceTo(p2)
@@ -212,7 +214,7 @@ def activate_schnitt(doc, z, skip_view=False):
vp_ids = _collect_viewport_ids(doc)
if not vp_ids:
print("[SCHNITT] keine Viewports — Plane wuerde nichts schneiden")
print("[SECTION] keine Viewports — Plane wuerde nichts schneiden")
return
n_planes = 0
@@ -250,7 +252,7 @@ def activate_schnitt(doc, z, skip_view=False):
# Projektion: 'parallel' (klassischer Schnitt) oder 'perspective'
# (Schnittperspektive — perspektivische Section mit gleicher Cut-
# Logik). Bei perspective wird Kamera leicht naeher geholt + FOV
# gesetzt; Cut-Planes sind identisch.
# set; Cut-Planes sind identisch.
projection = (z.get("projection") or "parallel").strip().lower()
if projection not in ("parallel", "perspective"): projection = "parallel"
@@ -274,7 +276,7 @@ def activate_schnitt(doc, z, skip_view=False):
cam_dist = max(50.0, depth_back * 3 + line_len)
# Bei Perspektive: Kamera + Target auf cam_z. Bei Parallel:
# plane_z (Mitte Hoehenrange) — Z spielt eh keine Rolle
# fuers Bild, aber sauber gesetzt fuer konsistente
# fuers Bild, aber sauber set fuer konsistente
# Kamera-Ausrichtung.
view_z = cam_z if projection == "perspective" else plane_z
cam_pos = rg.Point3d(
@@ -291,6 +293,17 @@ def activate_schnitt(doc, z, skip_view=False):
vp.ChangeToParallelProjection(True)
vp.SetCameraLocations(target, cam_pos)
vp.CameraUp = rg.Vector3d(0, 0, 1)
# Display-Mode auf 'Dossier Plan' — auch bei Perspektive, damit
# die Section-Hatches sichtbar sind. User kann manuell wechseln.
try:
from Rhino.Display import DisplayModeDescription
for dm in DisplayModeDescription.GetDisplayModes():
try:
if dm.EnglishName == "Dossier Plan":
vp.DisplayMode = dm
break
except Exception: pass
except Exception: pass
# Zoom auf Schnitt-BoundingBox + etwas Rand. Bei Perspektive
# macht ZoomBoundingBox auch Sinn — Rhino passt das FOV-Frame
# entsprechend an.
@@ -303,10 +316,10 @@ def activate_schnitt(doc, z, skip_view=False):
vp.ZoomBoundingBox(bb)
view.Redraw()
except Exception as ex:
print("[SCHNITT] view setup:", ex)
print("[SECTION] view setup:", ex)
kind = "Schnitt" if cut_at_line else "Ansicht"
print("[SCHNITT] {} '{}' aktiviert: {} Plane(s), depthBack={:.1f}m".format(
print("[SECTION] {} '{}' aktiviert: {} Plane(s), depthBack={:.1f}m".format(
kind, z.get("name"), n_planes, depth_back))
@@ -352,7 +365,7 @@ def save_pre_schnitt_view(doc):
except Exception: key = _STICKY_PRE_VIEW + "_default"
sc.sticky[key] = snap
except Exception as ex:
print("[SCHNITT] save view:", ex)
print("[SECTION] save view:", ex)
def restore_pre_schnitt_view(doc):
@@ -380,10 +393,10 @@ def restore_pre_schnitt_view(doc):
view.Redraw()
try: del sc.sticky[key]
except Exception: pass
print("[SCHNITT] Pre-Schnitt-View restored")
print("[SECTION] Pre-Schnitt-View restored")
return True
except Exception as ex:
print("[SCHNITT] restore view:", ex)
print("[SECTION] restore view:", ex)
return False
@@ -436,7 +449,7 @@ def delete_schnitt_entry(doc, schnitt_id):
doc.Strings.SetString("dossier_zeichnungsebenen",
json.dumps(new_lst, ensure_ascii=False))
except Exception as ex:
print("[SCHNITT] delete entry:", ex)
print("[SECTION] delete entry:", ex)
return False
cleanup_schnitt_artifacts(doc, schnitt_id, active_id=active_id)
return True
@@ -486,7 +499,7 @@ def create_schnitt_entry(doc, name, p1, p2, dir_sign=1, depth_back=8.0,
doc.Strings.SetString("dossier_zeichnungsebenen",
json.dumps(lst, ensure_ascii=False))
except Exception as ex:
print("[SCHNITT] persist entry:", ex)
print("[SECTION] persist entry:", ex)
return None
# 2D-Symbol auf Plan
@@ -500,7 +513,7 @@ def create_schnitt_entry(doc, name, p1, p2, dir_sign=1, depth_back=8.0,
attrs.SetUserString(_KEY_SCHNITT_ID, schnitt_id)
doc.Objects.AddCurve(crv, attrs)
except Exception as ex:
print("[SCHNITT] add symbol curve:", ex)
print("[SECTION] add symbol curve:", ex)
return schnitt_id
@@ -526,13 +539,13 @@ def activate_schnitt_by_id(doc, schnitt_id):
or sc.sticky.get("zeichnungsebenen_bridge_ref")
if eb is None:
# Fallback: direkt aktivieren ohne broadcast
print("[SCHNITT] keine EbenenBridge — direkt aktivieren")
print("[SECTION] keine EbenenBridge — direkt aktivieren")
activate_schnitt(doc, z)
return True
eb._set_active_zeichnungsebene(z)
return True
except Exception as ex:
print("[SCHNITT] activate_by_id:", ex)
print("[SECTION] activate_by_id:", ex)
return False
@@ -543,7 +556,7 @@ class _SchnittDoubleClickHandler(Rhino.UI.MouseCallback):
Wichtig: die Klicks selektieren das Curve vorab (Rhino-Default), wir
pruefen also einfach die aktuelle Selection. Bei Treffer wird der
Schnitt aktiviert + e.Cancel=True gesetzt damit Rhinos default
Schnitt aktiviert + e.Cancel=True set damit Rhinos default
Edit-Modus nicht zusaetzlich aufpoppt."""
def OnMouseDoubleClick(self, e):
try:
@@ -564,7 +577,7 @@ class _SchnittDoubleClickHandler(Rhino.UI.MouseCallback):
return
except Exception: pass
except Exception as ex:
print("[SCHNITT] OnMouseDoubleClick:", ex)
print("[SECTION] OnMouseDoubleClick:", ex)
def install_double_click_handler():
@@ -580,9 +593,9 @@ def install_double_click_handler():
h = _SchnittDoubleClickHandler()
h.Enabled = True
sc.sticky["_dossier_schnitt_dblclick_handler"] = h
print("[SCHNITT] Doppelklick-Handler aktiv")
print("[SECTION] Double-click handler active")
except Exception as ex:
print("[SCHNITT] install_double_click_handler:", ex)
print("[SECTION] install_double_click_handler:", ex)
def pick_schnitt_interactive(doc, defaults=None):
@@ -596,7 +609,7 @@ def pick_schnitt_interactive(doc, defaults=None):
# Project-Defaults als 2-stufiges Fallback (defaults > project > hardcoded)
proj_d = {}
try:
import rhinopanel
import layers_panel as rhinopanel
ps = rhinopanel.load_project_settings(doc) or {}
proj_d = ps.get("defaults", {}) or {}
except Exception: pass
@@ -629,7 +642,7 @@ def pick_schnitt_interactive(doc, defaults=None):
p2 = rg.Point3d(p2.X, p2.Y, 0)
if p1.DistanceTo(p2) < 0.01:
print("[SCHNITT] Linie zu kurz")
print("[SECTION] Linie zu kurz")
return None
# Pick Blickrichtung (welche Seite ist "hinten")
@@ -674,7 +687,7 @@ def pick_schnitt_interactive(doc, defaults=None):
sym_path = _el._layer_path_schnittlinie(doc, geschoss["name"])
symbol_layer_idx = _el._ensure_layer(doc, sym_path)
except Exception as ex:
print("[SCHNITT] symbol-layer resolve:", ex)
print("[SECTION] symbol-layer resolve:", ex)
sid = create_schnitt_entry(doc, auto_name, p1, p2,
dir_sign=dir_sign, depth_back=depth_back,
+277 -22
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
"""
startup.py
Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine
@@ -13,16 +15,28 @@ import json
import Rhino
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
# Splash SOFORT als allererstes — bevor irgendwas anderes passiert, damit der
# Nutzer waehrend Python-Imports + Panel-Registrierung nicht in eine schwarze
# Rhino-Oberflaeche schaut. Skipt automatisch wenn Launcher seinen eigenen
# Splash zeigt (Owner-Marker-Check).
# Skipt auch wenn Plugin bereits in dieser Session geladen ist (z.B. Cmd+N).
if not sc.sticky.get("_dossier_startup_scheduled"):
try:
import _startup_splash as _splash_first
_splash_first.show()
except Exception as _ex_splash:
print("[STARTUP] splash error:", _ex_splash)
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
print("[STARTUP] Python: {}".format(sys.version))
print("[STARTUP] Implementation: {}".format(
sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)"))
print("[STARTUP] Platform: {}".format(sys.platform))
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
# Pfad zur Custom-UI (Toolbars/Sidebar) — wird einmal pro Session geladen
_UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
@@ -30,13 +44,13 @@ _UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
# Muss synchron sein mit launcher/modules.json. Wenn neue Module dazukommen,
# beide Stellen pflegen.
_MODULE_TO_PY = {
"ebenen": "rhinopanel",
"oberleiste": "oberleiste",
"ebenen": "layers_panel",
"oberleiste": "toolbar",
"ausschnitte": "ausschnitte",
"gestaltung": "gestaltung",
"werkzeuge": "werkzeuge",
"gestaltung": "styles",
"werkzeuge": "tools",
"overrides": "overrides_panel",
"dimensionen": "dimensionen",
"dimensionen": "dimensions",
"layouts": "layouts",
"elemente": "elemente",
}
@@ -74,6 +88,178 @@ def _migrate_active_doc(*_):
print("[STARTUP] Migration:", ex)
_DOC_FLAG_VIEW_MODES = "dossier_view_modes_initialized"
def _assign_default_display_modes(doc):
"""Setzt einmalig pro Doc die Display-Modes auf die Dossier-Defaults:
- Parallel-Projektionen (Top/Front/Right/Schnitt-parallel) -> 'Dossier Plan'
- Perspektive (Perspective/Schnittperspektive) -> 'Dossier 3D'
Persistiert einen Flag in doc.Strings laeuft nur EINMAL pro Doc.
User-Overrides (manuelles Wechseln) bleiben damit erhalten.
"""
if doc is None: return
try:
if doc.Strings.GetValue(_DOC_FLAG_VIEW_MODES) == "1":
return # schon initialisiert
except Exception: pass
try:
from Rhino.Display import DisplayModeDescription
except Exception as ex:
print("[STARTUP] view-modes: DMD not available:", ex); return
# Mode-Lookup per Name
mode_plan = mode_3d = None
try:
for dm in DisplayModeDescription.GetDisplayModes():
try:
n = dm.EnglishName
if n == "Dossier Plan": mode_plan = dm
elif n == "Dossier 3D": mode_3d = dm
except Exception: pass
except Exception as ex:
print("[STARTUP] view-modes: mode list error:", ex); return
if mode_plan is None and mode_3d is None:
print("[STARTUP] view-modes: no Dossier display modes found — skip")
return
n_set = 0
try:
for view in doc.Views:
try:
vp = view.ActiveViewport
if vp is None: continue
is_par = bool(vp.IsParallelProjection)
target = mode_plan if is_par else mode_3d
if target is None: continue
try:
vp.DisplayMode = target
n_set += 1
except Exception as ex:
print("[STARTUP] view-modes set ({}): {}".format(
vp.Name, ex))
except Exception: pass
try:
doc.Views.Redraw()
except Exception: pass
except Exception as ex:
print("[STARTUP] view-modes iterate:", ex)
try:
doc.Strings.SetString(_DOC_FLAG_VIEW_MODES, "1")
except Exception: pass
print("[STARTUP] view-modes: {} viewport(s) set".format(n_set))
_DOC_FLAG_VIEW_MAXIMIZED = "dossier_top_view_maximized"
def _maximize_top_view(doc):
"""Maximiert den Top-Viewport (= einzige aktive View statt 4-Viewport-
Default). Persistiert Flag in doc.Strings laeuft nur EINMAL pro Doc.
User-Overrides (manuelles Wechseln zu 4-View etc) bleiben erhalten."""
if doc is None: return
try:
if doc.Strings.GetValue(_DOC_FLAG_VIEW_MAXIMIZED) == "1":
return # schon initialisiert
except Exception: pass
try:
top_view = None
for view in doc.Views:
try:
vp = view.ActiveViewport
if vp is None: continue
if vp.Name == "Top":
top_view = view
break
except Exception: pass
if top_view is None:
print("[STARTUP] view-max: no Top viewport found")
return
try:
top_view.Maximized = True
doc.Views.ActiveView = top_view
doc.Views.Redraw()
print("[STARTUP] view-max: Top viewport maximized")
except Exception as ex:
print("[STARTUP] view-max set error:", ex); return
try:
doc.Strings.SetString(_DOC_FLAG_VIEW_MAXIMIZED, "1")
except Exception: pass
except Exception as ex:
print("[STARTUP] view-max:", ex)
_DOC_FLAG_UNIT_CHECKED = "dossier_unit_checked"
def _check_doc_unit(doc):
"""Prueft ob doc.ModelUnitSystem der DOSSIER-Project-Setting-Arbeitseinheit
entspricht. Bei Mismatch: Modal-Dialog mit "Umstellen" / "Spaeter"-Option.
Idempotent pro Doc via doc.Strings-Flag wird nur EINMAL pro Doc gefragt.
Wenn User "Spaeter" waehlt, fragt DOSSIER beim selben Doc nicht mehr (Flag
bleibt set). Fuer erneute Frage: doc.Strings-Key loeschen.
"""
if doc is None: return
try:
if doc.Strings.GetValue(_DOC_FLAG_UNIT_CHECKED) == "1":
return
except Exception: pass
try:
import layers_panel as rhinopanel
target_unit_str = rhinopanel.get_project_unit(doc)
target_unit_enum = rhinopanel.get_project_unit_enum(doc)
except Exception as ex:
print("[STARTUP] unit-check: project setting read error:", ex)
return
if target_unit_enum is None: return
try:
current = doc.ModelUnitSystem
except Exception:
return
if current == target_unit_enum:
# Schon passend → einmalig Flag setzen, beim naechsten Open kein Check
try: doc.Strings.SetString(_DOC_FLAG_UNIT_CHECKED, "1")
except Exception: pass
return
# Mismatch — Dialog zeigen
try:
import Eto.Forms as ef
msg = ("Dieses Doc ist in '{}'.\n"
"DOSSIER-Projekteinstellung: '{}'.\n\n"
"Doc auf '{}' umstellen?\n"
"(Bestehende Geometrie wird skaliert)").format(
str(current), target_unit_str, target_unit_str)
result = ef.MessageBox.Show(
msg, "DOSSIER — Arbeitseinheit",
ef.MessageBoxButtons.YesNo,
ef.MessageBoxType.Question)
try:
doc.Strings.SetString(_DOC_FLAG_UNIT_CHECKED, "1")
except Exception: pass
if str(result).lower().endswith("yes"):
# _-Units _<unit> _Yes konvertiert Geometrie automatisch mit
unit_cmd = {"meters": "_Meters",
"millimeters": "_Millimeters",
"centimeters": "_Centimeters"}.get(target_unit_str)
if unit_cmd:
try:
Rhino.RhinoApp.RunScript(
"_-Units _Model {} _Yes _EnterEnd".format(unit_cmd),
False)
print("[STARTUP] Doc auf {} umgestellt (Geometrie skaliert)".format(
target_unit_str))
except Exception as ex:
print("[STARTUP] unit-convert RunScript:", ex)
else:
print("[STARTUP] User declined unit change — doc stays {}".format(current))
except Exception as ex:
print("[STARTUP] unit-check dialog error:", ex)
def _on_doc_opened(sender, e):
"""Greift bei jedem geoeffneten Doc nach Rhino-Start. Migration ist
idempotent (Flag in doc.Strings)."""
@@ -81,6 +267,9 @@ def _on_doc_opened(sender, e):
doc = e.Document if hasattr(e, "Document") else Rhino.RhinoDoc.ActiveDoc
import panel_base
panel_base.migrate_to_dossier(doc)
_assign_default_display_modes(doc)
_maximize_top_view(doc)
_check_doc_unit(doc)
except Exception as ex:
print("[STARTUP] _on_doc_opened:", ex)
@@ -92,9 +281,9 @@ def _hint_dossier_ui():
manuell laden kann. Rhino merkt sich die Anordnung dann persistent."""
if not os.path.isfile(_UI_FILE):
return
print("[STARTUP] DOSSIERUI gefunden: {}".format(_UI_FILE))
print("[STARTUP] Einmalig laden: Window -> Window Layout -> Open -> obige Datei")
print("[STARTUP] Anordnung bleibt danach ueber Rhino-Restarts erhalten.")
print("[STARTUP] DOSSIERUI found: {}".format(_UI_FILE))
print("[STARTUP] Load once: Window -> Window Layout -> Open -> file above")
print("[STARTUP] Layout persists across Rhino restarts.")
def _load_all(sender, e):
@@ -103,7 +292,8 @@ def _load_all(sender, e):
Rhino.RhinoApp.Idle -= _load_all
except Exception:
pass
print("[STARTUP] Lade DOSSIER-Panels...")
# Splash wird ganz oben in startup.py (vor diesem Idle) gezeigt.
print("[STARTUP] Loading DOSSIER panels...")
# Migration einmal fuer das beim Start aktive Doc
_migrate_active_doc()
# Und Listener fuer spaeter geoeffnete Docs registrieren
@@ -111,34 +301,92 @@ def _load_all(sender, e):
Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened
except Exception as ex:
print("[STARTUP] EndOpenDocument-Hook:", ex)
try:
Rhino.RhinoDoc.NewDocument += _on_doc_opened
except Exception as ex:
print("[STARTUP] NewDocument-Hook:", ex)
# Projekt-Config bestimmt, welche Module geladen werden. Ohne Config
# (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles.
config = _read_project_config()
if config and isinstance(config.get("modules"), list):
enabled_ids = [m for m in config["modules"] if m in _MODULE_TO_PY]
unknown = [m for m in config["modules"] if m not in _MODULE_TO_PY]
print("[STARTUP] Projekt: '{}'".format(config.get("name") or "?"))
print("[STARTUP] Aktivierte Module: {}".format(", ".join(enabled_ids) or "(keine)"))
print("[STARTUP] Project: '{}'".format(config.get("name") or "?"))
print("[STARTUP] Active modules: {}".format(", ".join(enabled_ids) or "(keine)"))
if unknown:
print("[STARTUP] Unbekannte Modul-IDs in Config: {}".format(unknown))
print("[STARTUP] Unknown module IDs in config: {}".format(unknown))
else:
enabled_ids = _ALL_MODULES
print("[STARTUP] Keine dossier.project.json — alle Module laden")
print("[STARTUP] No dossier.project.json — loading all modules")
# massstab.py wird als Library mitgeladen (von oberleiste/ausschnitte/...)
# und braucht hier nicht mehr als eigenstaendiges Panel zu erscheinen.
# Imports messen — das ist der grosse Block der bisher unmeasured war
import time as _t
import panel_base as _pb
for mod_id in enabled_ids:
py_name = _MODULE_TO_PY[mod_id]
_t_imp = _t.time()
try:
__import__(py_name)
_pb._t_mark("import", mod_id, _t_imp)
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
except Exception as ex:
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
print("[STARTUP] {} ({}) ERROR: {}".format(mod_id, py_name, ex))
# Text-Editor Doppelklick-Hook fuer DOSSIER-Texte
_t_te = _t.time()
try:
import text_editor
text_editor._ensure_double_click_hook()
_pb._t_mark("hook", "text_editor", _t_te)
except Exception as ex:
print("[STARTUP] text_editor hook:", ex)
# Display-Modes auf Default fuer aktives Doc setzen (einmalig)
_t_vm = _t.time()
try:
_assign_default_display_modes(Rhino.RhinoDoc.ActiveDoc)
_pb._t_mark("post_init", "view_modes", _t_vm)
except Exception as ex:
print("[STARTUP] view-modes assign error:", ex)
# Top-View maximieren (= einzige aktive View statt 4-View Default)
try:
_maximize_top_view(Rhino.RhinoDoc.ActiveDoc)
except Exception as ex:
print("[STARTUP] view-max:", ex)
# Unit-Check fuer das beim Start aktive Doc — fragt einmal pro Doc
# wenn doc.ModelUnitSystem != Project-Setting
_t_uc = _t.time()
try:
_check_doc_unit(Rhino.RhinoDoc.ActiveDoc)
_pb._t_mark("post_init", "unit_check", _t_uc)
except Exception as ex:
print("[STARTUP] unit-check active doc error:", ex)
# Aliases + Shortcuts (Defaults aus rhino/aliases/shortcuts_default.json
# + User-Overrides aus dossier_settings.json) registrieren. Idempotent —
# SetMacro/SetMacro ueberschreibt presente 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 error:", 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-
@@ -165,11 +413,18 @@ def _load_all(sender, e):
with open(os.path.join(marker_dir, "plugin_loaded.flag"), "w") as f:
f.write("ok")
except Exception as ex:
print("[STARTUP] marker schreiben:", ex)
print("[STARTUP] marker write error:", ex)
import threading
threading.Timer(2.0, _write_marker).start()
print("[STARTUP] Fertig")
print("[STARTUP] Done")
Rhino.RhinoApp.Idle += _load_all
print("[STARTUP] geplant - laedt sobald Rhino idle ist")
# 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] already scheduled — skip (parallel call)")
else:
sc.sticky["_dossier_startup_scheduled"] = True
Rhino.RhinoApp.Idle += _load_all
print("[STARTUP] scheduled — loads when Rhino is idle")
File diff suppressed because it is too large Load Diff
+6 -4
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
"""
swisstopo.py
STAC-API-Client + GeoTIFF/XYZ-Parser + Mesh-Builder fuer swisstopo-Daten.
@@ -707,7 +709,7 @@ def merge_grids(grids):
def mesh_from_grid(grid, origin_shift=(0, 0, 0), unit_scale=1.0):
"""Baut ein Rhino-Mesh aus dem XYZ-Grid. origin_shift wird auf jeden
Vertex angewendet (typisch: bbox-Center zu Welt-0/0/0 schieben).
Vertex applied (typisch: bbox-Center zu Welt-0/0/0 schieben).
unit_scale: Skalierung von Meter (Quelle XYZ) auf Doc-Units. Bei
mm-Doc = 1000, bei m-Doc = 1.0 ."""
es = grid["es"]; ns = grid["ns"]
@@ -897,7 +899,7 @@ def generate_patch_from_contours(doc, contour_curves, progress=None):
brep = rg.Brep.CreatePatch(
geom_list, u_spans, v_spans, doc.ModelAbsoluteTolerance)
if brep is None:
if progress: progress("Patch fehlgeschlagen (None zurueck)")
if progress: progress("Patch failed (None zurueck)")
return None
gid = doc.Objects.AddBrep(brep)
if gid and gid != System.Guid.Empty:
@@ -1025,7 +1027,7 @@ def volumize_terrain_object(doc, top_obj, depth_doc, progress=None):
old_id = top_obj.Id
new_gid = doc.Objects.AddMesh(vol, attrs)
if new_gid is None or new_gid == System.Guid.Empty:
if progress: progress("Volumize: AddMesh fehlgeschlagen")
if progress: progress("Volumize: AddMesh failed")
return None
doc.Objects.Delete(old_id, True)
new_obj = doc.Objects.Find(new_gid)
@@ -1135,7 +1137,7 @@ def _geotiff_to_png(tif_path, max_dim=2048):
img.width, img.height))
return png_path
except ImportError:
print("[SWISSTOPO] Pillow nicht verfuegbar — versuche Eto.Drawing")
print("[SWISSTOPO] Pillow not available — versuche Eto.Drawing")
except Exception as ex:
print("[SWISSTOPO] Pillow-convert fail:", ex)
# --- Variante 2: Eto.Drawing (Mac NSImage liest TIFF)
+30
View File
@@ -0,0 +1,30 @@
# Display-Mode-Templates
## dossier_plan.ini — was wir wollen
Das Plugin liest **`dossier_plan.ini`** aus diesem Ordner und lädt den
Dossier-Plan-Display-Mode daraus. So fügst du den Mode hinzu:
1. In Rhino: Display-Mode-Editor öffnen (Settings → View → Display Modes)
2. „Dossier Plan" auswählen (oder einen neuen Mode anlegen + benennen)
3. Settings so einstellen wie du sie haben willst:
- General → Visibility → „Show HiddenLines" **AUS**
- Clipping plane objects → „Use section styles" **AN**
- Curves, Mesh-Wires, Iso-Curves: alles aus
- Hintergrund weiss
- Was auch immer du brauchst…
4. Apply + OK
5. Display-Mode-Editor: rechts-klick auf „Dossier Plan" → **Export…**
6. Datei speichern als: `~/STUDIO/DOSSIER/rhino/templates/dossier_plan.ini`
Beim nächsten Reload (`_RunPythonScript /…/_reset_panels.py`) lädt das
Plugin den Mode aus dieser ini direkt. Name + Guid werden automatisch
auf „Dossier Plan" + `d0551e72-7e72-4170-b1a4-d0551e72d055` umgesetzt, alles
andere bleibt 1:1 wie du's exportiert hast.
## Fallback wenn keine ini da
Wenn `dossier_plan.ini` nicht existiert, klont das Plugin den
**Technical**-Mode und patcht ihn programmatisch (mit den paar Settings
die wir kennen). Funktioniert aber nicht so robust — Template ist
sauberer.
+323
View File
@@ -0,0 +1,323 @@
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d]
PipelineId=e1eb7363-87f2-4a2b-a861-256e77835369
SupportsShading=y
SupportsStereo=y
AddToMenu=y
AllowObjectAssignment=y
ShadedPipelineRequired=y
WireframePipelineRequired=y
PipelineLocked=y
Order=-5
DerivedFrom=00000000-0000-0000-0000-000000000000
Name=Dossier 3D
XrayAllObjects=n
IgnoreHighlights=n
DisableConduits=n
DisableTransparency=n
BBoxMode=0
RealtimeDisplayId=00000000-0000-0000-0000-000000000000
SupportsShadeCmd=y
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Lighting]
ShadowIntensity=40
ShadowClippingRadius=0
ShadowClippingUsage=0
PerPixelLighting=n
TransparencyTolerance=40
ShadowBlur=0
ShadowBias=10,12,0
ShowLights=n
UseHiddenLights=n
UseLightColor=n
LightingScheme=2
Luminosity=0
AmbientColor=0,0,0
LightCount=0
CastShadows=y
ShadowMapSize=2048
SkylightShadowQuality=4
NumSamples=4
ShadowMapType=2
ShadowBitDepth=32
ShadowColor=0,0,0
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects]
CPHidePoints=n
CPHighlight=y
CPHidden=n
nCPWireThickness=1
LockedUsage=2
LockedTrans=50
GhostLockedObjects=n
ClipSectionUsage=0
CPColor=0,0,0
eCVStyle=102
nCVSize=3
LayersFollowLockUsage=n
LockedObjectsBehind=n
LockedColor=100,100,100
CPSolidLines=n
CPSingleColor=n
CPHideSurface=n
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Annotations]
DotBorderColor=-1
ShowText=y
ShowAnnotations=y
DotTextColor=-1
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Curves]
ShowCurves=y
CurveThicknessUsage=0
LineJoinStyle=0
LineEndCapStyle=0
CurvePattern=-1
CurveTrans=0
CurveThickness=1
CurveColor=0,0,0
SingleCurveColor=n
ShowCurvatureHair=n
CurveThicknessScale=1
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Meshes]
ShowMeshNakedEdges=n
MeshNonmanifoldEdgeColor=0,0,0
MeshNakedEdgeColor=0,0,0
MeshEdgeColor=0,0,0
MeshNonmanifoldEdgeColorReduction=0
MeshNakedEdgeColorReduction=0
MeshEdgeColorReduction=0
MeshNonmanifoldEdgeThickness=0
MeshNakedEdgeThickness=2
MeshEdgeThickness=2
ShowMeshNonmanifoldEdges=n
ShowMeshEdges=n
MeshVertexSize=0
ShowMeshVertices=n
ShowMeshWires=n
MeshWirePattern=-1
MeshWireThickness=1
MeshWireColor=0,0,0
SingleMeshWireColor=n
HighlightMeshes=n
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Points]
PCSize=2
ShowPoints=y
PointStyle=51
PointSize=3
PCGripStyle=102
PCGripSize=2
PCStyle=50
ShowPointClouds=y
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\SubD]
CreaseVisible=n
CreaseUsage=0
CreaseColor=255,255,255
CreaseColorReduction=0
CreaseThickness=2
CreaseThicknessScale=1
CreaseApplyPattern=y
NonmanifoldVisible=n
NonmanifoldUsage=0
NonmanifoldColor=255,255,255
NonmanifoldColorReduction=0
NonmanifoldThickness=1
NonmanifoldThicknessScale=1
NonmanifoldApplyPattern=y
BoundaryVisible=n
BoundaryUsage=0
BoundaryColor=255,255,255
SmoothColorReduction=0
SmoothColor=255,255,255
ShowSymmetryAxis=y
SmoothVisible=n
SubDThicknessUsage=0
BoundaryColorReduction=0
BoundaryThickness=2
SmoothThickness=1
BoundaryThicknessScale=2
BoundaryApplyPattern=y
ShowReflectedPlane=y
SmoothUsage=0
PlaneColorUsage=2
SymmetryAxisThickness=0.02500000037252903
ReflectedColor=64,64,64
ColorPercentage=30
AxisColor=255,0,0
SmoothThicknessScale=1
SmoothApplyPattern=y
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Surfaces]
EdgeColorReduction=0
SurfaceKappaHair=n
HighlightSurfaces=n
ShowIsocurves=n
IsoThicknessUsed=n
IsocurveThickness=1
IsoUThickness=1
IsoVThickness=1
IsoWThickness=1
SingleIsoColor=n
IsoColor=0,0,0
IsoColorsUsed=n
IsoUColor=0,0,0
IsoVColor=0,0,0
IsoWColor=0,0,0
IsoPatternUsed=n
IsocurvePattern=-1
IsoUPattern=-1
IsoVPattern=-1
IsoWPattern=-1
ShowEdges=y
ShowNakedEdges=n
ShowTangentEdges=n
ShowTangentSeams=n
ShowNonmanifoldEdges=n
ShowEdgeEndpoints=n
EdgeThickness=1
EdgeColorUsage=0
NakedEdgeThickness=2
NakedEdgeColorUsage=0
NakedEdgeColorReduction=0
EdgeColor=0,0,0
NakedEdgeColor=0,0,0
NonmanifoldEdgeColor=0,0,0
EdgePattern=-1
NakedEdgePattern=-1
NonmanifoldEdgePattern=-1
SurfaceThicknessUsage=0
SurfaceEdgeThicknessScale=2
SurfaceEdgeApplyPattern=y
SurfaceNakedEdgeUseNormalEdgeThickness=y
SurfaceNakedEdgeThicknessScale=2
SurfaceNakedEdgeApplyPattern=y
SurfaceIsoUThicknessScale=1
SurfaceIsoVThicknessScale=1
SurfaceIsoWThicknessScale=1
SurfaceIsoUApplyPattern=y
SurfaceIsoVApplyPattern=y
SurfaceIsoWApplyPattern=y
ShowFlatSurfaceIsos=n
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Technical]
TEThickness=1
TCThickness=1
TSThickness=1
TIThickness=1
THColor=0,0,0
TEColor=0,0,0
TSiColor=0,0,0
TCColor=0,0,0
TSColor=0,0,0
TIColor=0,0,0
TechnicalMask=14
TechnicalUsageMask=0
THThickness=1
TSiThickness=1
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading]
ParallelLineRotation=0
CullBackfaces=n
ShadeVertexColors=n
SingleWireColor=n
WireColor=0,0,0
ShadeSurface=y
UseObjectMaterial=n
UseObjectBFMaterial=n
BakeTextures=y
ShowDecals=y
SurfaceColorWriting=y
ShadingEffect=0
ParallelLineWidth=2
ParallelLineSeparation=3
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material]
BackIsCustom=y
FrontIsCustom=n
UseBackMaterial=y
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Back Material]
ShineIntensity=100
Luminosity=0
Reflectivity=0
Transparency=0
Specular=255,255,255
Shine=0
Diffuse=126,126,126
OverrideObjectReflectivity=y
OverrideObjectTransparency=y
OverrideObjectColor=n
FlatShaded=n
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Back Material\BitmapTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Back Material\EmapTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Back Material\TransparencyTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Front Material]
Diffuse=126,126,126
Specular=255,255,255
Shine=128
ShineIntensity=100
Reflectivity=0
Transparency=0
FlatShaded=y
OverrideObjectColor=n
OverrideObjectTransparency=y
OverrideObjectReflectivity=y
Luminosity=0
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Front Material\BitmapTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Front Material\EmapTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Front Material\TransparencyTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\View settings]
ShowClippingPlanes=n
FlipGlasses=n
AGViewingMode=0
AGColorMode=0
StereoParallax=1
StereoSeparation=1
StereoModeEnabled=0
BackgroundBitmap=
GradBotRight=210,210,210
WzColor=0,0,150
GridTrans=60
WyColor=75,150,75
WxColor=150,75,75
WorldAxesColor=0
GridPlaneColor=0,0,0
PlaneUsesGridColor=n
AxesPercentage=100
PlaneVisibility=0
GridPlaneTrans=90
DrawTransGridPlane=n
GradTopRight=240,240,240
GradBotLeft=210,210,210
GradTopLeft=240,240,240
SolidColor=250,250,250
FillMode=2
CustomLinearWorkflowPostProcessGamma=2.200000047683716
CustomLinearWorkflowPreProcessGamma=2.200000047683716
CustomLinearWorkflowPostProcessFrameBuffer=n
CustomLinearWorkflowPreProcessTextures=y
CustomLinearWorkflowPreProcessColors=y
LinearWorkflowUsage=0
CustomGroundPlaneShadowOnly=y
CustomGroundPlaneAutomaticAltitude=y
CustomGroundPlaneAltitude=0
CustomGroundPlaneShow=y
GroundPlaneUsage=1
UseDocumentGrid=n
DrawGrid=n
DrawAxes=n
DrawZAxis=n
DrawWorldAxes=n
ShowGridOnTop=n
ShowTransGrid=n
BlendGrid=n
VertScale=1
HorzScale=1
ClippingCPColor=255,255,255
ClippingEdgeColor=0,0,0
ClippingSurfaceColor=128,128,128
ClippingEdgeThickness=3
ClippingCPTrans=95
ClippingCPUsage=0
ClippingEdgesUsage=2
ClippingSurfaceUsage=0
ClippingShowCP=n
ClippingClipSelected=y
ClippingShowXEdges=y
ClippingShowXSurface=y
+323
View File
@@ -0,0 +1,323 @@
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055]
PipelineId=e1eb7363-87f2-4a2b-a861-256e77835369
SupportsShading=y
SupportsStereo=y
AddToMenu=y
AllowObjectAssignment=y
ShadedPipelineRequired=y
WireframePipelineRequired=y
PipelineLocked=y
Order=-6
DerivedFrom=00000000-0000-0000-0000-000000000000
Name=Dossier Plan
XrayAllObjects=n
IgnoreHighlights=n
DisableConduits=n
DisableTransparency=n
BBoxMode=0
RealtimeDisplayId=00000000-0000-0000-0000-000000000000
SupportsShadeCmd=y
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Lighting]
ShadowIntensity=100
ShadowClippingRadius=0
ShadowClippingUsage=0
PerPixelLighting=n
TransparencyTolerance=40
ShadowBlur=0
ShadowBias=10,12,0
ShowLights=n
UseHiddenLights=n
UseLightColor=n
LightingScheme=0
Luminosity=0
AmbientColor=0,0,0
LightCount=0
CastShadows=n
ShadowMapSize=2048
SkylightShadowQuality=4
NumSamples=4
ShadowMapType=2
ShadowBitDepth=32
ShadowColor=0,0,0
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects]
CPHidePoints=n
CPHighlight=y
CPHidden=n
nCPWireThickness=1
LockedUsage=2
LockedTrans=50
GhostLockedObjects=n
ClipSectionUsage=0
CPColor=0,0,0
eCVStyle=102
nCVSize=3
LayersFollowLockUsage=n
LockedObjectsBehind=n
LockedColor=100,100,100
CPSolidLines=n
CPSingleColor=n
CPHideSurface=n
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Annotations]
DotBorderColor=-1
ShowText=y
ShowAnnotations=y
DotTextColor=-1
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Curves]
ShowCurves=y
CurveThicknessUsage=0
LineJoinStyle=0
LineEndCapStyle=0
CurvePattern=-1
CurveTrans=0
CurveThickness=1
CurveColor=0,0,0
SingleCurveColor=n
ShowCurvatureHair=n
CurveThicknessScale=1
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Meshes]
ShowMeshNakedEdges=n
MeshNonmanifoldEdgeColor=0,0,0
MeshNakedEdgeColor=0,0,0
MeshEdgeColor=0,0,0
MeshNonmanifoldEdgeColorReduction=0
MeshNakedEdgeColorReduction=0
MeshEdgeColorReduction=0
MeshNonmanifoldEdgeThickness=0
MeshNakedEdgeThickness=2
MeshEdgeThickness=2
ShowMeshNonmanifoldEdges=n
ShowMeshEdges=n
MeshVertexSize=0
ShowMeshVertices=n
ShowMeshWires=n
MeshWirePattern=-1
MeshWireThickness=1
MeshWireColor=0,0,0
SingleMeshWireColor=n
HighlightMeshes=n
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Points]
PCSize=2
ShowPoints=y
PointStyle=51
PointSize=3
PCGripStyle=102
PCGripSize=2
PCStyle=50
ShowPointClouds=y
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\SubD]
CreaseVisible=n
CreaseUsage=0
CreaseColor=255,255,255
CreaseColorReduction=0
CreaseThickness=2
CreaseThicknessScale=1
CreaseApplyPattern=y
NonmanifoldVisible=n
NonmanifoldUsage=0
NonmanifoldColor=255,255,255
NonmanifoldColorReduction=0
NonmanifoldThickness=1
NonmanifoldThicknessScale=1
NonmanifoldApplyPattern=y
BoundaryVisible=n
BoundaryUsage=0
BoundaryColor=255,255,255
SmoothColorReduction=0
SmoothColor=255,255,255
ShowSymmetryAxis=y
SmoothVisible=n
SubDThicknessUsage=0
BoundaryColorReduction=0
BoundaryThickness=2
SmoothThickness=1
BoundaryThicknessScale=2
BoundaryApplyPattern=y
ShowReflectedPlane=y
SmoothUsage=0
PlaneColorUsage=2
SymmetryAxisThickness=0.02500000037252903
ReflectedColor=64,64,64
ColorPercentage=30
AxisColor=255,0,0
SmoothThicknessScale=1
SmoothApplyPattern=y
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Surfaces]
EdgeColorReduction=0
SurfaceKappaHair=n
HighlightSurfaces=n
ShowIsocurves=n
IsoThicknessUsed=n
IsocurveThickness=1
IsoUThickness=1
IsoVThickness=1
IsoWThickness=1
SingleIsoColor=n
IsoColor=0,0,0
IsoColorsUsed=n
IsoUColor=0,0,0
IsoVColor=0,0,0
IsoWColor=0,0,0
IsoPatternUsed=n
IsocurvePattern=-1
IsoUPattern=-1
IsoVPattern=-1
IsoWPattern=-1
ShowEdges=y
ShowNakedEdges=n
ShowTangentEdges=n
ShowTangentSeams=n
ShowNonmanifoldEdges=n
ShowEdgeEndpoints=n
EdgeThickness=1
EdgeColorUsage=0
NakedEdgeThickness=2
NakedEdgeColorUsage=0
NakedEdgeColorReduction=0
EdgeColor=0,0,0
NakedEdgeColor=0,0,0
NonmanifoldEdgeColor=0,0,0
EdgePattern=-1
NakedEdgePattern=-1
NonmanifoldEdgePattern=-1
SurfaceThicknessUsage=0
SurfaceEdgeThicknessScale=2
SurfaceEdgeApplyPattern=y
SurfaceNakedEdgeUseNormalEdgeThickness=y
SurfaceNakedEdgeThicknessScale=2
SurfaceNakedEdgeApplyPattern=y
SurfaceIsoUThicknessScale=1
SurfaceIsoVThicknessScale=1
SurfaceIsoWThicknessScale=1
SurfaceIsoUApplyPattern=y
SurfaceIsoVApplyPattern=y
SurfaceIsoWApplyPattern=y
ShowFlatSurfaceIsos=n
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Technical]
TEThickness=1
TCThickness=1
TSThickness=1
TIThickness=1
THColor=0,0,0
TEColor=0,0,0
TSiColor=0,0,0
TCColor=0,0,0
TSColor=0,0,0
TIColor=0,0,0
TechnicalMask=14
TechnicalUsageMask=0
THThickness=1
TSiThickness=1
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading]
ParallelLineRotation=0
CullBackfaces=n
ShadeVertexColors=n
SingleWireColor=n
WireColor=0,0,0
ShadeSurface=y
UseObjectMaterial=n
UseObjectBFMaterial=n
BakeTextures=y
ShowDecals=y
SurfaceColorWriting=y
ShadingEffect=0
ParallelLineWidth=2
ParallelLineSeparation=3
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material]
BackIsCustom=y
FrontIsCustom=n
UseBackMaterial=y
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Back Material]
ShineIntensity=100
Luminosity=0
Reflectivity=0
Transparency=0
Specular=255,255,255
Shine=0
Diffuse=126,126,126
OverrideObjectReflectivity=y
OverrideObjectTransparency=y
OverrideObjectColor=n
FlatShaded=n
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Back Material\BitmapTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Back Material\EmapTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Back Material\TransparencyTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Front Material]
Diffuse=126,126,126
Specular=255,255,255
Shine=128
ShineIntensity=100
Reflectivity=0
Transparency=0
FlatShaded=y
OverrideObjectColor=n
OverrideObjectTransparency=y
OverrideObjectReflectivity=y
Luminosity=0
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Front Material\BitmapTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Front Material\EmapTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Front Material\TransparencyTexture]
=
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\View settings]
ShowClippingPlanes=n
FlipGlasses=n
AGViewingMode=0
AGColorMode=0
StereoParallax=1
StereoSeparation=1
StereoModeEnabled=0
BackgroundBitmap=
GradBotRight=140,140,140
WzColor=0,0,150
GridTrans=60
WyColor=75,150,75
WxColor=150,75,75
WorldAxesColor=0
GridPlaneColor=0,0,0
PlaneUsesGridColor=n
AxesPercentage=100
PlaneVisibility=0
GridPlaneTrans=90
DrawTransGridPlane=n
GradTopRight=200,200,200
GradBotLeft=140,140,140
GradTopLeft=200,200,200
SolidColor=230,230,230
FillMode=2
CustomLinearWorkflowPostProcessGamma=2.200000047683716
CustomLinearWorkflowPreProcessGamma=2.200000047683716
CustomLinearWorkflowPostProcessFrameBuffer=n
CustomLinearWorkflowPreProcessTextures=y
CustomLinearWorkflowPreProcessColors=y
LinearWorkflowUsage=0
CustomGroundPlaneShadowOnly=y
CustomGroundPlaneAutomaticAltitude=y
CustomGroundPlaneAltitude=0
CustomGroundPlaneShow=y
GroundPlaneUsage=1
UseDocumentGrid=n
DrawGrid=n
DrawAxes=n
DrawZAxis=n
DrawWorldAxes=n
ShowGridOnTop=n
ShowTransGrid=n
BlendGrid=n
VertScale=1
HorzScale=1
ClippingCPColor=255,255,255
ClippingEdgeColor=0,0,0
ClippingSurfaceColor=128,128,128
ClippingEdgeThickness=3
ClippingCPTrans=95
ClippingCPUsage=0
ClippingEdgesUsage=2
ClippingSurfaceUsage=0
ClippingShowCP=n
ClippingClipSelected=y
ClippingShowXEdges=y
ClippingShowXSurface=y
+2
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
"""
text_create.py
Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog.
+8 -6
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
"""
text_editor.py
React-WYSIWYG-Editor in Satellite-WebView (Topmost). User picked Frame
@@ -62,7 +64,7 @@ def _ensure_double_click_hook():
if not sc.sticky.get("dossier_text_idle_registered"):
Rhino.RhinoApp.Idle += _on_idle_check_pending_edit
sc.sticky["dossier_text_idle_registered"] = True
print("[TEXT-HOOK] Doppelklick-Hook installiert")
print("[TEXT-HOOK] Double-click hook installed")
except Exception as ex:
print("[TEXT-HOOK] install:", ex)
@@ -91,9 +93,9 @@ class TextEditorBridge(panel_base.BaseBridge):
self._initial_settings = settings
self._fonts = fonts
self._form_ref = None
self._edit_obj_id = edit_obj_id # bei Doppelklick-Edit gesetzt
self._edit_obj_id = edit_obj_id # bei Doppelklick-Edit set
self._initial_text = initial_text
self._initial_runs = initial_runs # rich-format-Runs falls vorhanden
self._initial_runs = initial_runs # rich-format-Runs falls present
self._initial_html = initial_html # 1:1 Editor-HTML beim Reopen
def set_form(self, form):
@@ -202,7 +204,7 @@ class TextEditorBridge(panel_base.BaseBridge):
st.get("underline"))
# 3. Text-Wrap im Frame — NACH dem Content damit es nicht
# durch RichText-Set zurueckgesetzt wird. Beide Setter
# durch RichText-Set zurueckset wird. Beide Setter
# versuchen (verschiedene Rhino-Versions-APIs).
applied_w = None
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
@@ -272,7 +274,7 @@ class TextEditorBridge(panel_base.BaseBridge):
break
except Exception: pass
if applied_scale is None:
print("[TEXT-EDITOR] AnnotationScaling-Property nicht gefunden")
print("[TEXT-EDITOR] AnnotationScaling-Property not found")
attrs = Rhino.DocObjects.ObjectAttributes()
col = st.get("color") # [r,g,b] oder None
@@ -571,7 +573,7 @@ def open_for_edit(obj):
if rj: initial_runs = json.loads(rj)
except Exception as ex:
print("[TEXT-EDITOR] read runs:", ex)
# Editor-innerHTML (Round-Trip-Konservierung): wenn vorhanden,
# Editor-innerHTML (Round-Trip-Konservierung): wenn present,
# wird der Editor exakt mit diesem HTML geoeffnet
initial_html = None
try:
File diff suppressed because it is too large Load Diff
+5 -3
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
"""
werkzeuge.py
WERKZEUGE-Panel: Architektur-orientierte Toolbar als React-WebView.
@@ -43,11 +45,11 @@ class WerkzeugeBridge(panel_base.BaseBridge):
if cmd.startswith("_") and "\n" not in cmd and ";" not in cmd:
try:
Rhino.RhinoApp.RunScript(cmd, False)
print("[WERKZEUGE] {}".format(cmd))
print("[TOOLS] {}".format(cmd))
except Exception as ex:
print("[WERKZEUGE] RunScript-Fehler:", ex)
print("[TOOLS] RunScript-Fehler:", ex)
else:
print("[WERKZEUGE] Befehl ignoriert (kein '_' Praefix oder unsicher):", cmd)
print("[TOOLS] Befehl ignoriert (kein '_' Praefix oder unsicher):", cmd)
def _bridge_factory():
+118
View File
@@ -0,0 +1,118 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
treppe_grips.py
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.
Marker-Logik pro Treppen-Art:
- gerade : PointAtStart, PointAtEnd der Linie
- 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
_MARKER_RADIUS_PX = 7
_MARKER_FILL = SD.Color.FromArgb(220, 95, 168, 150) # petrol-gruen, gleich wie wand_grips
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
def _treppe_endpoints(axis_obj):
"""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 []
geom = axis_obj.Geometry
if not isinstance(geom, rg.Curve): return []
art = a.GetUserString("dossier_treppe_art") or "gerade"
try:
if art == "wendel":
ok, poly = geom.TryGetPolyline()
if not ok or poly is None or poly.Count != 3: return []
return [poly[1], poly[2]]
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 Marker an allen selektierten Treppen-Achsen."""
def DrawForeground(self, e):
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
sel = list(doc.Objects.GetSelectedObjects(False, False))
seen = set()
for obj in sel:
a = obj.Attributes
eid = a.GetUserString("dossier_element_id") or ""
if not eid or eid in seen: continue
# 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.GetObjectList(_enumerator_all()):
if o is None or o.IsDeleted: continue
try:
a2 = o.Attributes
if a2.GetUserString("dossier_element_id") == eid and \
a2.GetUserString("dossier_element_type") == "treppe_axis":
axis = o; break
except Exception: continue
if axis is None: continue
seen.add(eid)
for pt in _treppe_endpoints(axis):
try:
e.Display.DrawPoint(pt,
rd.PointStyle.RoundControlPoint,
_MARKER_RADIUS_PX, _MARKER_FILL)
except Exception:
try: e.Display.DrawDot(pt, "", _MARKER_FILL, _MARKER_BORDER)
except Exception: pass
except Exception as ex:
print("[STAIR-GRIPS] DrawForeground:", ex)
_STICKY_CONDUIT = "_dossier_treppe_grips_conduit"
def install_handlers():
"""Idempotente Registrierung. Bei Modul-Reload alten Conduit zuerst
disablen, dann neuen anhaengen."""
try:
old = sc.sticky.get(_STICKY_CONDUIT)
if old is not None:
try: old.Enabled = False
except Exception: pass
conduit = _TreppeEndpointConduit()
conduit.Enabled = True
sc.sticky[_STICKY_CONDUIT] = conduit
print("[STAIR-GRIPS] Endpoint conduit active")
except Exception as ex:
print("[STAIR-GRIPS] install:", ex)
+94 -86
View File
@@ -1,5 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
wand_grips.py
Custom Endpoint-Grips fuer Waende Display-Conduit + MouseCallback Overlay.
@@ -94,53 +96,44 @@ def _find_axis_for_obj(doc, obj):
return None
def _curve_endpoints(curve):
"""Liefert (start_pt, end_pt) fuer eine wand_axis. Funktioniert fuer
LineCurve, PolylineCurve, NurbsCurve etc alle Curve-Typen haben
PointAtStart/PointAtEnd. Bei degenerierten Curves None."""
if curve is None: return None, None
def _axis_vertices(geom):
"""Liefert die Vertices der wand_axis-Curve als Liste.
- PolylineCurve: alle Vertices
- LineCurve / sonstige Curve: [Start, End] (zwei-Vertex-Faelle)
Returnt [] bei degeneriertem Input."""
if geom is None: return []
try:
return curve.PointAtStart, curve.PointAtEnd
if isinstance(geom, rg.PolylineCurve):
poly = geom.ToPolyline()
if poly is None or poly.Count < 2: return []
return list(poly)
p_start = geom.PointAtStart
p_end = geom.PointAtEnd
return [p_start, p_end]
except Exception:
return None, None
return []
def _replace_axis_endpoint(doc, axis_obj, kind, new_pt):
"""Tauscht den Start- (kind='start') oder Endpunkt (kind='end') der
wand_axis-Curve gegen new_pt. Geht intelligent um mit:
- LineCurve: erzeuge neue Line vom fixen Punkt zum neuen Punkt
- PolylineCurve: ersetze ersten/letzten Vertex, Rest bleibt
- andere Curve-Typen: aktuell nur Line-Fallback (Erst/Letzt-Vertex
rekonstruieren)
Setzt die neue Geometrie via Objects.Replace das feuert
def _replace_axis_vertex(doc, axis_obj, vertex_idx, new_pt):
"""Tauscht den Vertex an Index `vertex_idx` der wand_axis-Curve gegen
new_pt. Funktioniert fuer Linien (idx 0/1) und Polylinien (alle idx).
Setzt die neue Geometrie via Objects.Replace feuert
ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft."""
if axis_obj is None or axis_obj.IsDeleted: return False
geom = axis_obj.Geometry
if geom is None: return False
try:
# PolylineCurve mit > 2 Vertices: ersten/letzten Vertex ersetzen
if isinstance(geom, rg.PolylineCurve):
poly = geom.ToPolyline()
if poly is None or poly.Count < 2: return False
pts = list(poly)
if kind == "start":
pts[0] = new_pt
else:
pts[-1] = new_pt
new_poly = rg.Polyline(pts)
new_curve = rg.PolylineCurve(new_poly)
pts = _axis_vertices(geom)
if not pts: return False
if vertex_idx < 0 or vertex_idx >= len(pts): return False
pts[vertex_idx] = new_pt
if len(pts) == 2:
new_curve = rg.LineCurve(pts[0], pts[1])
else:
# LineCurve oder unbekannter Typ → reduziere auf Line zwischen
# neuem + altem fixen Punkt.
p_start, p_end = _curve_endpoints(geom)
if p_start is None or p_end is None: return False
if kind == "start":
new_curve = rg.LineCurve(new_pt, p_end)
else:
new_curve = rg.LineCurve(p_start, new_pt)
new_curve = rg.PolylineCurve(rg.Polyline(pts))
return doc.Objects.Replace(axis_obj.Id, new_curve)
except Exception as ex:
print("[WAND_GRIPS] replace endpoint:", ex)
print("[WALL-GRIPS] replace vertex:", ex)
return False
@@ -153,15 +146,17 @@ class _EndpointConduit(rd.DisplayConduit):
def __init__(self):
rd.DisplayConduit.__init__(self)
self.hot_key = None # (axis_id_str, kind) — fuer Hover
self.drag_key = None # (axis_id_str, kind) — waehrend aktivem Drag
self.drag_preview = None # rg.Line — Live-Vorschau waehrend GetPoint
self.hot_key = None # (axis_id_str, vidx) — fuer Hover
self.drag_key = None # (axis_id_str, vidx) — waehrend aktivem Drag
self.drag_preview = None # Liste von rg.Line — Live-Vorschau (Linien
# zu Nachbar-Vertices waehrend GetPoint)
def _collect_endpoints(self, doc):
"""Liefert Liste von (axis_obj, kind, world_pt) fuer alle selektier-
ten Waende. Iteriert die Selektion + dedupliziert Achsen (jede
Wand erscheint nur einmal, auch wenn mehrere Volumen mit-selek-
tiert sind)."""
def _collect_grip_points(self, doc):
"""Liefert Liste von (axis_obj, vertex_idx, world_pt) fuer ALLE
Vertices aller selektierten Waende fuer Polyline-Waende ist
jeder Knick ein eigener Grip. Iteriert die Selektion + dedupli-
ziert Achsen (jede Wand erscheint nur einmal, auch wenn mehrere
Volumen mit-selektiert sind)."""
out = []
seen_axis = set()
try:
@@ -173,24 +168,21 @@ class _EndpointConduit(rd.DisplayConduit):
aid = str(axis.Id)
if aid in seen_axis: continue
seen_axis.add(aid)
p_start, p_end = _curve_endpoints(axis.Geometry)
if p_start is not None:
out.append((axis, "start", p_start))
if p_end is not None:
out.append((axis, "end", p_end))
for i, pt in enumerate(_axis_vertices(axis.Geometry)):
out.append((axis, i, pt))
return out
def DrawForeground(self, e):
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
for axis, kind, pt in self._collect_endpoints(doc):
for axis, vidx, pt in self._collect_grip_points(doc):
aid = str(axis.Id)
# Skip den gerade gezogenen Marker — der wird via
# drag_preview separat dargestellt.
if self.drag_key and self.drag_key == (aid, kind):
if self.drag_key and self.drag_key == (aid, vidx):
continue
is_hot = self.hot_key and self.hot_key == (aid, kind)
is_hot = self.hot_key and self.hot_key == (aid, vidx)
r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX
fill = _MARKER_HOVER if is_hot else _MARKER_FILL
# DrawPoint mit RoundControlPoint = gefuellter Kreis +
@@ -202,13 +194,14 @@ class _EndpointConduit(rd.DisplayConduit):
# Fallback fuer aeltere Rhino-Versionen: einfacher
# DrawDot mit Label "●"
e.Display.DrawDot(pt, "", fill, _MARKER_BORDER)
# Drag-Preview-Linie waehrend GetPoint aktiv ist
if self.drag_preview is not None:
try:
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
except Exception: pass
# Drag-Preview-Linien waehrend GetPoint aktiv ist
if self.drag_preview:
for line in self.drag_preview:
try:
e.Display.DrawLine(line, _MARKER_HOVER, 2)
except Exception: pass
except Exception as ex:
print("[WAND_GRIPS] DrawForeground:", ex)
print("[WALL-GRIPS] DrawForeground:", ex)
# --- Mouse-Handler --------------------------------------------------------
@@ -224,21 +217,21 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point
def _hit_test(self, view, screen_pt):
"""Liefert (axis, kind, world_pt) wenn screen_pt nahe eines Endpoint-
Markers liegt, sonst None. Iteriert die aktuelle Conduit-Liste."""
"""Liefert (axis, vertex_idx, world_pt) wenn screen_pt nahe eines
Vertex-Markers liegt, sonst None."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return None
try:
vp = view.ActiveViewport
except Exception: return None
thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX
for axis, kind, world_pt in self.conduit._collect_endpoints(doc):
for axis, vidx, world_pt in self.conduit._collect_grip_points(doc):
try:
s = vp.WorldToClient(world_pt)
dx = s.X - screen_pt.X
dy = s.Y - screen_pt.Y
if (dx * dx + dy * dy) <= thresh2:
return axis, kind, world_pt
return axis, vidx, world_pt
except Exception: continue
return None
@@ -272,46 +265,61 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
# Default-Klick (Selection) abwuergen — wir uebernehmen
try: e.Cancel = True
except Exception: pass
axis, kind, world_pt = hit
self._start_drag(view.Document, axis, kind, world_pt)
axis, vidx, world_pt = hit
self._start_drag(view.Document, axis, vidx, world_pt)
except Exception as ex:
print("[WAND_GRIPS] OnMouseDown:", ex)
print("[WALL-GRIPS] OnMouseDown:", ex)
def _start_drag(self, doc, axis, kind, anchor_pt):
"""Startet eine Rhino-GetPoint-Interaktion um den neuen Endpunkt
zu picken. Der ANDERE Endpunkt (Fix-Punkt) wird als BasePoint
gesetzt damit kriegt der User Tracking-Linie, Ortho-Mode etc.
wie bei _Move."""
def _start_drag(self, doc, axis, vertex_idx, anchor_pt):
"""Startet eine Rhino-GetPoint-Interaktion um den Vertex zu
verschieben. BasePoint-Strategie:
- End-Vertex (idx 0 oder letzter): gegenueberliegender End-Vertex
User bekommt Tracking-Linie + Wand-Laenge wie bei _Move
- Mittel-Vertex (Polyline-Knick): Vertex selbst, plus Live-Preview
zu beiden Nachbar-Vertices damit beide Segmente sichtbar mit-
schwingen."""
if doc is None: return
geom = axis.Geometry
if geom is None: return
p_start, p_end = _curve_endpoints(geom)
if p_start is None or p_end is None: return
fixed_pt = p_end if kind == "start" else p_start
# Conduit-State: drag-Marker hervorheben + Preview-Linie
self.conduit.drag_key = (str(axis.Id), kind)
self.conduit.drag_preview = rg.Line(fixed_pt, anchor_pt)
pts = _axis_vertices(geom)
if not pts or vertex_idx < 0 or vertex_idx >= len(pts): return
is_first = vertex_idx == 0
is_last = vertex_idx == len(pts) - 1
prev_pt = pts[vertex_idx - 1] if not is_first else None
next_pt = pts[vertex_idx + 1] if not is_last else None
if is_first: base_pt = next_pt
elif is_last: base_pt = prev_pt
else: base_pt = anchor_pt
# Conduit-State: drag-Marker hervorheben + Preview-Linien
self.conduit.drag_key = (str(axis.Id), vertex_idx)
self.conduit.drag_preview = []
if prev_pt is not None:
self.conduit.drag_preview.append(rg.Line(prev_pt, anchor_pt))
if next_pt is not None:
self.conduit.drag_preview.append(rg.Line(next_pt, anchor_pt))
self._busy = True
try:
gp = Rhino.Input.Custom.GetPoint()
gp.SetCommandPrompt("Wand-Endpunkt: neuer Punkt (Esc=Abbruch)")
gp.SetBasePoint(fixed_pt, True)
gp.DrawLineFromPoint(fixed_pt, True)
# Live-Preview ueber Conduit (zusaetzlich zu Rhinos eigener
# Tracking-Linie) — sieht ueblich, hilft beim Verstehen welcher
# Endpunkt sich bewegt.
gp.SetCommandPrompt("Wand-Vertex: neuer Punkt (Esc=Abbruch)")
gp.SetBasePoint(base_pt, True)
gp.DrawLineFromPoint(base_pt, True)
def _on_mouse_move(sender, args):
try:
self.conduit.drag_preview = rg.Line(fixed_pt, args.Point)
preview = []
if prev_pt is not None:
preview.append(rg.Line(prev_pt, args.Point))
if next_pt is not None:
preview.append(rg.Line(next_pt, args.Point))
self.conduit.drag_preview = preview
except Exception: pass
try: gp.MouseMove += _on_mouse_move
except Exception: pass
res = gp.Get()
if res == Rhino.Input.GetResult.Point:
new_pt = gp.Point()
_replace_axis_endpoint(doc, axis, kind, new_pt)
_replace_axis_vertex(doc, axis, vertex_idx, new_pt)
except Exception as ex:
print("[WAND_GRIPS] _start_drag:", ex)
print("[WALL-GRIPS] _start_drag:", ex)
finally:
self.conduit.drag_key = None
self.conduit.drag_preview = None
@@ -346,6 +354,6 @@ def install_handlers():
handler.Enabled = True
sc.sticky[_STICKY_CONDUIT] = conduit
sc.sticky[_STICKY_HANDLER] = handler
print("[WAND_GRIPS] Endpoint-Conduit + Mouse-Handler aktiv")
print("[WALL-GRIPS] Endpoint conduit + mouse handler active")
except Exception as ex:
print("[WAND_GRIPS] install:", ex)
print("[WALL-GRIPS] install:", ex)
+562
View File
@@ -0,0 +1,562 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
welcome.py
Welcome-Screen + Shortcuts-Cheatsheet als WebView-Dialog im DOSSIER-Style
(passend zum Splashscreen Petrol-Gradient, Mono-Font).
Funktionen:
- show_welcome() erscheint NACH dem Splash (eigener Idle-Timer), einmal
pro Version. User kann "nicht mehr anzeigen" rechts unten anklicken.
- show_cheatsheet() DOSSIER-Shortcut-Liste, aufrufbar via dkeys-Alias.
Marker-Datei fuer "schon gesehen" wird in
~/Library/Application Support/ch.gabrielevarano.Dossier/welcome_shown abgelegt.
"""
import os
import json
import Rhino
DOSSIER_VERSION = "0.6.3"
DOSSIER_GITHUB = "https://github.com/karimgvarano/DOSSIER"
DOSSIER_SUPPORT_EMAIL = "karim@gabrielevarano.ch"
_WELCOME_DIR = os.path.expanduser(
"~/Library/Application Support/ch.gabrielevarano.Dossier")
_WELCOME_FLAG = os.path.join(_WELCOME_DIR, "welcome_shown.txt")
_WELCOME_OPTOUT = os.path.join(_WELCOME_DIR, "welcome_dontshow.txt")
_SPLASH_MIN_DELAY_SEC = 3.5
_HERE = os.path.dirname(os.path.abspath(__file__))
_SHORTCUTS_JSON = os.path.join(_HERE, "aliases", "shortcuts_default.json")
def _has_optout():
return os.path.exists(_WELCOME_OPTOUT)
def _has_seen_version(version):
try:
if not os.path.exists(_WELCOME_FLAG): return False
with open(_WELCOME_FLAG, "r") as f:
return f.read().strip() == version
except Exception:
return False
def _mark_seen(version):
try:
os.makedirs(_WELCOME_DIR, exist_ok=True)
with open(_WELCOME_FLAG, "w") as f:
f.write(version)
except Exception as ex:
print("[WELCOME] mark-seen err:", ex)
def _write_optout():
try:
os.makedirs(_WELCOME_DIR, exist_ok=True)
with open(_WELCOME_OPTOUT, "w") as f:
f.write("1")
except Exception as ex:
print("[WELCOME] optout-write err:", ex)
def _load_shortcuts():
try:
with open(_SHORTCUTS_JSON, "r", encoding="utf-8") as f:
data = json.load(f)
items = []
for k, v in data.items():
if k.startswith("_") or not isinstance(v, dict): continue
items.append({
"id": k,
"trigger": v.get("trigger", ""),
"label": v.get("label", k),
"type": v.get("type", ""),
})
return items
except Exception as ex:
print("[WELCOME] shortcuts-load err:", ex)
return []
# ---- HTML — DOSSIER-Style passend zum Splash ----------------------------
_WELCOME_HTML = """<!DOCTYPE html>
<html lang="de"><head><meta charset="utf-8"/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&family=Playfair+Display:wght@400;500&display=swap" rel="stylesheet"/>
<style>
:root {{
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
--paper: #fff; --paper-mute: rgba(255,255,255,0.78); --paper-faint: rgba(255,255,255,0.5);
--font-display: Krungthep, 'Archivo Black', sans-serif;
--font-serif: 'Playfair Display', serif;
--font-mono: 'DM Mono', 'Menlo', monospace;
}}
* {{ box-sizing:border-box; }}
html, body {{
margin:0; padding:0; width:100%; height:100%; background:transparent !important;
color:var(--paper); overflow:hidden; font-family:var(--font-mono); user-select:none;
-webkit-user-select:none;
}}
.frame {{
box-sizing:border-box; width:100%; height:100%; padding:28px 32px 24px;
display:flex; flex-direction:column;
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
border-radius:16px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
}}
.brand-row {{ display:flex; align-items:baseline; justify-content:space-between; gap:12px; }}
.brand {{
font-family:var(--font-display); font-size:32px; letter-spacing:-0.01em;
line-height:1; color:var(--paper);
}}
.brand-dot {{ color:var(--accent-deep); }}
.version {{
font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em;
color:var(--paper-mute); text-transform:uppercase;
}}
.title {{
font-family:var(--font-serif); font-size:22px; line-height:1.3;
color:var(--paper); margin-top:20px; font-weight:500;
}}
.intro {{
font-size:11px; line-height:1.65; color:var(--paper-mute); margin-top:10px;
letter-spacing:0.02em;
}}
.section-title {{
font-size:9px; letter-spacing:0.18em; text-transform:uppercase;
color:var(--paper-faint); margin:22px 0 10px;
}}
.links {{ display:flex; flex-direction:column; gap:8px; }}
a {{ color:inherit; text-decoration:none; }}
.link {{
display:flex; align-items:flex-start; gap:12px;
padding:10px 14px; border-radius:6px; cursor:pointer;
background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.12);
transition:background 0.15s;
color:var(--paper); text-decoration:none;
}}
.link:hover {{ background:rgba(255,255,255,0.16); }}
.link-icon {{
font-family:var(--font-display); font-size:14px; color:var(--accent-deep);
background:var(--paper); width:24px; height:24px; border-radius:50%;
display:flex; align-items:center; justify-content:center; flex-shrink:0;
margin-top:1px;
}}
.link-content {{ flex:1; min-width:0; }}
.link-title {{ font-size:12px; color:var(--paper); font-weight:500; }}
.link-desc {{ font-size:10px; color:var(--paper-mute); margin-top:2px; }}
kbd {{
background:rgba(0,0,0,0.18); padding:1px 6px; border-radius:3px;
font-family:var(--font-mono); font-size:10px; color:var(--paper);
border:1px solid rgba(255,255,255,0.15);
}}
.footer {{
margin-top:auto; display:flex; align-items:center; justify-content:space-between;
padding-top:18px; gap:12px;
}}
.footer-meta {{
font-size:9px; letter-spacing:0.14em; color:var(--paper-faint);
text-transform:uppercase;
}}
.optout {{
display:flex; align-items:center; gap:6px; cursor:pointer;
font-size:10px; color:var(--paper-mute); user-select:none;
}}
.optout:hover {{ color:var(--paper); }}
.optout input {{ accent-color:var(--paper); margin:0; }}
.win-ctrl {{
position:absolute; top:14px; right:16px; display:flex; gap:6px; z-index:20;
}}
.win-btn {{
width:22px; height:22px; border-radius:50%; cursor:pointer;
display:flex; align-items:center; justify-content:center;
background:rgba(0,0,0,0.18); border:1px solid rgba(255,255,255,0.18);
color:var(--paper); font-family:var(--font-mono); font-size:13px;
text-decoration:none; transition:background 0.12s;
line-height:1; user-select:none;
}}
.win-btn:hover {{ background:rgba(0,0,0,0.32); }}
</style></head><body>
<div class="frame">
<div class="win-ctrl">
<a class="win-btn" href="dossier:close" title="Schliessen">&times;</a>
</div>
<div class="brand-row">
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
<div class="version">Version {ver}</div>
</div>
<div class="title">Willkommen im Studio</div>
<div class="intro">
DOSSIER ist dein Architektur-Studio-Plugin fuer Rhino 8
Waende, Decken, Treppen, Fenster, Tueren, Raumstempel,
Layouts. Alles aus einer Hand, im selben Stil.
</div>
<div class="section-title">Einstieg</div>
<div class="links">
<a class="link" href="dossier:cheatsheet">
<div class="link-icon"></div>
<div class="link-content">
<div class="link-title">Shortcuts &amp; Cheatsheet</div>
<div class="link-desc">Tippe <kbd>dkeys</kbd> im Command-Prompt fuer die volle Liste</div>
</div>
</a>
<a class="link" href="{github}" target="_blank">
<div class="link-icon">i</div>
<div class="link-content">
<div class="link-title">Einfuehrung &amp; Doku</div>
<div class="link-desc">{github}</div>
</div>
</a>
<a class="link" href="{github}/releases" target="_blank">
<div class="link-icon">v</div>
<div class="link-content">
<div class="link-title">Changelog</div>
<div class="link-desc">Was ist neu in dieser Version</div>
</div>
</a>
<a class="link" href="mailto:{email}" target="_blank">
<div class="link-icon">?</div>
<div class="link-content">
<div class="link-title">Support &amp; Problem melden</div>
<div class="link-desc">{email} oder GitHub-Issues</div>
</div>
</a>
</div>
<div class="footer">
<div class="footer-meta">AGPL-3.0 &middot; Karim Gabriele Varano</div>
<label class="optout">
<input type="checkbox" id="optout" onchange="window.location='dossier:optout?'+this.checked"/>
Nicht mehr anzeigen
</label>
</div>
</div>
</body></html>"""
_CHEATSHEET_HTML = """<!DOCTYPE html>
<html><head><meta charset="utf-8"/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet"/>
<style>
:root {{
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
--paper: #fff; --paper-mute: rgba(255,255,255,0.78); --paper-faint: rgba(255,255,255,0.5);
--font-display: Krungthep, 'Archivo Black', sans-serif;
--font-mono: 'DM Mono', 'Menlo', monospace;
}}
* {{ box-sizing:border-box; }}
html, body {{
margin:0; padding:0; width:100%; height:100%; background:transparent !important;
color:var(--paper); overflow:auto; font-family:var(--font-mono);
}}
.frame {{
box-sizing:border-box; min-height:100%; padding:24px 28px;
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
border-radius:16px;
}}
.brand-row {{ display:flex; align-items:baseline; justify-content:space-between; gap:12px; }}
.brand {{ font-family:var(--font-display); font-size:24px; line-height:1; color:var(--paper); }}
.brand-dot {{ color:var(--accent-deep); }}
.version {{ font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em; color:var(--paper-mute); text-transform:uppercase; }}
h2 {{
font-size:10px; letter-spacing:0.18em; color:var(--paper); margin:18px 0 8px;
text-transform:uppercase; font-weight:500;
}}
table {{ width:100%; border-collapse:collapse; }}
td {{ padding:5px 8px; border-bottom:1px solid rgba(255,255,255,0.12); vertical-align:middle; }}
td:first-child {{ width:170px; }}
kbd {{
background:rgba(0,0,0,0.18); padding:2px 8px; border-radius:3px;
font-family:var(--font-mono); font-size:11px; color:var(--paper);
border:1px solid rgba(255,255,255,0.18);
}}
.lab {{ color:var(--paper); font-size:11px; }}
.badge {{
font-size:9px; padding:1px 5px; border-radius:2px; margin-left:6px;
background:rgba(255,255,255,0.12); color:var(--paper-mute);
font-family:var(--font-mono);
}}
.win-ctrl {{
position:fixed; top:14px; right:18px; display:flex; gap:6px; z-index:20;
}}
.win-btn {{
width:22px; height:22px; border-radius:50%; cursor:pointer;
display:flex; align-items:center; justify-content:center;
background:rgba(0,0,0,0.22); border:1px solid rgba(255,255,255,0.18);
color:var(--paper); font-family:var(--font-mono); font-size:13px;
text-decoration:none; transition:background 0.12s;
line-height:1; user-select:none;
}}
.win-btn:hover {{ background:rgba(0,0,0,0.38); }}
</style></head><body>
<div class="frame">
<div class="win-ctrl">
<a class="win-btn" href="dossier:back" title="Zurueck">&lsaquo;</a>
<a class="win-btn" href="dossier:close" title="Schliessen">&times;</a>
</div>
<div class="brand-row">
<div class="brand">DOSSIER<span class="brand-dot">.</span> Shortcuts</div>
<div class="version">v {ver}</div>
</div>
{sections}
</div></body></html>"""
def _build_cheatsheet_html():
items = _load_shortcuts()
groups = {
"DOSSIER BIM": [],
"2D-Werkzeuge": [],
"Views & Navigation": [],
"Modify-Tools": [],
"Sonstige Aliases": [],
}
bim_ids = {"wand", "tuer", "fenster", "decke", "treppe", "stuetze",
"traeger", "raum", "symbol", "stempel", "dach", "aussparung"}
view_ids = {"view_plan", "view_3d", "view_material", "zoom_ext",
"zoom_sel", "geschoss_up", "geschoss_down",
"panel_layer", "panel_elemente"}
twod_ids = {"text", "line", "arc", "rectangle", "polyline", "curve",
"hatch", "polygon", "ellipse", "circle"}
for it in items:
i = it["id"]
if i in bim_ids: groups["DOSSIER BIM"].append(it)
elif i in view_ids: groups["Views & Navigation"].append(it)
elif i.startswith("mod_"): groups["Modify-Tools"].append(it)
elif i in twod_ids or i.endswith("_alias"): groups["2D-Werkzeuge"].append(it)
else: groups["Sonstige Aliases"].append(it)
def _row(it):
trig = it["trigger"]
trig = trig.replace("Cmd+", "⌘+").replace("Shift+", "⇧+").replace("Alt+", "⌥+")
return ('<tr><td><kbd>{}</kbd></td>'
'<td class="lab">{}</td></tr>'
.format(trig, it["label"]))
sections = []
for gname, gitems in groups.items():
if not gitems: continue
rows = "".join(_row(it) for it in gitems)
sections.append('<h2>{}</h2><table>{}</table>'.format(gname, rows))
return _CHEATSHEET_HTML.format(ver=DOSSIER_VERSION, sections="".join(sections))
# ---- Dialog-Anzeige ------------------------------------------------------
def _try_borderless_mac(form):
"""Borderless NSWindow + transparenten Hintergrund (analog _startup_splash)."""
try:
import System
nsw = getattr(form, "ControlObject", None)
if nsw is None: return False
# StyleMask = 0 (Borderless)
try:
cur = nsw.StyleMask
nsw.StyleMask = System.Enum.ToObject(type(cur), 0)
except Exception as ex:
print("[WELCOME] StyleMask:", ex)
# Transparent background damit border-radius vom HTML sichtbar
for prop, val in [("TitlebarAppearsTransparent", True),
("IsOpaque", False), ("HasShadow", True),
("MovableByWindowBackground", True)]:
try: setattr(nsw, prop, val)
except Exception: pass
try:
tv_type = type(nsw.TitleVisibility)
nsw.TitleVisibility = System.Enum.ToObject(tv_type, 1)
except Exception: pass
try:
from AppKit import NSColor as _NSC
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
if clear is not None: nsw.BackgroundColor = clear
except Exception: pass
return True
except Exception as ex:
print("[WELCOME] borderless:", ex)
return False
def _webview_transparent(web):
"""WKWebView vollstaendig transparent — KVC drawsBackground=NO,
UnderPageBackgroundColor=Clear, Layer.BackgroundColor=CGColor.Clear."""
wk = getattr(web, "ControlObject", None)
if wk is None: return
try:
from Foundation import NSNumber, NSString
try: wk.SetValueForKey(NSNumber.FromBoolean(False), NSString("drawsBackground"))
except Exception as ex: print("[WELCOME] KVC drawsBackground:", ex)
except Exception as ex: print("[WELCOME] Foundation:", ex)
try:
from AppKit import NSColor as _NSC
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
if clear is not None:
try: wk.UnderPageBackgroundColor = clear
except Exception: pass
try:
layer = getattr(wk, "Layer", None)
if layer is not None:
layer.BackgroundColor = clear.CGColor
layer.Opaque = False
except Exception as ex: print("[WELCOME] Layer:", ex)
except Exception as ex: print("[WELCOME] NSColor:", ex)
def _show_html_form(title, html, width=620, height=720, on_navigating=None,
borderless=True):
"""Eto.Forms.Form mit WebView + Inline-HTML. Optional borderless +
Navigation-Hook fuer custom URL-Schemes."""
try:
import Eto.Forms as ef
import Eto.Drawing as ed
except Exception as ex:
print("[WELCOME] Eto.Forms not available:", 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 active ({}) — 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()

Some files were not shown because too many files have changed in this diff Show More