Compare commits

...

47 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
109 changed files with 12839 additions and 1548 deletions
+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."
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# clean-rhino.sh — setzt DOSSIER in Rhino zurueck auf "frisch installiert" Zustand.
# Damit kann das Setup im Launcher (Settings → Setup tab) jederzeit von Null
# durchgespielt werden.
#
# Aufgaben:
# 1. yak uninstall dossier (Plugin raus)
# 2. Window-Layout-Datei loeschen (workspaces/<guid>.xml)
# 3. StartupCommands-XML-Eintrag entfernen (Python-Bootstrap-Trigger)
#
# Bleibt unangetastet:
# - dossier_settings.json (User-Praeferenzen, Tags, etc.)
# - launcher recent.json
# - alles ausserhalb DOSSIER
set -e
RHINO_APP="/Applications/Rhino 8.app"
YAK="$RHINO_APP/Contents/Resources/bin/yak"
SETTINGS_XML="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"
WORKSPACES_DIR="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces"
LAYOUT_GUID="b6b68c03-3031-4899-bca2-fe6e425146fc"
# --- Safety: Rhino muss zu sein ---
if pgrep -f "Rhino 8.app/Contents/MacOS/Rhinoceros$" >/dev/null; then
echo "FEHLER: Rhino laeuft. Bitte beenden und nochmal."
exit 1
fi
# --- 1. Yak uninstall (idempotent — meldet 'package not installed' wenn schon weg) ---
echo "==> 1. Yak uninstall dossier"
if [ -x "$YAK" ]; then
"$YAK" uninstall dossier 2>&1 | sed 's/^/ /' || true
else
echo " WARN: yak nicht gefunden — skip"
fi
# --- 2. Window-Layout-Datei loeschen ---
echo "==> 2. Window-Layout-Datei loeschen"
LAYOUT_FILE="$WORKSPACES_DIR/$LAYOUT_GUID.xml"
if [ -f "$LAYOUT_FILE" ]; then
rm -v "$LAYOUT_FILE" | sed 's/^/ /'
else
echo " schon weg"
fi
# --- 3. StartupCommands-Eintrag aus XML entfernen ---
echo "==> 3. StartupCommands-Eintrag entfernen"
if [ -f "$SETTINGS_XML" ]; then
# sed: matche genau unsere DOSSIER-Zeile und loesche
# (egal welcher Pfad — solange startup.py drin steht)
if grep -q 'StartupCommands.*startup.py' "$SETTINGS_XML"; then
# macOS sed braucht leeres Backup-Suffix
sed -i '' '/<entry key="StartupCommands">.*startup\.py.*<\/entry>/d' "$SETTINGS_XML"
echo " entfernt"
else
echo " schon weg"
fi
else
echo " WARN: Rhino-settings-XML nicht gefunden"
fi
echo
echo "Clean fertig. Naechster Schritt:"
echo " → Launcher → Settings → Setup → 'Setup starten'"
+266 -2
View File
@@ -303,9 +303,17 @@ fn splash_owner_marker_path() -> PathBuf {
fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> {
let settings = load_settings();
// XML-Edit nur sinnvoll wenn Rhino nicht laeuft (sonst ueberschreibt's
// beim Beenden) UND der Eintrag fuer den naechsten Start eh schon greift.
// Setup-Schritte nur wenn Rhino NICHT laeuft sonst ueberschreibt Rhino
// unsere XML-Edits beim Beenden, und yak install kann die in-use .rhp
// nicht ersetzen.
if settings.auto_load_plugin && !is_rhino_running() {
// Schritt 1: .rhp via yak installieren/aktualisieren. Soft-Fail wenn
// .yak nicht gebundelt ist (Dev-Setup ohne build.sh ausgefuehrt) —
// Bootstrap via XML reicht alleine, Commands fehlen nur.
if let Err(e) = ensure_rhino_plugin_installed() {
eprintln!("[DOSSIER] Plugin-Install skip: {e}");
}
// Schritt 2: StartupCommands-XML fuer Python-Bootstrap setzen.
let startup_path = settings.plugin_startup_path
.clone()
.unwrap_or_else(default_plugin_startup_path);
@@ -466,11 +474,264 @@ fn ensure_rhino_startup_command(startup_path: &str) -> Result<(), String> {
Ok(())
}
// ===== Rhino-Plugin Install via Yak =====
// Mac Rhino lehnt Auto-Load fuer dritte Plugins ab (egal welche Methode).
// Wir installieren `.yak` aber trotzdem in den User-Plugin-Pfad — beim ersten
// dWall/dDoor/... laedt Rhino das Plugin on-demand und cached es danach.
// Der Python-Bootstrap (Panels/Aliases) laeuft parallel via StartupCommands-
// XML (ensure_rhino_startup_command). Beide Pfade zusammen = full setup.
fn yak_binary_path() -> PathBuf {
PathBuf::from("/Applications/Rhino 8.app/Contents/Resources/bin/yak")
}
fn rhino_packages_dir() -> PathBuf {
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
home.join("Library/Application Support/McNeel/Rhinoceros/packages/8.0")
}
fn installed_dossier_version() -> Option<String> {
let manifest = rhino_packages_dir().join("DOSSIER/manifest.txt");
fs::read_to_string(&manifest).ok().map(|s| s.trim().to_string())
}
// .yak + version.txt im App-Bundle (Production) oder Repo-dist/ (Dev).
fn bundled_plugin_paths() -> (PathBuf, PathBuf) {
if let Ok(exe) = std::env::current_exe() {
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
let yak = contents_dir.join("Resources/plugin/dossier.yak");
let ver = contents_dir.join("Resources/plugin/dossier-version.txt");
if yak.is_file() && ver.is_file() {
return (yak, ver);
}
}
}
// Dev-Fallback: Repo-Pfad
let repo = PathBuf::from("/Users/karim/STUDIO/DOSSIER/csharp/DOSSIER/dist");
(repo.join("dossier.yak"), repo.join("dossier-version.txt"))
}
fn bundled_plugin_version() -> Option<String> {
let (_, ver_path) = bundled_plugin_paths();
fs::read_to_string(&ver_path).ok().map(|s| s.trim().to_string())
}
// Installiert/aktualisiert das DOSSIER-Plugin via yak. Idempotent — skip wenn
// installierte Version == gebundelte Version. Soft-Fail wenn .yak fehlt oder
// yak-CLI nicht vorhanden (Logging, kein Error — Bootstrap via XML laeuft eh).
fn ensure_rhino_plugin_installed() -> Result<(), String> {
let yak = yak_binary_path();
if !yak.is_file() {
return Err(format!("yak CLI nicht gefunden: {}", yak.display()));
}
let (yak_pkg, _) = bundled_plugin_paths();
if !yak_pkg.is_file() {
return Err(format!(
"DOSSIER .yak-Paket nicht gefunden: {} (build.sh in csharp/DOSSIER ausfuehren)",
yak_pkg.display()
));
}
let bundled_ver = bundled_plugin_version();
let installed_ver = installed_dossier_version();
if let (Some(b), Some(i)) = (&bundled_ver, &installed_ver) {
if b == i {
return Ok(()); // schon aktuell — kein Re-Install
}
}
if is_rhino_running() {
return Err(
"Rhino laeuft — Plugin-Update kann erst nach Rhino-Quit installiert werden."
.into(),
);
}
let pkg_dir = yak_pkg.parent().ok_or_else(|| "Plugin-Pfad ohne Parent".to_string())?;
let output = Command::new(&yak)
.arg("install")
.arg("dossier")
.arg("--source")
.arg(pkg_dir)
.output()
.map_err(|e| format!("yak install: {e}"))?;
if !output.status.success() {
return Err(format!(
"yak install fehlgeschlagen: {}",
String::from_utf8_lossy(&output.stderr)
));
}
eprintln!(
"[DOSSIER] Plugin via yak installiert: {}{}",
installed_ver.unwrap_or_else(|| "(nicht installiert)".into()),
bundled_ver.unwrap_or_else(|| "(unbekannt)".into())
);
Ok(())
}
#[tauri::command]
fn open_rhino(app: tauri::AppHandle, path3dm: String) -> Result<(), String> {
open_rhino_internal(&app, &path3dm)
}
#[tauri::command]
fn install_rhino_plugin() -> Result<String, String> {
ensure_rhino_plugin_installed()?;
Ok(installed_dossier_version().unwrap_or_else(|| "(version unbekannt)".into()))
}
// ===== Window-Layout Installation =====
// Rhino-Workspaces sind XML-Dateien im User-Pfad benannt nach Layout-GUID.
// Wir bundlen DOSSIERs Master-Layout(s) im Repo unter rhino/workspaces/ und
// kopieren sie beim Init in Rhinos Workspaces-Folder, damit startup.py das
// Layout per `_-WindowLayout "<name>" _Enter` direkt anwenden kann.
fn rhino_workspaces_dir() -> PathBuf {
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
home.join("Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces")
}
fn bundled_workspaces_dir() -> PathBuf {
if let Ok(exe) = std::env::current_exe() {
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
let bundled = contents_dir.join("Resources/rhino/workspaces");
if bundled.is_dir() {
return bundled;
}
}
}
PathBuf::from("/Users/karim/STUDIO/DOSSIER/rhino/workspaces")
}
// Kopiert alle Workspace-XMLs aus dem bundle in Rhinos Workspaces-Folder.
// Vorhandene Files werden ueberschrieben (User-Customizations gehen verloren —
// dafuer ist's reproduzierbar). Returns Anzahl kopierter Files.
fn ensure_window_layout_installed() -> Result<usize, String> {
let src = bundled_workspaces_dir();
if !src.is_dir() {
return Err(format!("Workspace-Quelle fehlt: {}", src.display()));
}
let dst = rhino_workspaces_dir();
fs::create_dir_all(&dst)
.map_err(|e| format!("Workspace-Zielordner erstellen: {e}"))?;
if is_rhino_running() {
return Err("Rhino laeuft — bitte erst beenden (Layout-Datei wird sonst ueberschrieben).".into());
}
let mut count = 0;
for entry in fs::read_dir(&src).map_err(|e| format!("Workspace-Quelle lesen: {e}"))? {
let entry = entry.map_err(|e| format!("DirEntry: {e}"))?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("xml")).unwrap_or(false) {
let file_name = path.file_name().ok_or("FileName")?;
let dst_path = dst.join(file_name);
fs::copy(&path, &dst_path)
.map_err(|e| format!("Workspace kopieren ({}): {e}", file_name.to_string_lossy()))?;
count += 1;
}
}
Ok(count)
}
// ===== DOSSIER INIT =====
// Komplettes Setup auf einem neuen PC: Plugin via Yak + StartupCommands-XML +
// Window-Layout-Files. Returns Status pro Schritt fuer das Frontend-Dialog.
#[derive(Serialize, Clone, Debug)]
struct InitStep {
id: String,
label: String,
status: String, // "ok" | "error" | "skipped"
detail: String,
}
#[derive(Serialize, Clone, Debug)]
struct InitResult {
steps: Vec<InitStep>,
overall_ok: bool,
}
#[derive(Serialize, Clone, Debug)]
struct InitStatus {
plugin_installed: bool,
startup_cmd_set: bool,
layout_installed: bool,
initialized: bool,
}
#[tauri::command]
fn check_dossier_initialized() -> InitStatus {
let plugin_installed = installed_dossier_version().is_some();
let startup_cmd_set = fs::read_to_string(rhino_settings_xml_path())
.map(|s| s.contains("StartupCommands") && s.contains("startup.py"))
.unwrap_or(false);
let layout_installed = rhino_workspaces_dir()
.join("b6b68c03-3031-4899-bca2-fe6e425146fc.xml")
.is_file();
InitStatus {
plugin_installed,
startup_cmd_set,
layout_installed,
// initialized = ALLE drei vorhanden. So zeigt der Dialog auch nach
// teilweisem Clean (z.B. nur layout geloescht) noch an.
initialized: plugin_installed && startup_cmd_set && layout_installed,
}
}
#[tauri::command]
fn dossier_init() -> Result<InitResult, String> {
if is_rhino_running() {
return Err("Rhino laeuft — bitte erst beenden, dann Init nochmal starten.".into());
}
let mut steps = Vec::new();
let mut overall_ok = true;
// Schritt 1: Plugin via Yak installieren/aktualisieren
let (status, detail) = match ensure_rhino_plugin_installed() {
Ok(()) => {
let v = installed_dossier_version().unwrap_or_else(|| "(?)".into());
("ok".into(), format!("Version {v}"))
}
Err(e) => { overall_ok = false; ("error".into(), e) }
};
steps.push(InitStep {
id: "plugin".into(),
label: "DOSSIER-Plugin via Yak installieren".into(),
status, detail,
});
// Schritt 2: StartupCommands-XML eintragen (Python-Bootstrap)
let startup_path = default_plugin_startup_path();
let (status, detail) = if !Path::new(&startup_path).is_file() {
overall_ok = false;
("error".into(), format!("startup.py nicht gefunden: {startup_path}"))
} else {
match ensure_rhino_startup_command(&startup_path) {
Ok(()) => ("ok".into(), startup_path.clone()),
Err(e) => { overall_ok = false; ("error".into(), e) }
}
};
steps.push(InitStep {
id: "startup".into(),
label: "Python-Bootstrap (StartupCommands-XML)".into(),
status, detail,
});
// Schritt 3: Window-Layout-Files in Rhinos Workspaces-Folder kopieren
let (status, detail) = match ensure_window_layout_installed() {
Ok(n) => ("ok".into(), format!("{n} Layout-Datei(en) installiert")),
Err(e) => { overall_ok = false; ("error".into(), e) }
};
steps.push(InitStep {
id: "layout".into(),
label: "Window-Layout in Rhino kopieren".into(),
status, detail,
});
Ok(InitResult { steps, overall_ok })
}
#[tauri::command]
fn trigger_plugin_load_now() -> Result<(), String> {
// Schreibt den `_RunPythonScript <pfad>` Eintrag in Rhinos Startup-Command-
@@ -924,6 +1185,9 @@ pub fn run() {
read_project_config,
open_rhino,
trigger_plugin_load_now,
install_rhino_plugin,
dossier_init,
check_dossier_initialized,
get_default_plugin_startup_path,
show_in_finder,
is_rhino_running,
+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": {
+151
View File
@@ -126,6 +126,13 @@ export default function App() {
invoke('list_window_layouts').then(setLayouts).catch(() => {})
invoke('read_dossier_settings').then(ds => setActiveLayout(ds?.windowLayout || '')).catch(() => {})
invoke('read_settings').then(s => setTags(s?.tags || [])).catch(() => {})
// Auto-Open Setup-Dialog wenn DOSSIER nicht initialisiert ist (z.B. nach
// clean-rhino.sh oder auf einem neuen Mac).
invoke('check_dossier_initialized')
.then(st => {
if (!st?.initialized) { setSettingsTab('setup'); setSettingsOpen(true) }
})
.catch(() => {})
}, [])
// File-Meta laden sobald recent sich aendert
@@ -750,6 +757,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
<header style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span>Einstellungen</span>
<div style={{ display: 'flex', gap: 4, marginLeft: 'auto', flexWrap: 'wrap' }}>
<TabBtn active={tab === 'setup'} onClick={() => setTab('setup')}>Setup</TabBtn>
<TabBtn active={tab === 'rhino'} onClick={() => setTab('rhino')}>Rhino</TabBtn>
<TabBtn active={tab === 'view'} onClick={() => setTab('view')}>View</TabBtn>
<TabBtn active={tab === 'ebenen'} onClick={() => setTab('ebenen')}>Ebenen</TabBtn>
@@ -759,6 +767,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
</div>
</header>
<div className="body">
{tab === 'setup' && <SetupSettings />}
{tab === 'rhino' && <RhinoSettings />}
{tab === 'view' && <ViewSettings />}
{tab === 'ebenen' && <EbenenSchemaSettings />}
@@ -774,6 +783,148 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
)
}
function SetupSettings() {
const [running, setRunning] = useState(false)
const [result, setResult] = useState(null) // { steps, overall_ok }
const [error, setError] = useState(null)
const [rhinoBusy, setRhinoBusy] = useState(false)
const [status, setStatus] = useState(null) // { plugin_installed, startup_cmd_set, layout_installed, initialized }
const [rhinoApp, setRhinoApp] = useState('')
const [startupPath, setStartupPath] = useState('')
// Live-Check: ob Rhino laeuft (Init kann nicht laufen wenn ja)
useEffect(() => {
let cancelled = false
const tick = () => {
invoke('is_rhino_running')
.then(v => { if (!cancelled) setRhinoBusy(!!v) })
.catch(() => {})
}
tick()
const id = setInterval(tick, 2000)
return () => { cancelled = true; clearInterval(id) }
}, [])
// Initialer State-Check + erkannte Rhino-Konfig
const refreshStatus = useCallback(() => {
invoke('check_dossier_initialized').then(setStatus).catch(() => {})
}, [])
useEffect(() => {
refreshStatus()
invoke('read_settings').then(s => setRhinoApp(s?.rhinoApp || 'Rhinoceros 8')).catch(() => {})
invoke('get_default_plugin_startup_path').then(setStartupPath).catch(() => {})
}, [refreshStatus])
const runInit = async () => {
setRunning(true); setError(null); setResult(null)
try {
const r = await invoke('dossier_init')
setResult(r)
refreshStatus()
} catch (e) {
setError(typeof e === 'string' ? e : (e?.message || String(e)))
} finally {
setRunning(false)
}
}
const dot = (ok) => (
<span style={{
display: 'inline-block', width: 8, height: 8, borderRadius: 4,
background: ok ? 'var(--accent)' : '#e87b6b', marginRight: 8,
}} />
)
return (
<div>
<h3 style={{ marginTop: 0 }}>DOSSIER einrichten</h3>
<p style={{ fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
Setzt DOSSIER auf einem frischen Mac komplett auf: installiert das C#-Plugin in Rhino via Yak,
traegt den Python-Bootstrap als Startup-Command ein, und kopiert das DOSSIER-Window-Layout in Rhinos
Workspaces-Folder. Idempotent kann mehrfach ausgefuehrt werden.
</p>
{/* Erkannte Konfiguration */}
<div style={{
marginTop: 14, padding: 10, background: 'rgba(255,255,255,0.04)',
border: '1px solid var(--border)', borderRadius: 6, fontSize: 11,
}}>
<div style={{ color: 'var(--text-muted)', marginBottom: 6 }}>Erkannte Konfiguration:</div>
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 4 }}>
<span style={{ color: 'var(--text-muted)' }}>Rhino-App:</span>
<span>{rhinoApp || '(nicht gesetzt)'}</span>
<span style={{ color: 'var(--text-muted)' }}>startup.py:</span>
<span style={{ wordBreak: 'break-all' }}>{startupPath || '(nicht gefunden)'}</span>
</div>
<div style={{ marginTop: 6, color: 'var(--text-muted)', fontSize: 10 }}>
(Aendern unter Settings Rhino)
</div>
</div>
{/* Aktueller Install-Status (live) */}
{status && (
<div style={{ marginTop: 12, fontSize: 11 }}>
<div style={{ color: 'var(--text-muted)', marginBottom: 4 }}>Status:</div>
<div>{dot(status.plugin_installed)}DOSSIER-Plugin (.rhp) installiert</div>
<div>{dot(status.startup_cmd_set)}Python-Bootstrap in Rhino-StartupCommands</div>
<div>{dot(status.layout_installed)}Window-Layout in Rhino-Workspaces</div>
</div>
)}
<p style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 14 }}>
Hinweis: Rhino muss waehrend des Setups <strong>geschlossen</strong> sein.
</p>
<div style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
<button
className="primary pill"
onClick={runInit}
disabled={running || rhinoBusy}
title={rhinoBusy ? 'Rhino laeuft — bitte beenden' : 'Setup starten'}
>
{running ? 'Setup laeuft…' : 'Setup starten'}
</button>
{rhinoBusy && (
<span style={{ fontSize: 11, color: '#e87b6b' }}>
Rhino laeuft bitte beenden.
</span>
)}
</div>
{result && (
<ul style={{ listStyle: 'none', padding: 0, marginTop: 20, borderTop: '1px solid var(--border)' }}>
{result.steps.map(s => (
<li key={s.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 10,
padding: '10px 0', borderBottom: '1px solid var(--border)' }}>
<span style={{ width: 16, fontSize: 14,
color: s.status === 'ok' ? 'var(--accent)' : '#e87b6b' }}>
{s.status === 'ok' ? '✓' : '✗'}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12 }}>{s.label}</div>
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2,
wordBreak: 'break-all' }}>
{s.detail}
</div>
</div>
</li>
))}
<li style={{ padding: '10px 0', fontSize: 11,
color: result.overall_ok ? 'var(--accent)' : '#e87b6b' }}>
{result.overall_ok
? '✓ Alle Schritte erfolgreich. Rhino oeffnen — Plugin laedt bei erstem dWall/dDoor/...-Aufruf, startup.py bootstrappt automatisch.'
: '✗ Mindestens ein Schritt ist fehlgeschlagen. Details oben.'}
</li>
</ul>
)}
{error && (
<p style={{ color: '#e87b6b', marginTop: 12, fontSize: 12 }}>{error}</p>
)}
</div>
)
}
function TabBtn({ active, onClick, children }) {
return (
<button
+1 -1
View File
@@ -76,7 +76,7 @@ else:
print(" err:", ex)
if css is None:
print(" None (kein Custom-SectionStyle gesetzt)")
print(" None (kein Custom-SectionStyle set)")
else:
print(" Type:", type(css).__name__)
print("")
+1 -1
View File
@@ -23,7 +23,7 @@ for dm in DisplayModeDescription.GetDisplayModes():
dmd = dm; break
if dmd is None:
print("[INSPECT] 'Dossier Plan' nicht gefunden")
print("[INSPECT] 'Dossier Plan' not found")
else:
attrs = dmd.DisplayAttributes
print("[INSPECT] Mode gefunden: {} (Id={})".format(dmd.EnglishName, dmd.Id))
+3 -3
View File
@@ -109,7 +109,7 @@ def _try_borderless_mac(form):
style_type = type(current)
borderless = System.Enum.ToObject(style_type, 0)
nswindow.StyleMask = borderless
print("[SPLASH] StyleMask=0 (Borderless) gesetzt")
print("[SPLASH] StyleMask=0 (Borderless) applied")
ok = True
except Exception as ex:
print("[SPLASH] StyleMask Enum:", ex)
@@ -120,7 +120,7 @@ def _try_borderless_mac(form):
style_type = type(current)
full = System.Enum.ToObject(style_type, 1 | 32768)
nswindow.StyleMask = full
print("[SPLASH] StyleMask=Titled|FullSize gesetzt (Fallback)")
print("[SPLASH] StyleMask=Titled|FullSize set (Fallback)")
ok = True
except Exception as ex2:
print("[SPLASH] StyleMask Fallback:", ex2)
@@ -333,7 +333,7 @@ def show():
# die NSWindow existiert
try:
if _try_borderless_mac(form):
print("[SPLASH] Borderless (Mac NSWindow) angewendet")
print("[SPLASH] Borderless (Mac NSWindow) applied")
except Exception as ex:
print("[SPLASH] borderless-mac:", ex)
# WebView transparent (rounded corners via HTML border-radius)
+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")
+39 -39
View File
@@ -149,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):
@@ -243,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
@@ -257,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"))
@@ -274,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:
@@ -300,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
@@ -450,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
@@ -466,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-
@@ -483,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
@@ -495,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.
@@ -523,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
@@ -540,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
@@ -556,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
@@ -584,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:
@@ -600,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
@@ -610,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()
@@ -621,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
@@ -630,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
@@ -653,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")
@@ -668,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 []):
@@ -714,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
@@ -738,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
@@ -773,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}
@@ -785,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": {
@@ -826,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
@@ -848,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)
+1 -1
View File
@@ -50,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)
+20 -7
View File
@@ -183,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
@@ -201,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
@@ -227,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
@@ -586,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)
@@ -606,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():
+4853 -505
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -56,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)
+16 -16
View File
@@ -82,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
@@ -102,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,)
@@ -154,7 +154,7 @@ 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))
@@ -170,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()
@@ -341,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
@@ -384,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))))
@@ -433,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:
@@ -487,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))
@@ -532,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:
@@ -540,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")
@@ -656,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):
@@ -682,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):
+257 -101
View File
@@ -88,14 +88,14 @@ def _list_hatch_patterns_full(doc):
try: lines = hp.GetHatchLines()
except Exception: pass
if lines is None:
print("[EBENEN] hp[{}] '{}' HatchLines=None".format(
print("[LAYERS] hp[{}] '{}' HatchLines=None".format(
i, hp.Name))
else:
try: cnt = len(lines)
except Exception:
try: cnt = lines.Count
except Exception: cnt = -1
print("[EBENEN] hp[{}] '{}' HatchLines type={} count={}".format(
print("[LAYERS] hp[{}] '{}' HatchLines type={} count={}".format(
i, hp.Name, type(lines).__name__, cnt))
for hl in lines:
try:
@@ -132,9 +132,9 @@ def _list_hatch_patterns_full(doc):
}
hlines.append(entry)
except Exception as ex:
print("[EBENEN] hp[{}] hl FAIL:".format(i), ex)
print("[LAYERS] hp[{}] hl FAIL:".format(i), ex)
except Exception as ex:
print("[EBENEN] hp[{}] HatchLines outer FAIL:".format(i), ex)
print("[LAYERS] hp[{}] HatchLines outer FAIL:".format(i), ex)
out.append({
"index": i,
"name": hp.Name or "",
@@ -146,7 +146,7 @@ def _list_hatch_patterns_full(doc):
except Exception:
continue
except Exception as ex:
print("[EBENEN] _list_hatch_patterns_full:", ex)
print("[LAYERS] _list_hatch_patterns_full:", ex)
return out
@@ -198,7 +198,7 @@ def _list_linetypes_full(doc):
stype = "Space"
segs.append({"length": length, "type": stype})
except Exception as ex:
print("[EBENEN] lt[{}] GetSegment({}) FAIL: {}".format(
print("[LAYERS] lt[{}] GetSegment({}) FAIL: {}".format(
i, s, ex))
ref = 0
try: ref = int(lt.Reference)
@@ -211,16 +211,16 @@ def _list_linetypes_full(doc):
"isReference": (ref > 0),
})
except Exception as ex:
print("[EBENEN] linetype outer FAIL:", ex)
print("[LAYERS] linetype outer FAIL:", ex)
continue
except Exception as ex:
print("[EBENEN] _list_linetypes_full:", ex)
print("[LAYERS] _list_linetypes_full:", ex)
return out
def _read_launcher_schema():
"""Liest das Default-Layer-Schema aus dossier_settings.json (Launcher-Pfad).
Liefert eine Liste {code, name, color, lw} oder None wenn nicht gesetzt."""
Liefert eine Liste {code, name, color, lw} oder None wenn nicht set."""
paths = [
os.path.expanduser("~/Library/Application Support/"
"ch.gabrielevarano.Dossier/dossier_settings.json"),
@@ -234,7 +234,7 @@ def _read_launcher_schema():
data = json.loads(f.read().decode("utf-8"))
schema = (data or {}).get("layerSchema")
if isinstance(schema, list) and schema:
# Sanity: alle vier Pflichtfelder vorhanden
# Sanity: alle vier Pflichtfelder present
clean = [r for r in schema
if isinstance(r, dict)
and r.get("code") and r.get("name")
@@ -242,7 +242,7 @@ def _read_launcher_schema():
and r.get("lw") is not None]
if clean: return clean
except Exception as ex:
print("[EBENEN] launcher-schema lesen ({}):".format(p), ex)
print("[LAYERS] launcher-schema lesen ({}):".format(p), ex)
return None
@@ -272,7 +272,7 @@ def _broadcast_state(doc=None, hatch_patterns=None):
"layerCombinationActive": get_active_comb_name(doc),
}
except Exception as ex:
print("[EBENEN] broadcast prepare:", ex)
print("[LAYERS] broadcast prepare:", ex)
return
for key in ("ebenen_bridge_ref", "zeichnungsebenen_bridge_ref"):
b = sc.sticky.get(key)
@@ -309,6 +309,63 @@ _PROJECT_SETTINGS_DEFAULTS = {
"unit": "meters", # "meters" | "millimeters" | "centimeters"
},
"materials": [],
# Wand-Stile: Wand-Typ-Templates mit Prio fuer Joint-Dominanz.
#
# SOLID-Style (layered=False):
# material = Material-Identitaet (driver fuer Section-Hatch)
# dicke = DEFAULT bei Erstellung — User kann pro Wand ueberschreiben
# referenz = DEFAULT
#
# LAYERED-Style (layered=True):
# layers = fixe Schicht-Komposition mit Material+Dicke pro Layer
# dicke = ignoriert (kommt aus Summe der Layers)
#
# PRIO (1-999, 999=dominant): zwei Wand-Stile koennen das gleiche Material
# haben aber verschiedene Prios (z.B. Beton tragend prio=800,
# Beton innen prio=400). Bei Joints zwischen verschiedenen Stilen gewinnt
# der mit hoeherer Prio die Ecke (Phase 3, noch nicht implementiert).
#
# SECTION-MERGE (Phase 2, noch nicht implementiert): Hatches mergen visuell
# an Joints zwischen Waenden mit GLEICHEM Material — egal ob gleicher Stil.
# So sind „Beton tragend" und „Beton innen" im Schnitt verbunden.
"wand_styles": [
{"id": "style_beton_tragend", "name": "Beton tragend",
"prio": 800, "dicke": 0.25, "referenz": "mid",
"layered": False, "material": "Stahlbeton", "layers": []},
{"id": "style_beton_innen", "name": "Beton innen",
"prio": 400, "dicke": 0.15, "referenz": "mid",
"layered": False, "material": "Stahlbeton", "layers": []},
{"id": "style_gips", "name": "Gipswand",
"prio": 200, "dicke": 0.10, "referenz": "mid",
"layered": False, "material": "Putz", "layers": []},
{"id": "style_mauerwerk", "name": "Mauerwerk",
"prio": 300, "dicke": 0.12, "referenz": "mid",
"layered": False, "material": "Mauerwerk", "layers": []},
{"id": "style_aussen30", "name": "Aussenwand 30 cm gedaemmt",
"prio": 900, "dicke": 0.30, "referenz": "left",
"layered": True, "material": "",
"layers": [
{"material": "Stahlbeton", "dicke": 0.18},
{"material": "Daemmung", "dicke": 0.10},
{"material": "Putz", "dicke": 0.02},
]},
{"id": "style_innen_holz", "name": "Innenwand Holzstaender 14 cm",
"prio": 250, "dicke": 0.14, "referenz": "mid",
"layered": True, "material": "",
"layers": [
{"material": "Putz", "dicke": 0.015},
{"material": "Holzstaender", "dicke": 0.110},
{"material": "Putz", "dicke": 0.015},
]},
{"id": "style_innen_beton", "name": "Innenwand Beton 20 cm",
"prio": 700, "dicke": 0.23, "referenz": "mid",
"layered": True, "material": "",
"layers": [
{"material": "Putz", "dicke": 0.015},
{"material": "Stahlbeton", "dicke": 0.200},
{"material": "Putz", "dicke": 0.015},
]},
],
"project": {
"name": "",
"number": "",
@@ -379,6 +436,48 @@ def _normalize_project_meta(p):
}
def _normalize_wand_style(s):
"""Garantiert Wand-Style-Schema. Stil = Bundle aus Geometrie + Prio.
Felder:
- id (str, kebab-case empfohlen), name (str)
- prio (int 1-999): bei Joints zwischen verschiedenen Stilen wins der hoehere
- dicke (float, Meter)
- referenz ('mid'|'left'|'right')
- layered (bool): wenn True, layers definieren die Schichten
- material (str): bei layered=False der Material-Name
- layers (list of {material, dicke}): bei layered=True"""
if not isinstance(s, dict): return None
sid = str(s.get("id") or "").strip()
if not sid: return None
try: prio = int(s.get("prio", 500))
except Exception: prio = 500
prio = max(1, min(999, prio))
try: dicke = float(s.get("dicke", 0.25))
except Exception: dicke = 0.25
ref = str(s.get("referenz") or "mid")
if ref not in ("mid", "left", "right"): ref = "mid"
layered = bool(s.get("layered"))
layers = []
if layered and isinstance(s.get("layers"), list):
for ly in s["layers"]:
if not isinstance(ly, dict): continue
try: ld = float(ly.get("dicke", 0))
except Exception: ld = 0.0
if ld <= 0: continue
layers.append({"material": str(ly.get("material") or ""),
"dicke": ld})
return {
"id": sid,
"name": str(s.get("name") or sid),
"prio": prio,
"dicke": dicke,
"referenz": ref,
"layered": layered,
"material": str(s.get("material") or ""),
"layers": layers,
}
def _normalize_material(m):
"""Garantiert Material-Schema. Material ist REIN 3D — Section-Hatch
(2D-Schnitt) wird via Ebenen-Settings am Layer konfiguriert.
@@ -442,6 +541,7 @@ def load_project_settings(doc):
out = {
"defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]),
"materials": list(_PROJECT_SETTINGS_DEFAULTS["materials"]),
"wand_styles": [dict(s) for s in _PROJECT_SETTINGS_DEFAULTS["wand_styles"]],
"project": dict(_PROJECT_SETTINGS_DEFAULTS["project"]),
}
if raw:
@@ -458,6 +558,12 @@ def load_project_settings(doc):
_normalize_material(x) for x in m
if _normalize_material(x) is not None
]
ws = data.get("wand_styles")
if isinstance(ws, list):
out["wand_styles"] = [
_normalize_wand_style(s) for s in ws
if _normalize_wand_style(s) is not None
]
pr = data.get("project")
if isinstance(pr, dict):
out["project"] = _normalize_project_meta(pr)
@@ -515,7 +621,7 @@ def store_layer_presets(doc, presets):
doc.Strings.SetString(_PRESETS_KEY,
json.dumps(presets, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] store_layer_presets:", ex)
print("[LAYERS] store_layer_presets:", ex)
def get_active_comb_name(doc):
@@ -530,7 +636,7 @@ def set_active_comb_name(doc, name):
try:
doc.Strings.SetString(_ACTIVE_COMB_KEY, name or "")
except Exception as ex:
print("[EBENEN] set_active_comb_name:", ex)
print("[LAYERS] set_active_comb_name:", ex)
def list_layer_preset_names(doc):
@@ -547,7 +653,7 @@ def _notify_oberleiste_combs():
b._cached_combinations = None
b._send_state(force=True)
except Exception as ex:
print("[EBENEN] notify oberleiste combs:", ex)
print("[LAYERS] notify oberleiste combs:", ex)
def _notify_layer_combinations_editor():
@@ -557,7 +663,7 @@ def _notify_layer_combinations_editor():
b = sc.sticky.get("layer_combinations_bridge")
if b is not None: b._send_state()
except Exception as ex:
print("[EBENEN] notify layer-combinations editor:", ex)
print("[LAYERS] notify layer-combinations editor:", ex)
def apply_layer_preset_by_name(doc, name):
@@ -567,7 +673,7 @@ def apply_layer_preset_by_name(doc, name):
presets = load_layer_presets(doc)
preset = next((p for p in presets if p.get("name") == name), None)
if preset is None:
print("[EBENEN] apply_layer_preset_by_name: '{}' nicht gefunden".format(name))
print("[LAYERS] apply_layer_preset_by_name: '{}' not found".format(name))
return False
payload = {
"layers": preset.get("layers") or [],
@@ -581,7 +687,7 @@ def apply_layer_preset_by_name(doc, name):
try:
eb._apply_combination(payload)
except Exception as ex:
print("[EBENEN] apply via bridge:", ex)
print("[LAYERS] apply via bridge:", ex)
return False
else:
# Fallback: direkt doc.Strings + doc.Layer setzen (kein Bridge offen)
@@ -619,7 +725,7 @@ def _apply_layer_preset_inline(doc, payload):
doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False))
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False))
except Exception as ex:
print("[EBENEN] inline preset-apply (eye-state):", ex)
print("[LAYERS] inline preset-apply (eye-state):", ex)
# Layer-ID-Pfad als Sekundaer (AUSSCHNITTE-Kompat)
layer_states = payload.get("layers") or []
if layer_states:
@@ -661,7 +767,7 @@ def save_current_as_layer_preset(doc, name):
"locked": bool(layer.IsLocked),
})
except Exception as ex:
print("[EBENEN] save_current_as_layer_preset enum:", ex)
print("[LAYERS] save_current_as_layer_preset enum:", ex)
# 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen
pe_state, pz_state = [], []
try:
@@ -683,7 +789,7 @@ def save_current_as_layer_preset(doc, name):
"visible": bool(z.get("visible", True)),
})
except Exception as ex:
print("[EBENEN] save_current_as_layer_preset eye-states:", ex)
print("[LAYERS] save_current_as_layer_preset eye-states:", ex)
presets = load_layer_presets(doc)
new_data = {
"name": name,
@@ -700,7 +806,7 @@ def save_current_as_layer_preset(doc, name):
set_active_comb_name(doc, name)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
print("[LAYERS] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format(
name, len(layers), len(pe_state)))
return True
@@ -714,7 +820,7 @@ def delete_layer_preset(doc, name):
set_active_comb_name(doc, None)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] Kombination '{}' geloescht".format(name))
print("[LAYERS] Kombination '{}' geloescht".format(name))
return True
@@ -764,7 +870,7 @@ class EbenenBridge(panel_base.BaseBridge):
"hatchPatterns": _hatch_pattern_names(doc),
})
except Exception as ex:
print("[EBENEN] State-Sync:", ex)
print("[LAYERS] State-Sync:", ex)
else:
payload = {"hatchPatterns": _hatch_pattern_names(doc)}
# Falls der User im Launcher eigene Default-Ebenen definiert hat,
@@ -772,7 +878,7 @@ class EbenenBridge(panel_base.BaseBridge):
launcher_schema = _read_launcher_schema()
if launcher_schema:
payload["defaultEbenen"] = launcher_schema
print("[EBENEN] FIRST_RUN mit Launcher-Schema ({} Ebenen)".format(
print("[LAYERS] FIRST_RUN mit Launcher-Schema ({} Ebenen)".format(
len(launcher_schema)))
self.send("FIRST_RUN", payload)
@@ -788,7 +894,7 @@ class EbenenBridge(panel_base.BaseBridge):
if t == "READY":
self._on_ready()
elif t == "APPLY":
print("[EBENEN-BE] APPLY from mode={} payload-z={} payload-e={}".format(
print("[LAYERS-BE] APPLY from mode={} payload-z={} payload-e={}".format(
self._mode,
len(p.get("zeichnungsebenen") or []),
len(p.get("ebenen") or [])))
@@ -796,13 +902,13 @@ class EbenenBridge(panel_base.BaseBridge):
z_payload = p.get("zeichnungsebenen") or []
e_raw = doc.Strings.GetValue("dossier_ebenen")
e_payload = json.loads(e_raw) if e_raw else []
print("[EBENEN-BE] mode=zeichnungsebenen: e from doc.Strings n={}".format(len(e_payload)))
print("[LAYERS-BE] mode=zeichnungsebenen: e from doc.Strings n={}".format(len(e_payload)))
self._apply(z_payload, e_payload, save_z=True, save_e=False)
else:
e_payload = p.get("ebenen") or []
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
z_payload = json.loads(z_raw) if z_raw else []
print("[EBENEN-BE] mode=ebenen: z from doc.Strings n={}".format(len(z_payload)))
print("[LAYERS-BE] mode=ebenen: z from doc.Strings n={}".format(len(z_payload)))
self._apply(z_payload, e_payload, save_z=False, save_e=True)
elif t == "LAYER_STYLE":
layer_builder.update_layer_style(doc, p["code"], p.get("color"), p.get("lw"))
@@ -836,16 +942,16 @@ class EbenenBridge(panel_base.BaseBridge):
if new_z is not None:
self._set_active_zeichnungsebene(new_z)
except Exception as ex:
print("[SCHNITT] auto-activate:", ex)
print("[SECTION] auto-activate:", ex)
except Exception as ex:
print("[SCHNITT] CREATE_SCHNITT:", ex)
print("[SECTION] CREATE_SCHNITT:", ex)
elif t == "DELETE_SCHNITT":
try:
import schnitte
if schnitte.delete_schnitt_entry(doc, p.get("id") or ""):
_broadcast_state(doc)
except Exception as ex:
print("[SCHNITT] DELETE_SCHNITT:", ex)
print("[SECTION] DELETE_SCHNITT:", ex)
elif t == "SET_ACTIVE_LAYER":
code = p.get("code", "")
if code:
@@ -909,21 +1015,21 @@ class EbenenBridge(panel_base.BaseBridge):
elif t == "OPEN_LAYER_COMBINATIONS_DIALOG":
try: open_layer_combinations_window()
except Exception as ex:
print("[EBENEN] open layer-combinations:", ex)
print("[LAYERS] open layer-combinations:", ex)
elif t == "OPEN_PROJECT_SETTINGS":
try: self._open_project_settings()
except Exception as ex:
print("[EBENEN] open project-settings:", ex)
print("[LAYERS] open project-settings:", ex)
elif t == "OPEN_LIBRARY":
try: self._open_library()
except Exception as ex:
print("[EBENEN] open library:", ex)
print("[LAYERS] open library:", ex)
elif t == "PICK_TEXTURE_FILE":
# Oeffnet macOS-File-Picker fuer Bild-Dateien. Antwort an
# Frontend via TEXTURE_PICKED-Message.
try: self._pick_texture_file(p)
except Exception as ex:
print("[EBENEN] pick texture:", ex)
print("[LAYERS] pick texture:", ex)
# ---- Helpers ----
@@ -976,6 +1082,7 @@ class EbenenBridge(panel_base.BaseBridge):
"defaults": current.get("defaults", {}),
"project": current.get("project", {}),
"materials": current.get("materials", []),
"wandStyles": current.get("wand_styles", []),
"builtinMaterials": built_in,
"hatchPatterns": _hatch_pattern_names(doc),
"hatchPatternsFull": _list_hatch_patterns_full(doc),
@@ -989,9 +1096,12 @@ class EbenenBridge(panel_base.BaseBridge):
def on_save(updated):
doc2 = Rhino.RhinoDoc.ActiveDoc
if doc2 is None: return
# Snapshot OLD settings VOR dem Save fuer Stale-Detection
old_settings = load_project_settings(doc2) or {}
new_settings = {
"defaults": updated.get("defaults", {}),
"materials": updated.get("materials", []),
"wand_styles": updated.get("wandStyles", []),
"project": updated.get("project", {}),
}
save_project_settings(doc2, new_settings)
@@ -1010,33 +1120,79 @@ class EbenenBridge(panel_base.BaseBridge):
if eb is not None: eb._send_state()
except Exception as ex:
print("[PROJECT-SETTINGS] elemente refresh:", ex)
# Alle wand_axis im Doc regenen damit Material-Aenderungen
# (PBR/Texturen/Hatch) auf existing Waende durchschlagen.
# Stale-Detection: welche Wand-Styles oder Materials haben sich
# geaendert? Nur betroffene Waende regenen (statt alle).
try:
import elemente
undo_serial = doc2.BeginUndoRecord("Material-Update Regen")
prev_redraw = doc2.Views.RedrawEnabled
doc2.Views.RedrawEnabled = False
old_styles = {s.get("id"): s for s in
(old_settings.get("wand_styles") or [])
if isinstance(s, dict) and s.get("id")}
new_styles = {s.get("id"): s for s in
(new_settings.get("wand_styles") or [])
if isinstance(s, dict) and s.get("id")}
changed_style_ids = set()
for sid in (set(old_styles) | set(new_styles)):
if old_styles.get(sid) != new_styles.get(sid):
changed_style_ids.add(sid)
old_mats = {m.get("name"): m for m in
(old_settings.get("materials") or [])
if isinstance(m, dict) and m.get("name")}
new_mats = {m.get("name"): m for m in
(new_settings.get("materials") or [])
if isinstance(m, dict) and m.get("name")}
changed_material_names = set()
for n in (set(old_mats) | set(new_mats)):
if old_mats.get(n) != new_mats.get(n):
changed_material_names.add(n)
# Welche Waende sind betroffen?
wall_ids = []
seen = set()
for obj in doc2.Objects:
m = elemente._read_meta(obj)
if m and m.get("type") == "wand_axis":
wall_ids.append(m["id"])
# Chain-Anchor regent automatisch alle members — wir koennen
# trotzdem alle einzeln triggern, _REGEN_BUSY-Guard verhindert
# Doppel-Arbeit. Einfacher als Anchor-Election hier.
if not m or m.get("type") != "wand_axis": continue
wid = m["id"]
if wid in seen: continue
affected = False
sid = (m.get("wand_style_id") or "").strip()
if sid and sid in changed_style_ids:
affected = True
if not affected and m.get("wand_layered"):
for layer in (m.get("wand_layers") or []):
if isinstance(layer, dict) and layer.get("material") \
in changed_material_names:
affected = True; break
if not affected and sid and changed_material_names:
# Solid-Wand mit Style: Material-Aenderung des
# Style-referenzierten Materials trifft sie
st = new_styles.get(sid) or old_styles.get(sid)
if st and st.get("material") in changed_material_names:
affected = True
if affected:
wall_ids.append(wid); seen.add(wid)
if not wall_ids:
print("[PROJECT-SETTINGS] keine wand-relevanten Aenderungen")
return
undo_serial = doc2.BeginUndoRecord(
"Stil-Update Regen ({})".format(len(wall_ids)))
prev_redraw = doc2.Views.RedrawEnabled
doc2.Views.RedrawEnabled = False
try:
sc.sticky["_dossier_regen_batch_active"] = True
for wid in wall_ids:
try: elemente._regenerate_element(doc2, wid)
except Exception as ex:
print("[PROJECT-SETTINGS] regen", wid, ex)
finally:
sc.sticky["_dossier_regen_batch_active"] = False
doc2.Views.RedrawEnabled = prev_redraw
try: doc2.EndUndoRecord(undo_serial)
except Exception: pass
try: doc2.Views.Redraw()
except Exception: pass
print("[PROJECT-SETTINGS] {} Waende regenert".format(len(wall_ids)))
print("[PROJECT-SETTINGS] {} Waende regenert ({} Stile, "
"{} Materials geaendert)".format(
len(wall_ids), len(changed_style_ids),
len(changed_material_names)))
except Exception as ex:
print("[PROJECT-SETTINGS] wall regen:", ex)
# Custom-Bridge fuer Project-Settings: handelt SAVE/CANCEL +
@@ -1465,7 +1621,7 @@ class EbenenBridge(panel_base.BaseBridge):
def _send_library(self):
"""Sendet aktuelle Library-Items ans Frontend
(LIBRARY_ITEMS-Message). Items kommen mit
previewDataUri (base64 PNG) wenn Vorschau vorhanden."""
previewDataUri (base64 PNG) wenn Vorschau present."""
try:
import library
m = library.load_manifest()
@@ -1482,7 +1638,7 @@ class EbenenBridge(panel_base.BaseBridge):
"""Datei-Picker → kopieren in library/assets/ → Item zum
Manifest hinzufuegen. Payload: {variant: '2d'|'3d',
targetId: ?, itemType: 'symbol'|'object'}.
Wenn targetId gesetzt: bestehendes Item updaten (Datei
Wenn targetId set: bestehendes Item updaten (Datei
wird seinem files2d/3d zugewiesen). Sonst: neues Item."""
try:
import library
@@ -1587,7 +1743,7 @@ class EbenenBridge(panel_base.BaseBridge):
def _save_selection_as_library(self, payload):
"""Aktuelle Selection im Doc als Library-Item speichern.
payload: {variant: '2d'|'3d', targetId?: str, itemType?: str}.
Wenn targetId gesetzt: bestehendes Item updaten. Sonst:
Wenn targetId set: bestehendes Item updaten. Sonst:
Rhino-Prompt nach Name + neues Item anlegen."""
d = Rhino.RhinoDoc.ActiveDoc
if d is None: return
@@ -1687,7 +1843,7 @@ class EbenenBridge(panel_base.BaseBridge):
path = dlg.FileName or ""
self.send("TEXTURE_PICKED", {"slot": slot, "path": path})
except Exception as ex:
print("[EBENEN] pick texture:", ex)
print("[LAYERS] pick texture:", ex)
self.send("TEXTURE_PICKED", {"slot": slot, "path": None})
def _open_library(self):
@@ -1757,7 +1913,7 @@ class EbenenBridge(panel_base.BaseBridge):
GeschossSettingsDialog. Save updated den Eintrag in doc.Strings +
triggert Cross-Panel-Sync."""
if not isinstance(geschoss, dict) or not geschoss.get("id"):
print("[EBENEN] open_geschoss_settings: kein Geschoss-Payload")
print("[LAYERS] open_geschoss_settings: kein Geschoss-Payload")
return
gid = geschoss["id"]
doc = Rhino.RhinoDoc.ActiveDoc
@@ -1779,16 +1935,16 @@ class EbenenBridge(panel_base.BaseBridge):
val = updated.pop("projectZeroMum")
val = float(val) if val is not None else 0.0
doc.Strings.SetString("dossier_project_zero_mum", str(val))
print("[EBENEN] project_zero_mum = {} m.ü.M".format(val))
print("[LAYERS] project_zero_mum = {} m.ü.M".format(val))
except Exception as ex:
print("[EBENEN] project_zero_mum save:", ex)
print("[LAYERS] project_zero_mum save:", ex)
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if not z_raw:
print("[EBENEN] save_geschoss: kein z-Store"); return
print("[LAYERS] save_geschoss: kein z-Store"); return
try:
z_list = json.loads(z_raw)
except Exception as ex:
print("[EBENEN] save_geschoss JSON:", ex); return
print("[LAYERS] save_geschoss JSON:", ex); return
replaced = False
for i, z in enumerate(z_list):
if isinstance(z, dict) and z.get("id") == gid:
@@ -1796,7 +1952,7 @@ class EbenenBridge(panel_base.BaseBridge):
replaced = True
break
if not replaced:
print("[EBENEN] save_geschoss: id {} nicht gefunden".format(gid))
print("[LAYERS] save_geschoss: id {} not found".format(gid))
return
# Build_layers + Save via _apply (durchlaeuft ohne save_e)
e_raw = doc.Strings.GetValue("dossier_ebenen")
@@ -1814,7 +1970,7 @@ class EbenenBridge(panel_base.BaseBridge):
import schnitte
schnitte.activate_schnitt(doc, updated)
except Exception as ex:
print("[SCHNITT] post-save reactivate:", ex)
print("[SECTION] post-save reactivate:", ex)
panel_base.open_satellite_window(
"geschoss_settings",
params=params,
@@ -1828,7 +1984,7 @@ class EbenenBridge(panel_base.BaseBridge):
die aktuelle Ebene live (SAVE_KEEP), Schliess-/Übernehmen-Knopf
persistiert + schliesst (SAVE)."""
if not isinstance(ebene, dict) or not ebene.get("code"):
print("[EBENEN] open_ebenen_settings: kein Ebene-Payload")
print("[LAYERS] open_ebenen_settings: kein Ebene-Payload")
return
bridge_holder = {"form": None}
apply_self = self
@@ -1879,7 +2035,7 @@ class EbenenBridge(panel_base.BaseBridge):
if not e_raw: return
try: e_list = json.loads(e_raw)
except Exception as ex:
print("[EBENEN] save_ebene JSON:", ex); return
print("[LAYERS] save_ebene JSON:", ex); return
# Rekursive Suche + Replace durch den Tree — Sub-Ebenen
# (children) liegen verschachtelt, nicht in der Top-Level-Liste.
def _replace_in_tree(lst, target_code, new_data):
@@ -1899,7 +2055,7 @@ class EbenenBridge(panel_base.BaseBridge):
return False
replaced = _replace_in_tree(e_list, orig_code, updated)
if not replaced:
print("[EBENEN] save_ebene: code {} nicht gefunden".format(orig_code))
print("[LAYERS] save_ebene: code {} not found".format(orig_code))
return
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
try: z_list = json.loads(z_raw) if z_raw else []
@@ -1918,7 +2074,7 @@ class EbenenBridge(panel_base.BaseBridge):
"""Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als
Satelliten-Fenster. Save schreibt die ganze z-Liste neu."""
if not isinstance(zeichnungsebenen, list):
print("[EBENEN] open_geschoss_dialog: keine Liste"); return
print("[LAYERS] open_geschoss_dialog: keine Liste"); return
def on_save(payload):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
@@ -1936,7 +2092,7 @@ class EbenenBridge(panel_base.BaseBridge):
on_save=on_save)
def _apply(self, zeichnungsebenen, ebenen, save_z=True, save_e=True):
print("[EBENEN] _apply START z={} e={} (save_z={} save_e={})".format(
print("[LAYERS] _apply START z={} e={} (save_z={} save_e={})".format(
len(zeichnungsebenen) if zeichnungsebenen else 0,
len(ebenen) if ebenen else 0, save_z, save_e))
doc = Rhino.RhinoDoc.ActiveDoc
@@ -1991,19 +2147,19 @@ class EbenenBridge(panel_base.BaseBridge):
new_ids = _schn.schnitt_ids_in_list(zeichnungsebenen)
schnitte_removed = old_ids - new_ids
except Exception as ex:
print("[SCHNITT] cleanup detection:", ex)
print("[SECTION] cleanup detection:", ex)
_set_processing(True)
try:
print("[EBENEN] _apply: build_layers ...")
print("[LAYERS] _apply: build_layers ...")
layer_builder.build_layers(doc, zeichnungsebenen, ebenen)
print("[EBENEN] _apply: json.dumps ...")
print("[LAYERS] _apply: json.dumps ...")
# WICHTIG: ensure_ascii=False umgeht einen Bug in Rhinos eigener
# json/encoder.py die bei ASCII-escape s.decode('utf-8') aufruft
# und dabei mit 0xC4 (Umlaut) in den CP1252-Decoder lauft.
z_json = json.dumps(zeichnungsebenen, ensure_ascii=False)
e_json = json.dumps(ebenen, ensure_ascii=False)
print("[EBENEN] _apply: SetString ...")
print("[LAYERS] _apply: SetString ...")
if save_z:
doc.Strings.SetString("dossier_zeichnungsebenen", z_json)
if save_e:
@@ -2019,10 +2175,10 @@ class EbenenBridge(panel_base.BaseBridge):
for sid in schnitte_removed:
n_total += _schn.cleanup_schnitt_artifacts(
doc, sid, active_id=active_id)
print("[SCHNITT] {} Schnitt(e) geloescht, {} Symbol-Curves entfernt".format(
print("[SECTION] {} Schnitt(e) geloescht, {} Symbol-Curves entfernt".format(
len(schnitte_removed), n_total))
except Exception as ex:
print("[SCHNITT] artifact cleanup:", ex)
print("[SECTION] artifact cleanup:", ex)
# Smart-Elemente (Waende) regenerieren — Geschoss-Hoehen/OKFF
# haben sich evtl. geaendert, gebundene Waende muessen neu
# extrudiert werden. Best-effort, faengt jeden Fehler ab.
@@ -2031,37 +2187,37 @@ class EbenenBridge(panel_base.BaseBridge):
if elem_bridge is not None:
elem_bridge._regenerate_all()
except Exception as _ex:
print("[EBENEN] elemente regen:", _ex)
print("[LAYERS] elemente regen:", _ex)
n_with_fill = sum(1 for e in ebenen if isinstance(e, dict)
and isinstance(e.get("fill"), dict)
and e["fill"].get("pattern") not in (None, "None"))
print("[EBENEN] dossier_ebenen gespeichert: {} Ebenen, davon {} mit fill, JSON-len={}".format(
print("[LAYERS] dossier_ebenen saved: {} layers, of which {} with fill, JSON-len={}".format(
len(ebenen), n_with_fill, len(e_json)))
re_read = doc.Strings.GetValue("dossier_ebenen")
print("[EBENEN] dossier_ebenen verifiziert: len={}".format(len(re_read) if re_read else 0))
print("[EBENEN] _apply: cleanup_default_layers ...")
print("[LAYERS] dossier_ebenen verified: len={}".format(len(re_read) if re_read else 0))
print("[LAYERS] _apply: cleanup_default_layers ...")
layer_builder.cleanup_default_layers(doc)
print("[EBENEN] _apply: ensure_active_sublayer ...")
print("[LAYERS] _apply: ensure_active_sublayer ...")
self._ensure_active_sublayer()
# Existierende 'Nach Ebene'-Hatches an neue Pattern/Skala/Drehung
# angleichen — ABER nur wenn die Fill-Signatur sich tatsaechlich
# geaendert hat (nicht bei reinen Name/Farb-Aenderungen, die das
# Settings-Dialog auch triggern koennte).
try:
import gestaltung
import styles as gestaltung
if fill_changed:
gestaltung.refresh_layer_fills(doc)
else:
print("[EBENEN] _apply: fill-Signatur unveraendert -> kein Hatch-Refresh")
print("[LAYERS] _apply: fill signature unchanged -> no hatch refresh")
# Plot-Color Repair laeuft immer (no-op falls schon synchron)
gestaltung.repair_plot_colors(doc)
except Exception as ex:
print("[EBENEN] gestaltung sync:", ex)
print("[LAYERS] gestaltung sync:", ex)
finally:
_set_processing(False)
print("[EBENEN] _apply: update_clipping ...")
print("[LAYERS] _apply: update_clipping ...")
self._update_clipping()
print("[EBENEN] _apply: send APPLY_OK")
print("[LAYERS] _apply: send APPLY_OK")
self.send("APPLY_OK", {})
# Strukturelle Aenderung (neue/umbenannte/geloeschte Ebene) → aktives
# Preset passt nicht mehr exakt.
@@ -2069,7 +2225,7 @@ class EbenenBridge(panel_base.BaseBridge):
# Anderes Panel (Zeichnungsebenen/Ebenen) ueber den neuen State
# informieren — sonst hinkt es hinter der DOM-Persistenz her.
_broadcast_state(doc)
print("[EBENEN] _apply: DONE")
print("[LAYERS] _apply: DONE")
def _ensure_active_sublayer(self):
"""Setzt den aktiven Rhino-Layer auf den DOSSIER-Sublayer (Fallback: erste Z + 20_WAENDE)."""
@@ -2187,7 +2343,7 @@ class EbenenBridge(panel_base.BaseBridge):
any_changed = (_vis_lock_changed(z_full, merged_z)
or _vis_lock_changed(e_full, merged_e))
if has_new_structural:
print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)")
print("[LAYERS] _apply_visibility: structural change pending → skip save (waiting for APPLY)")
else:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
@@ -2286,7 +2442,7 @@ class EbenenBridge(panel_base.BaseBridge):
if eb is not None: eb._notify_active_geschoss()
except Exception: pass
except Exception as ex:
print("[SCHNITT] activate fehler:", ex)
print("[SECTION] activate fehler:", ex)
return
# Geschoss-Pfad (default): falls vorher ein Schnitt aktiv war,
# dessen Clipping-Planes aufraeumen + Pre-Schnitt-View restoren.
@@ -2296,7 +2452,7 @@ class EbenenBridge(panel_base.BaseBridge):
if prev_was_schnitt:
schnitte.restore_pre_schnitt_view(doc)
except Exception as ex:
print("[SCHNITT] cleanup beim Wechsel auf Geschoss:", ex)
print("[SECTION] cleanup beim Wechsel auf Geschoss:", ex)
# Aktiven Sublayer auf die GLEICHE Ebene unter dem neuen Geschoss
# umschalten — wenn User auf "20 Wände" steht und das Geschoss
# wechselt, soll Rhino's aktiver Layer "1OG::20_Wände" werden statt
@@ -2339,12 +2495,12 @@ class EbenenBridge(panel_base.BaseBridge):
vp.SetConstructionPlane(new_plane)
updated += 1
except Exception as ex:
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
print("[LAYERS] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
# KEIN doc.Views.Redraw() hier — die folgende SET_VISIBILITY-Round-
# trip (30 ms debounce in React) feuert ohnehin layer_builder
# .apply_visibility() das am Ende selbst redrawt. Sparen wir uns
# einen doppelten Full-Repaint pro Geschoss-Klick.
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
print("[LAYERS] CPlane Z={} on {} top-style view(s) set".format(okff, updated))
def _needs_clipping_update(self, doc, prev_active_id, new_z):
"""Liefert True wenn entweder das alte oder das neue Geschoss
@@ -2440,16 +2596,16 @@ class EbenenBridge(panel_base.BaseBridge):
doc = Rhino.RhinoDoc.ActiveDoc
z_id = doc.Strings.GetValue("dossier_active_id")
if not z_id:
print("[EBENEN] Keine aktive Zeichnungsebene")
print("[LAYERS] Keine aktive Zeichnungsebene")
return
parent_idx = layer_builder._find_top_by_id(doc, z_id)
if parent_idx < 0:
print("[EBENEN] Parent fuer aktive Zeichnungsebene nicht gefunden")
print("[LAYERS] Parent fuer aktive Zeichnungsebene not found")
return
parent_id = doc.Layers[parent_idx].Id
sub_idx = layer_builder._find_sublayer_by_code(doc, parent_id, code)
if sub_idx < 0:
print("[EBENEN] Sublayer {} unter {} nicht gefunden".format(code, doc.Layers[parent_idx].Name))
print("[LAYERS] Sublayer {} unter {} not found".format(code, doc.Layers[parent_idx].Name))
return
objs = list(doc.Objects.GetSelectedObjects(False, False))
moved = 0
@@ -2459,7 +2615,7 @@ class EbenenBridge(panel_base.BaseBridge):
if doc.Objects.ModifyAttributes(obj, attrs, True):
moved += 1
doc.Views.Redraw()
print("[EBENEN] {} Objekt(e) auf {} verschoben".format(moved, doc.Layers[sub_idx].FullPath))
print("[LAYERS] {} Objekt(e) auf {} verschoben".format(moved, doc.Layers[sub_idx].FullPath))
def _set_active_sublayer(self, code):
if not code:
@@ -2481,7 +2637,7 @@ class EbenenBridge(panel_base.BaseBridge):
if z_id:
layer_builder.set_active_sublayer(doc, z_id, code)
else:
print("[EBENEN] Aktive Zeichnungsebene unbekannt — Layer wird nicht gesetzt")
print("[LAYERS] Aktive Zeichnungsebene unbekannt — Layer wird nicht set")
def _remove_ebene_from_state(self, code):
doc = Rhino.RhinoDoc.ActiveDoc
@@ -2493,7 +2649,7 @@ class EbenenBridge(panel_base.BaseBridge):
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
_broadcast_state(doc)
except Exception as ex:
print("[EBENEN] remove:", ex)
print("[LAYERS] remove:", ex)
def _update_ebene_field(self, code, field, value):
doc = Rhino.RhinoDoc.ActiveDoc
@@ -2521,7 +2677,7 @@ class EbenenBridge(panel_base.BaseBridge):
doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False))
_broadcast_state(doc)
except Exception as ex:
print("[EBENEN] update:", ex)
print("[LAYERS] update:", ex)
# ---- Ebenen-Kombinationen / Presets (geteilt mit AUSSCHNITTE) --------
@@ -2559,7 +2715,7 @@ class EbenenBridge(panel_base.BaseBridge):
})
layers_out.sort(key=lambda x: x["fullPath"])
except Exception as ex:
print("[EBENEN] _send_combination layers:", ex)
print("[LAYERS] _send_combination layers:", ex)
try:
presets = self._load_presets(doc)
except Exception:
@@ -2593,7 +2749,7 @@ class EbenenBridge(panel_base.BaseBridge):
pe_states = None
pz_states = None
# --- Eye-State-Pfad (wenn vorhanden) ---
# --- Eye-State-Pfad (wenn present) ---
if pe_states is not None or pz_states is not None:
try:
e_raw = doc.Strings.GetValue("dossier_ebenen") or "[]"
@@ -2626,11 +2782,11 @@ class EbenenBridge(panel_base.BaseBridge):
})
try: doc.Views.Redraw()
except Exception: pass
print("[EBENEN] Eye-State-Preset angewandt: {} Ebenen, {} Zeichnungsebenen".format(
print("[LAYERS] Eye-State-Preset angewandt: {} Ebenen, {} Zeichnungsebenen".format(
len(pe_states or []), len(pz_states or [])))
return
except Exception as ex:
print("[EBENEN] _apply_combination eye-state:", ex)
print("[LAYERS] _apply_combination eye-state:", ex)
# Fall through zum Layer-ID-Pfad als Fallback
# --- Layer-ID-Pfad (alt / AUSSCHNITTE) ---
@@ -2720,12 +2876,12 @@ class EbenenBridge(panel_base.BaseBridge):
"ebenen": ebenen_list,
})
except Exception as ex:
print("[EBENEN] STATE_SYNC push:", ex)
print("[LAYERS] STATE_SYNC push:", ex)
except Exception as ex:
print("[EBENEN] _apply_combination sync:", ex)
print("[LAYERS] _apply_combination sync:", ex)
try: doc.Views.Redraw()
except Exception: pass
print("[EBENEN] Kombination angewandt: {} Layer".format(n))
print("[LAYERS] Kombination angewandt: {} Layer".format(n))
def _save_preset(self, name, layers):
name = (name or "").strip()
@@ -2749,7 +2905,7 @@ class EbenenBridge(panel_base.BaseBridge):
store_layer_presets(doc, presets)
_notify_oberleiste_combs()
_notify_layer_combinations_editor()
print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
print("[LAYERS] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean)))
def _save_current_as_preset(self, name):
"""Speichert die aktuellen Eye-States (dossier_ebenen + dossier_zeichnungs-
@@ -2947,11 +3103,11 @@ def _install_layer_listener(bridge):
if updated:
_broadcast_state(doc)
except Exception as ex:
print("[EBENEN] Layer-Event:", ex)
print("[LAYERS] Layer-Event:", ex)
Rhino.RhinoDoc.LayerTableEvent += on_layer_event
sc.sticky["ebenen_layer_listener"] = True
print("[EBENEN] Layer-Listener aktiv")
print("[LAYERS] Layer-Listener active")
panel_base.register_and_open("ebenen", "Ebenen", PANEL_GUID_STR, _ebenen_bridge_factory,
+6 -6
View File
@@ -5,7 +5,7 @@
"""
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
@@ -389,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)
@@ -414,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:
@@ -423,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()
@@ -515,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:
@@ -538,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
+3 -3
View File
@@ -664,7 +664,7 @@ def import_material(doc, item):
"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:
@@ -698,7 +698,7 @@ 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
@@ -840,7 +840,7 @@ def import_item(doc, item_id, at_point=None, layer2d=-1, layer3d=-1):
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)
+3 -3
View File
@@ -14,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
@@ -88,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
@@ -175,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"
+50 -50
View File
@@ -34,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
@@ -104,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
@@ -137,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())
@@ -154,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:
@@ -188,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
@@ -204,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
@@ -266,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
@@ -348,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
@@ -421,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:
@@ -451,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:
@@ -463,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
@@ -479,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
@@ -499,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):
@@ -572,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
@@ -599,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
@@ -612,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):
@@ -656,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"
@@ -675,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)
@@ -702,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):
@@ -723,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
@@ -733,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):
@@ -778,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
@@ -786,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)
@@ -796,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.
@@ -815,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 ""))
@@ -833,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
@@ -856,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.
@@ -875,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
@@ -884,12 +884,12 @@ 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)
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-
@@ -899,22 +899,22 @@ def _apply_scale(doc, vp, 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("[MASSSTAB] {} masstab-Raum/Raeume regenned".format(n_regen))
print("[SCALE] {} masstab-Raum/Raeume regenned".format(n_regen))
except Exception as ex:
print("[MASSSTAB] Raumstempel-Regen:", 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
@@ -924,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:
@@ -957,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
@@ -972,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):
@@ -1014,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()
@@ -1061,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():
+4 -4
View File
@@ -257,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:
@@ -676,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
@@ -865,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)")
+1 -1
View File
@@ -199,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:
+118 -60
View File
@@ -20,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 ------------------------------------------------
@@ -47,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))
@@ -98,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:
@@ -192,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
@@ -208,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)
@@ -267,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))
@@ -310,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()
@@ -341,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
@@ -353,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
@@ -411,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":
@@ -430,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
@@ -451,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
@@ -547,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
@@ -593,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
@@ -682,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")
@@ -699,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)
@@ -751,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
@@ -761,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
@@ -803,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
@@ -824,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
@@ -838,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:
@@ -857,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:
@@ -873,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)
@@ -900,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)
+10 -10
View File
@@ -100,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
@@ -133,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).
@@ -150,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
@@ -236,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 --------------------------------------------------------
@@ -291,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
@@ -370,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:
@@ -406,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)
+28 -28
View File
@@ -96,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:
@@ -127,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):
@@ -137,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
@@ -151,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
@@ -181,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))
@@ -199,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)
@@ -214,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
@@ -252,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"
@@ -276,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(
@@ -316,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))
@@ -365,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):
@@ -393,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
@@ -449,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
@@ -499,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
@@ -513,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
@@ -539,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
@@ -556,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:
@@ -577,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():
@@ -593,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):
@@ -609,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
@@ -642,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")
@@ -687,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,
+117 -32
View File
@@ -23,11 +23,13 @@ if _HERE not in sys.path:
# Nutzer waehrend Python-Imports + Panel-Registrierung nicht in eine schwarze
# Rhino-Oberflaeche schaut. Skipt automatisch wenn Launcher seinen eigenen
# Splash zeigt (Owner-Marker-Check).
try:
# 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 early:", _ex_splash)
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))
@@ -42,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",
}
@@ -105,7 +107,7 @@ def _assign_default_display_modes(doc):
try:
from Rhino.Display import DisplayModeDescription
except Exception as ex:
print("[STARTUP] view-modes: DMD nicht verfuegbar:", ex); return
print("[STARTUP] view-modes: DMD not available:", ex); return
# Mode-Lookup per Name
mode_plan = mode_3d = None
@@ -117,10 +119,10 @@ def _assign_default_display_modes(doc):
elif n == "Dossier 3D": mode_3d = dm
except Exception: pass
except Exception as ex:
print("[STARTUP] view-modes: Mode-List:", ex); return
print("[STARTUP] view-modes: mode list error:", ex); return
if mode_plan is None and mode_3d is None:
print("[STARTUP] view-modes: keine Dossier-Modes registriert — skip")
print("[STARTUP] view-modes: no Dossier display modes found — skip")
return
n_set = 0
@@ -148,7 +150,46 @@ def _assign_default_display_modes(doc):
try:
doc.Strings.SetString(_DOC_FLAG_VIEW_MODES, "1")
except Exception: pass
print("[STARTUP] view-modes: {} Viewport(s) gesetzt".format(n_set))
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"
@@ -160,7 +201,7 @@ def _check_doc_unit(doc):
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 gesetzt). Fuer erneute Frage: doc.Strings-Key loeschen.
bleibt set). Fuer erneute Frage: doc.Strings-Key loeschen.
"""
if doc is None: return
try:
@@ -168,11 +209,11 @@ def _check_doc_unit(doc):
return
except Exception: pass
try:
import rhinopanel
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 lesen:", ex)
print("[STARTUP] unit-check: project setting read error:", ex)
return
if target_unit_enum is None: return
try:
@@ -214,9 +255,9 @@ def _check_doc_unit(doc):
except Exception as ex:
print("[STARTUP] unit-convert RunScript:", ex)
else:
print("[STARTUP] User hat Unit-Umstellung verweigertDoc bleibt {}".format(current))
print("[STARTUP] User declined unit changedoc stays {}".format(current))
except Exception as ex:
print("[STARTUP] unit-check dialog:", ex)
print("[STARTUP] unit-check dialog error:", ex)
def _on_doc_opened(sender, e):
@@ -227,6 +268,7 @@ def _on_doc_opened(sender, e):
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)
@@ -239,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):
@@ -251,7 +293,7 @@ def _load_all(sender, e):
except Exception:
pass
# Splash wird ganz oben in startup.py (vor diesem Idle) gezeigt.
print("[STARTUP] Lade DOSSIER-Panels...")
print("[STARTUP] Loading DOSSIER panels...")
# Migration einmal fuer das beim Start aktive Doc
_migrate_active_doc()
# Und Listener fuer spaeter geoeffnete Docs registrieren
@@ -259,19 +301,23 @@ 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
@@ -285,7 +331,7 @@ def _load_all(sender, e):
_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:
@@ -300,7 +346,12 @@ def _load_all(sender, e):
_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:", 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()
@@ -308,7 +359,34 @@ def _load_all(sender, e):
_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:", 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-
@@ -335,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")
+206 -77
View File
@@ -50,11 +50,11 @@ def _sync_plot_color_to_display(attrs):
else:
attrs.PlotColorSource = _PLOT_FROM_LAYER
except Exception as ex:
print("[GESTALTUNG] sync plot-color:", ex)
print("[STYLES] sync plot-color:", ex)
_FILL_KEY = "ebenen_fill_hatch_id"
_FILL_SOURCE_KEY = "ebenen_fill_source" # "layer" oder "object"
_FILL_OWNER_KEY = "ebenen_fill_owner" # Curve-ID, auf Hatch gesetzt
_FILL_OWNER_KEY = "ebenen_fill_owner" # Curve-ID, auf Hatch set
_NO_FILL_KEY = "ebenen_no_fill" # "1" wenn User Fuellung explizit aus hat
# Loop-Guard fuer Live-Update
@@ -101,7 +101,7 @@ def _save_pending_hatch(curve_id, hatch_obj):
"timestamp": time.time(),
}
except Exception as ex:
print("[GESTALTUNG] save pending-hatch err:", ex)
print("[STYLES] save pending-hatch err:", ex)
return
m = sc.sticky.get("gestaltung_pending_hatch")
if not isinstance(m, dict):
@@ -130,7 +130,7 @@ def _restore_hatch_from_pending(doc, obj, meta):
new_hatches = rg.Hatch.Create(geom,
meta["pattern_idx"], meta["rotation"], meta["scale"], 0.0)
except Exception as ex:
print("[GESTALTUNG] restore Hatch.Create:", ex)
print("[STYLES] restore Hatch.Create:", ex)
return False
if not new_hatches or len(new_hatches) == 0: return False
new_attrs = Rhino.DocObjects.ObjectAttributes()
@@ -150,7 +150,7 @@ def _restore_hatch_from_pending(doc, obj, meta):
try:
hatch_id = doc.Objects.AddHatch(new_hatches[0], new_attrs)
except Exception as ex:
print("[GESTALTUNG] restore AddHatch:", ex)
print("[STYLES] restore AddHatch:", ex)
return False
if hatch_id == System.Guid.Empty: return False
try:
@@ -173,7 +173,7 @@ def _color_to_hex(c):
try:
return "#{:02x}{:02x}{:02x}".format(int(c.R), int(c.G), int(c.B))
except Exception as ex:
print("[GESTALTUNG] color-hex Fehler:", ex)
print("[STYLES] color-hex Fehler:", ex)
return None
@@ -189,9 +189,97 @@ def _hex_to_color(h):
return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
# Default-Pen ("Stift fuer neue Objekte"): wird in on_add auf frisch
# gezeichnete Kurven gestempelt, solange aktiv. Persistiert in Sticky und
# gilt bis der User wieder auf "Nach Ebene" zurueckstellt. Wird vom selben
# UI-Block gesetzt wie der Selektions-Pen — nur ohne Auswahl (siehe Setter).
_DEFAULT_PEN_KEY = "_dossier_default_pen"
def _default_pen():
p = sc.sticky.get(_DEFAULT_PEN_KEY)
if not isinstance(p, dict):
p = {}
return {
"colorSource": p.get("colorSource", "layer"),
"color": p.get("color"),
"lwSource": p.get("lwSource", "layer"),
"lw": p.get("lw"),
"linetypeSource": p.get("linetypeSource", "layer"),
"linetype": p.get("linetype"),
}
def _set_default_pen(**changes):
p = _default_pen()
p.update(changes)
sc.sticky[_DEFAULT_PEN_KEY] = p
def _default_pen_active(pen=None):
p = pen or _default_pen()
return (p["colorSource"] == "object" or p["lwSource"] == "object"
or p["linetypeSource"] == "object")
def _has_selection(doc):
return bool(list(doc.Objects.GetSelectedObjects(False, False)))
def _apply_pen_to_attrs(doc, a, pen):
"""Stempelt den aktiven Default-Pen auf eine Attribut-Kopie (in-place)."""
if pen["colorSource"] == "object":
a.ColorSource = _FROM_OBJECT
if pen["color"]:
a.ObjectColor = _hex_to_color(pen["color"])
_sync_plot_color_to_display(a)
if pen["lwSource"] == "object" and pen["lw"] is not None:
a.PlotWeightSource = _LW_FROM_OBJECT
try:
import massstab
massstab.write_plotweight(doc, a, float(pen["lw"]))
except Exception:
a.PlotWeight = float(pen["lw"])
if pen["linetypeSource"] == "object" and pen["linetype"]:
idx = -1
try: idx = doc.Linetypes.Find(pen["linetype"], True)
except Exception: idx = -1
if idx >= 0:
a.LinetypeSource = _LT_FROM_OBJECT
a.LinetypeIndex = idx
def _new_object_pen_summary(doc, pen):
"""PenBlock-kompatibles Objekt fuer den leeren Panel-Zustand: zeigt den
aktiven Default-Pen, "Nach Ebene"-Vorschau aus der aktuellen Ebene."""
cur = doc.Layers.CurrentLayer
layer_color = _color_to_hex(cur.Color)
try:
import massstab as _ms
layer_lw = round(_ms.read_plotweight(cur), 4)
except Exception:
layer_lw = round(cur.PlotWeight, 4)
layer_lt = _linetype_name(doc, cur.LinetypeIndex)
return {
"colorSource": pen["colorSource"],
"color": pen["color"] or layer_color,
"layerColor": layer_color,
"lwSource": pen["lwSource"],
"lw": pen["lw"] if pen["lw"] is not None else layer_lw,
"layerLw": layer_lw,
"linetypeSource": pen["linetypeSource"],
"linetype": pen["linetype"] or layer_lt,
"layerLinetype": layer_lt,
"linetypes": _all_linetypes(doc),
"layerName": _safe_layer_label(doc, cur, doc.Layers.CurrentLayerIndex),
"geometryKind": "curveOpen",
"active": _default_pen_active(pen),
}
def _force_load_linetypes(doc):
"""Rhinos Linetype-Tabelle wird lazy initialisiert — wir triggern es."""
# 1) Eingebaute Methode (falls vorhanden)
# 1) Eingebaute Methode (falls present)
for method_name in ("LoadDefaultLinetypes", "LoadDefaults", "LoadStandardLinetypes"):
try:
getattr(doc.Linetypes, method_name)()
@@ -291,17 +379,17 @@ def _ebene_fill_for_layer(doc, layer):
except Exception:
code = None
if not code:
print("[GESTALTUNG] _ebene_fill_for_layer: kein dossier_code auf Layer idx={}".format(
print("[STYLES] _ebene_fill_for_layer: kein dossier_code auf Layer idx={}".format(
getattr(layer, "LayerIndex", "?")))
return None
raw = doc.Strings.GetValue("dossier_ebenen")
if not raw:
print("[GESTALTUNG] _ebene_fill_for_layer: dossier_ebenen leer in doc.Strings")
print("[STYLES] _ebene_fill_for_layer: dossier_ebenen leer in doc.Strings")
return None
try:
ebenen = json.loads(raw)
except Exception as ex:
print("[GESTALTUNG] _ebene_fill_for_layer: json-Fehler:", ex)
print("[STYLES] _ebene_fill_for_layer: json-Fehler:", ex)
return None
if not isinstance(ebenen, list): return None
# Rekursiv durch Tree — Sub-Ebenen sind in children verschachtelt
@@ -320,7 +408,7 @@ def _ebene_fill_for_layer(doc, layer):
if True:
f = e.get("fill")
if not isinstance(f, dict):
print("[GESTALTUNG] _ebene_fill_for_layer: Ebene code={} hat KEIN fill-Feld".format(code))
print("[STYLES] _ebene_fill_for_layer: Ebene code={} has NO fill field".format(code))
return None
# lw: Strichstaerke der Hatch-Linien in mm. None = "wie Stift der Ebene"
# (ColorSource/PlotWeightSource bleibt auf FromLayer).
@@ -340,9 +428,9 @@ def _ebene_fill_for_layer(doc, layer):
"rotation": float(f.get("rotation", 0)) if f.get("rotation") is not None else 0.0,
"lw": lw_val,
}
print("[GESTALTUNG] _ebene_fill_for_layer code={} -> {}".format(code, result))
print("[STYLES] _ebene_fill_for_layer code={} -> {}".format(code, result))
return result
print("[GESTALTUNG] _ebene_fill_for_layer: code={} nicht in dossier_ebenen gefunden".format(code))
print("[STYLES] _ebene_fill_for_layer: code={} nicht in dossier_ebenen gefunden".format(code))
return None
@@ -400,7 +488,7 @@ def _apply_ebene_fill(doc, obj):
try:
hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0)
except Exception as ex:
print("[GESTALTUNG] Auto-Fill Hatch.Create:", ex)
print("[STYLES] Auto-Fill Hatch.Create:", ex)
return False
if not hatches or len(hatches) == 0: return False
@@ -429,7 +517,7 @@ def _apply_ebene_fill(doc, obj):
try:
hatch_id = doc.Objects.AddHatch(hatches[0], new_attrs)
except Exception as ex:
print("[GESTALTUNG] Auto-Fill AddHatch:", ex)
print("[STYLES] Auto-Fill AddHatch:", ex)
return False
if hatch_id == System.Guid.Empty: return False
@@ -440,7 +528,7 @@ def _apply_ebene_fill(doc, obj):
if h_obj is not None:
massstab.post_create_hatch_scale(doc, h_obj, float(fill["scale"]) or 1.0)
except Exception as ex:
print("[GESTALTUNG] post_create_hatch_scale (auto-fill):", ex)
print("[STYLES] post_create_hatch_scale (auto-fill):", ex)
try:
ca = obj.Attributes.Duplicate()
@@ -449,7 +537,7 @@ def _apply_ebene_fill(doc, obj):
try: doc.Objects.ModifyAttributes(obj, ca, True)
finally: _processing.discard(obj.Id)
except Exception as ex:
print("[GESTALTUNG] Auto-Fill UserString:", ex)
print("[STYLES] Auto-Fill UserString:", ex)
_link_curve_hatch(obj.Id, hatch_id)
return True
@@ -533,7 +621,7 @@ def refresh_layer_fills(doc):
targets.append((obj, owner))
owner_ids.add(str(owner.Id))
except Exception as ex:
print("[GESTALTUNG] refresh_layer_fills scan:", ex)
print("[STYLES] refresh_layer_fills scan:", ex)
return 0
updated = 0
@@ -602,9 +690,9 @@ def refresh_layer_fills(doc):
if h_obj is not None:
_ms.post_create_hatch_scale(doc, h_obj, scale_v)
except Exception as _ex:
print("[GESTALTUNG] post_create_hatch_scale (refresh):", _ex)
print("[STYLES] post_create_hatch_scale (refresh):", _ex)
except Exception as ex:
print("[GESTALTUNG] refresh rebuild:", ex)
print("[STYLES] refresh rebuild:", ex)
# (2) Farb-Sync — Hatch mit source=='layer' folgt der Ebenen-Definition
try:
@@ -641,7 +729,7 @@ def refresh_layer_fills(doc):
_processing.discard(refreshed.Id)
color_updated += 1
except Exception as ex:
print("[GESTALTUNG] refresh color-sync:", ex)
print("[STYLES] refresh color-sync:", ex)
# (3) Hatch-PlotWeight an fill.lw anpassen (None = wieder ByLayer)
try:
@@ -683,7 +771,7 @@ def refresh_layer_fills(doc):
finally:
_processing.discard(refreshed.Id)
except Exception as ex:
print("[GESTALTUNG] refresh lw-sync:", ex)
print("[STYLES] refresh lw-sync:", ex)
# --- 3) Auto-Fill nachziehen fuer Kurven ohne Hatch ---
added = 0
@@ -731,13 +819,13 @@ def refresh_layer_fills(doc):
if _apply_ebene_fill(doc, obj):
added += 1
except Exception as ex:
print("[GESTALTUNG] refresh auto-fill:", ex)
print("[STYLES] refresh auto-fill:", ex)
except Exception as ex:
print("[GESTALTUNG] refresh auto-fill scan:", ex)
print("[STYLES] refresh auto-fill scan:", ex)
if updated or color_updated or added:
doc.Views.Redraw()
print("[GESTALTUNG] refresh_layer_fills: pattern={}, farbe={}, neu={}, unveraendert={}".format(
print("[STYLES] refresh_layer_fills: pattern={}, farbe={}, neu={}, unchanged={}".format(
updated, color_updated, added, skipped))
return updated + color_updated + added
@@ -791,13 +879,13 @@ def repair_plot_colors(doc):
_processing.discard(obj.Id)
fixed += 1
except Exception as ex:
print("[GESTALTUNG] repair_plot_colors entry:", ex)
print("[STYLES] repair_plot_colors entry:", ex)
except Exception as ex:
print("[GESTALTUNG] repair_plot_colors scan:", ex)
print("[STYLES] repair_plot_colors scan:", ex)
return 0
if fixed:
doc.Views.Redraw()
print("[GESTALTUNG] repair_plot_colors: {} Objekte repariert (von {} mit Eigenfarbe gescannt)".format(fixed, scanned))
print("[STYLES] repair_plot_colors: {} Objekte repariert (von {} mit Eigenfarbe gescannt)".format(fixed, scanned))
return fixed
@@ -840,6 +928,7 @@ def _selection_summary(doc):
objs = list(doc.Objects.GetSelectedObjects(False, False))
base = {"count": 0, "linetypes": _all_linetypes(doc), "hatchPatterns": _all_hatch_patterns(doc)}
if not objs:
base["newObjectPen"] = _new_object_pen_summary(doc, _default_pen())
return base
color_sources, colors = set(), set()
@@ -910,7 +999,7 @@ def _selection_summary(doc):
# WICHTIG: layer.FullPath/Name liefert auf Mac mit Umlauten (Ä in WAENDE etc.)
# eine UnicodeDecodeError ueber die IronPython<->.NET-Bruecke. Wir benutzen
# stattdessen unsere ASCII-only UserStrings (dossier_id + dossier_code) die wir
# beim Layer-Bau gesetzt haben.
# beim Layer-Bau set haben.
nm = _safe_layer_label(doc, layer, a.LayerIndex)
layer_names.add(nm)
@@ -1137,7 +1226,7 @@ def _selection_summary(doc):
"fillRotation": single(fill_rots),
"hatchPatterns": _all_hatch_patterns(doc),
})
print("[GESTALTUNG] sel: n={} colorSrc={} color={} layerColor={}".format(
print("[STYLES] sel: n={} colorSrc={} color={} layerColor={}".format(
result.get("count"), result.get("colorSource"),
result.get("color"), result.get("layerColor")))
return result
@@ -1153,7 +1242,7 @@ class GestaltungBridge(panel_base.BaseBridge):
before = doc.Linetypes.Count
ok = _force_load_linetypes(doc)
after = doc.Linetypes.Count
print("[GESTALTUNG] Linetypes vor: {}, nach LoadDefaults({}): {}".format(before, ok, after))
print("[STYLES] Linetypes before: {}, nach LoadDefaults({}): {}".format(before, ok, after))
entries = []
for i in range(after):
lt = doc.Linetypes[i]
@@ -1163,15 +1252,15 @@ class GestaltungBridge(panel_base.BaseBridge):
try: nm = lt.Name
except Exception: nm = "?"
entries.append("[{}] {} ({})".format(i, nm, flags))
print("[GESTALTUNG] {}".format(" | ".join(entries)))
print("[STYLES] {}".format(" | ".join(entries)))
except Exception as ex:
print("[GESTALTUNG] Linetype-Diagnose:", ex)
print("[STYLES] Linetype-Diagnose:", ex)
# One-Shot Repair: aeltere Hatches (vor dem PlotColor-Fix angelegt)
# bekommen ihre Print-Attribute mit Display synchronisiert.
try:
repair_plot_colors(doc)
except Exception as ex:
print("[GESTALTUNG] repair on ready:", ex)
print("[STYLES] repair on ready:", ex)
self._send_selection()
def handle(self, data):
@@ -1222,7 +1311,7 @@ class GestaltungBridge(panel_base.BaseBridge):
try:
self.send("SELECTION", _selection_summary(doc))
except Exception as ex:
print("[GESTALTUNG] Selection:", ex)
print("[STYLES] Selection:", ex)
# ---- Attribute-Setter ------------------------------------------------
@@ -1238,6 +1327,12 @@ class GestaltungBridge(panel_base.BaseBridge):
self._send_selection()
def _set_color_source(self, source, color_hex):
doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(colorSource=source,
color=(color_hex if source == "object" else None))
self._send_selection()
return
col = _hex_to_color(color_hex) if (source == "object" and color_hex) else None
def m(a, _obj):
if source == "layer":
@@ -1255,11 +1350,16 @@ class GestaltungBridge(panel_base.BaseBridge):
# Print-Mode-aware: bei aktivem Print-View werden PlotWeights skaliert.
# write_plotweight() kuemmert sich um beides (Original-Speicherung +
# Skalierungs-Multiplier).
doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(lwSource=source,
lw=(lw if source == "object" else None))
self._send_selection()
return
try:
import massstab
except Exception:
massstab = None
doc = Rhino.RhinoDoc.ActiveDoc
def m(a, _obj):
if source == "layer":
a.PlotWeightSource = _LW_FROM_LAYER
@@ -1294,7 +1394,7 @@ class GestaltungBridge(panel_base.BaseBridge):
applied = True
break
except Exception as ex:
print("[GESTALTUNG] attr {} fehler: {}".format(prop, ex))
print("[STYLES] attr {} fehler: {}".format(prop, ex))
# Versuch 2: direkt auf RhinoObject
if not applied:
for prop in ("LinetypePatternLengthScale", "LinetypeScale"):
@@ -1304,18 +1404,23 @@ class GestaltungBridge(panel_base.BaseBridge):
applied = True
break
except Exception as ex:
print("[GESTALTUNG] obj {} fehler: {}".format(prop, ex))
print("[STYLES] obj {} fehler: {}".format(prop, ex))
if applied:
ok += 1
doc.Views.Redraw()
if ok == 0:
print("[GESTALTUNG] Linetype-Scale nicht unterstuetzt (Rhino-Version?)")
print("[STYLES] Linetype-Scale nicht unterstuetzt (Rhino-Version?)")
else:
print("[GESTALTUNG] Linetype-Scale auf {} Objekt(e) angewendet".format(ok))
print("[STYLES] Linetype-Scale auf {} Objekt(e) applied".format(ok))
self._send_selection()
def _set_linetype_source(self, source, name):
doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(linetypeSource=source,
linetype=(name if source == "object" else None))
self._send_selection()
return
idx = -1
if source == "object" and name:
try:
@@ -1387,7 +1492,7 @@ class GestaltungBridge(panel_base.BaseBridge):
try: doc.Objects.ModifyAttributes(obj, ca, True)
finally: _processing.discard(obj.Id)
except Exception as ex:
print("[GESTALTUNG] _set_fill follow-layer empty:", ex)
print("[STYLES] _set_fill follow-layer empty:", ex)
continue
else:
pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True)
@@ -1460,7 +1565,7 @@ class GestaltungBridge(panel_base.BaseBridge):
if h_obj is not None:
_ms2.post_create_hatch_scale(doc, h_obj, scale_v)
except Exception as _ex:
print("[GESTALTUNG] post_create_hatch_scale (replace):", _ex)
print("[STYLES] post_create_hatch_scale (replace):", _ex)
# Farbe / Source / FILL_SOURCE-Marker aktualisieren
refreshed = doc.Objects.FindId(existing_hatch.Id) or existing_hatch
ha = refreshed.Attributes.Duplicate()
@@ -1510,7 +1615,7 @@ class GestaltungBridge(panel_base.BaseBridge):
if h_obj is not None:
_ms.post_create_hatch_scale(doc, h_obj, scale_v)
except Exception as _ex:
print("[GESTALTUNG] post_create_hatch_scale (set_fill):", _ex)
print("[STYLES] post_create_hatch_scale (set_fill):", _ex)
else:
if existing_hatch is not None and not existing_hatch.IsDeleted:
_processing.add(existing_hatch.Id)
@@ -1545,7 +1650,7 @@ class GestaltungBridge(panel_base.BaseBridge):
doc = Rhino.RhinoDoc.ActiveDoc
objs = list(doc.Objects.GetSelectedObjects(False, False))
is_layer_source = (source == "layer")
print("[GESTALTUNG] _set_section_style: source={} enabled={} pattern={}".format(
print("[STYLES] _set_section_style: source={} enabled={} pattern={}".format(
source, enabled, pattern_name))
# SectionStyle-Klasse + Source-Enum holen.
@@ -1557,28 +1662,28 @@ class GestaltungBridge(panel_base.BaseBridge):
try:
SS = Rhino.DocObjects.SectionStyle
except Exception as ex:
print("[GESTALTUNG] SectionStyle-Klasse fehlt:", ex)
print("[STYLES] SectionStyle-Klasse fehlt:", ex)
return
SAS = None
for cls_name in ("ObjectSectionAttributesSource", "SectionAttributesSource"):
try:
SAS = getattr(Rhino.DocObjects, cls_name)
if SAS is not None:
print("[GESTALTUNG] Source-Enum: Rhino.DocObjects.{}".format(cls_name))
print("[STYLES] Source-Enum: Rhino.DocObjects.{}".format(cls_name))
break
except Exception: pass
if SAS is None:
print("[GESTALTUNG] WARNUNG: kein Source-Enum gefunden")
print("[STYLES] WARNUNG: kein Source-Enum gefunden")
if objs and not getattr(self, "_ss_api_logged", False):
o = objs[0]
for meth in ("SetCustomSectionStyle", "RemoveCustomSectionStyle",
"HasCustomSectionStyle", "GetCustomSectionStyle"):
print("[GESTALTUNG] RhinoObject.{}: {}".format(
print("[STYLES] RhinoObject.{}: {}".format(
meth, hasattr(o, meth)))
try:
a = o.Attributes
for meth in ("SetCustomSectionStyle", "RemoveCustomSectionStyle"):
print("[GESTALTUNG] Attributes.{}: {}".format(
print("[STYLES] Attributes.{}: {}".format(
meth, hasattr(a, meth)))
except Exception: pass
self._ss_api_logged = True
@@ -1616,13 +1721,13 @@ class GestaltungBridge(panel_base.BaseBridge):
try:
a.SectionAttributesSource = SAS.FromObject
except Exception as ex:
print("[GESTALTUNG] set Source.FromObject fail:", ex)
print("[STYLES] set Source.FromObject fail:", ex)
ok_modify = doc.Objects.ModifyAttributes(obj, a, True)
_log_post(obj, "Attributes.SetCustomSectionStyle+FromObject",
ok_modify)
return "Attributes.SetCustomSectionStyle"
except Exception as ex:
print("[GESTALTUNG] attr.SetCustomSectionStyle fail:", ex)
print("[STYLES] attr.SetCustomSectionStyle fail:", ex)
return None
def _log_post(obj, via, ok_modify=None):
@@ -1643,10 +1748,10 @@ class GestaltungBridge(panel_base.BaseBridge):
got = "HatchIndex={}".format(getattr(css, "HatchIndex", "?"))
except Exception as ex:
got = "get-err: {}".format(ex)
print("[GESTALTUNG] post via {} (modify_ok={}): Source={} Got={}".format(
print("[STYLES] post via {} (modify_ok={}): Source={} Got={}".format(
via, ok_modify, src, got))
except Exception as ex:
print("[GESTALTUNG] post-check:", ex)
print("[STYLES] post-check:", ex)
def _remove_custom(obj):
"""Entfernt Custom-SectionStyle + schaltet Source auf FromLayer
@@ -1659,11 +1764,11 @@ class GestaltungBridge(panel_base.BaseBridge):
try:
a.SectionAttributesSource = SAS.FromLayer
except Exception as ex:
print("[GESTALTUNG] set Source.FromLayer fail:", ex)
print("[STYLES] set Source.FromLayer fail:", ex)
doc.Objects.ModifyAttributes(obj, a, True)
return "Attributes.RemoveCustomSectionStyle+FromLayer"
except Exception as ex:
print("[GESTALTUNG] attr.RemoveCustomSectionStyle fail:", ex)
print("[STYLES] attr.RemoveCustomSectionStyle fail:", ex)
return None
n_ok = 0
@@ -1674,7 +1779,7 @@ class GestaltungBridge(panel_base.BaseBridge):
if is_layer_source:
# Custom entfernen → Layer-SectionStyle wird wirksam
via = _remove_custom(obj)
print("[GESTALTUNG] obj {}: remove custom via {}".format(
print("[STYLES] obj {}: remove custom via {}".format(
str(obj.Id)[:8], via))
if via: n_ok += 1
continue
@@ -1691,7 +1796,7 @@ class GestaltungBridge(panel_base.BaseBridge):
except Exception as ex:
obj_col = None
obj_col_src = "fail:{}".format(ex)
print("[GESTALTUNG] obj {} color src={} val={}".format(
print("[STYLES] obj {} color src={} val={}".format(
str(obj.Id)[:8], obj_col_src, obj_col))
# Per-Object: frischen SectionStyle bauen wie in layer_builder
style = SS()
@@ -1746,11 +1851,11 @@ class GestaltungBridge(panel_base.BaseBridge):
break
except Exception: pass
via = _apply_custom(obj, style)
print("[GESTALTUNG] obj {}: set custom via {} (hatch_idx={})".format(
print("[STYLES] obj {}: set custom via {} (hatch_idx={})".format(
str(obj.Id)[:8], via, pat_idx))
if via: n_ok += 1
print("[GESTALTUNG] SectionStyle auf {} Objekt(e) appliziert".format(n_ok))
print("[STYLES] SectionStyle auf {} Objekt(e) appliziert".format(n_ok))
doc.Views.Redraw()
self._send_selection()
@@ -1766,7 +1871,7 @@ def _install_selection_listener(bridge):
# Selection-Refresh wird via Idle-Event debounced:
# Rhino feuert pro Object-Select/Deselect einzeln. Bei mass-Delete von
# 327 Objekten = 327 refresh-Calls → 327 IPC-Sends in den WebView →
# UI haengt + Command-History wird mit '[GESTALTUNG] sel: n=N'
# UI haengt + Command-History wird mit '[STYLES] sel: n=N'
# zugemuellt. Wir setzen nur ein Dirty-Flag und feuern EINMAL beim
# naechsten Idle-Tick.
def refresh(*args):
@@ -1793,7 +1898,7 @@ def _install_selection_listener(bridge):
Rhino.RhinoApp.Idle += on_idle_flush
sc.sticky["_gestaltung_idle_attached"] = True
except Exception as ex:
print("[GESTALTUNG] Idle-Hook fail:", ex)
print("[STYLES] Idle-Hook fail:", ex)
def on_replace(sender, args):
"""Sync Curve↔Hatch bei Move/Replace:
@@ -1838,7 +1943,7 @@ def _install_selection_listener(bridge):
try:
new_curves = new_obj.Geometry.Get3dCurves(True)
except Exception as ex:
print("[GESTALTUNG] hatch.Get3dCurves:", ex)
print("[STYLES] hatch.Get3dCurves:", ex)
return
if not new_curves or len(new_curves) == 0:
return
@@ -1847,14 +1952,14 @@ def _install_selection_listener(bridge):
try:
doc2.Objects.Replace(owner_id, new_curve)
except Exception as ex:
print("[GESTALTUNG] hatch→curve replace:", ex)
print("[STYLES] hatch→curve replace:", ex)
finally:
_processing.discard(owner_id)
return
hatch_id_str = a.GetUserString(_FILL_KEY)
if not hatch_id_str:
return
print("[GESTALTUNG] on_replace fuer Curve mit Fill")
print("[STYLES] on_replace fuer Curve mit Fill")
try:
hatch_id = System.Guid(hatch_id_str)
except Exception:
@@ -1885,7 +1990,7 @@ def _install_selection_listener(bridge):
try:
doc.Objects.Replace(hatch_id, new_hatches[0])
except Exception as ex:
print("[GESTALTUNG] Hatch-Update:", ex)
print("[STYLES] Hatch-Update:", ex)
finally:
_processing.discard(hatch_id)
@@ -1927,7 +2032,7 @@ def _install_selection_listener(bridge):
hatch_id_str = _lookup_hatch_for_curve(obj.Id)
if not hatch_id_str:
return
print("[GESTALTUNG] on_delete: hatch via sticky map gefunden")
print("[STYLES] on_delete: hatch via sticky map gefunden")
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
if hatch_id_str:
@@ -1944,10 +2049,10 @@ def _install_selection_listener(bridge):
_processing.add(hatch_id)
try:
ok = doc.Objects.Delete(hatch_id, True)
print("[GESTALTUNG] Curve geloescht -> Hatch {} ({})".format(
print("[STYLES] Curve geloescht -> Hatch {} ({})".format(
"weg" if ok else "konnte nicht geloescht werden", hatch_id))
except Exception as ex:
print("[GESTALTUNG] Hatch-Loeschen:", ex)
print("[STYLES] Hatch-Loeschen:", ex)
finally:
_processing.discard(hatch_id)
_unlink_curve(obj.Id)
@@ -1971,7 +2076,7 @@ def _install_selection_listener(bridge):
finally:
_processing.discard(owner_id)
except Exception as ex:
print("[GESTALTUNG] Curve-Verweis aufraeumen:", ex)
print("[STYLES] Curve-Verweis aufraeumen:", ex)
def on_add(sender, args):
"""Auto-Fill bzw. Drag-Recovery: neues Objekt -> ggf. Hatch erzeugen.
@@ -1998,10 +2103,10 @@ def _install_selection_listener(bridge):
try:
ok = _restore_hatch_from_pending(doc, obj, pending)
except Exception as ex:
print("[GESTALTUNG] on_add restore Exception:", ex)
print("[STYLES] on_add restore Exception:", ex)
ok = False
if ok:
print("[GESTALTUNG] Drag-Recovery: Hatch wiederhergestellt fuer {}".format(obj.Id))
print("[STYLES] Drag-Recovery: Hatch wiederhergestellt fuer {}".format(obj.Id))
b = sc.sticky.get("gestaltung_bridge")
if b is not None:
try: b._send_selection()
@@ -2012,10 +2117,34 @@ def _install_selection_listener(bridge):
# Waehrend Move/Rotate werden Sub-Volumen erzeugt die kein Auto-Fill
# brauchen, und elemente uebernimmt die Coupling.
if sc.sticky.get("_dossier_user_transform_active"): return
# 2b) Default-Pen ("Stift fuer neue Objekte") auf frisch gezeichnete
# 2D-Objekte stempeln (Kurven, Text, Hatch, Bemassung ...), solange
# aktiv. Ausgeschlossen: 3D-Volumen (Pen ist ein 2D-Begriff) und
# DOSSIER-Element-Geometrie (dossier_element_type), die elemente.py setzt.
pen = _default_pen()
g = obj.Geometry
is_3d = isinstance(g, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD))
if _default_pen_active(pen) and g is not None and not is_3d:
try: dossier_type = obj.Attributes.GetUserString("dossier_element_type") or ""
except Exception: dossier_type = ""
if not dossier_type:
try:
a = obj.Attributes.Duplicate()
_apply_pen_to_attrs(doc, a, pen)
_processing.add(obj.Id)
try:
doc.Objects.ModifyAttributes(obj, a, True)
finally:
_processing.discard(obj.Id)
doc.Views.Redraw()
except Exception as ex:
print("[STYLES] on_add default-pen:", ex)
try:
ok = _apply_ebene_fill(doc, obj)
except Exception as ex:
print("[GESTALTUNG] on_add Exception:", ex)
print("[STYLES] on_add Exception:", ex)
return
if ok:
b = sc.sticky.get("gestaltung_bridge")
@@ -2060,7 +2189,7 @@ def _install_selection_listener(bridge):
finally:
_processing.discard(obj.Id)
except Exception as ex:
print("[GESTALTUNG] on_modify_attrs plot-sync:", ex)
print("[STYLES] on_modify_attrs plot-sync:", ex)
# --- (1) Layer-Wechsel -> Hatch mitziehen ---
if old_lyr == new_lyr:
@@ -2089,16 +2218,16 @@ def _install_selection_listener(bridge):
doc.Objects.ModifyAttributes(hatch_obj, ha, True)
finally:
_processing.discard(hatch_id)
print("[GESTALTUNG] Curve {} Layer geaendert -> Hatch mitgezogen".format(obj.Id))
print("[STYLES] Curve {} Layer geaendert -> Hatch mitgezogen".format(obj.Id))
except Exception as ex:
print("[GESTALTUNG] on_modify_attrs:", ex)
print("[STYLES] on_modify_attrs:", ex)
return
# Falls die neue Ebene andere Fill-Settings hat (Pattern/Skala/Drehung),
# die Hatch entsprechend an die neue Layer-Definition angleichen.
try:
refresh_layer_fills(doc)
except Exception as ex:
print("[GESTALTUNG] on_modify_attrs refresh:", ex)
print("[STYLES] on_modify_attrs refresh:", ex)
Rhino.RhinoDoc.SelectObjects += refresh
Rhino.RhinoDoc.DeselectObjects += refresh
@@ -2108,7 +2237,7 @@ def _install_selection_listener(bridge):
Rhino.RhinoDoc.AddRhinoObject += on_add
Rhino.RhinoDoc.ModifyObjectAttributes += on_modify_attrs
sc.sticky[flag] = True
print("[GESTALTUNG] Listener aktiv (Selection + Hatch-Live-Update + Ebene-Auto-Fill + Layer-Sync)")
print("[STYLES] Listener active (Selection + Hatch-Live-Update + Ebene-Auto-Fill + Layer-Sync)")
def _bridge_factory():
+4 -4
View File
@@ -709,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"]
@@ -899,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:
@@ -1027,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)
@@ -1137,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)
+6 -6
View File
@@ -64,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)
@@ -93,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):
@@ -204,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"):
@@ -274,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
@@ -573,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
+3 -3
View File
@@ -45,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)
+90 -84
View File
@@ -96,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
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:
pts[-1] = new_pt
new_poly = rg.Polyline(pts)
new_curve = rg.PolylineCurve(new_poly)
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
@@ -155,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:
@@ -175,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 +
@@ -204,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:
# Drag-Preview-Linien waehrend GetPoint aktiv ist
if self.drag_preview:
for line in self.drag_preview:
try:
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
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 --------------------------------------------------------
@@ -226,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
@@ -274,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
@@ -348,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()
@@ -0,0 +1,561 @@
<?xml version="1.0" encoding="utf-8"?>
<!--This file was generated by Rhinoceros, DO NOT modify this file-->
<RhinoUI major_ver="1" minor_ver="0" created_on_platform="Mac" created_on_rhino_version="8.31.26126.13432" name="DOSSIERUIV0.2">
<!--Window Layout display name for this file-->
<name>
<locale_1033>DOSSIERUIV0.2</locale_1033>
</name>
<!--Tab panel collection definitions and dock bar placement information-->
<dock_bars major_ver="1" minor_ver="0">
<dock_bar guid="035c14a5-6fa6-4e54-9769-5db41f770a3c">
<placement dock_location="Top" recent_dock_location="Top" dock_band_size="-1,62" dock_band_item_size="1,-1" docked_placement="0,0" float_point="610,52" float_size="200,200" visible="True" />
<tabs name="Oberleiste" selected_item="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" display_style="Text">
<name>
<locale_1033>Oberleiste</locale_1033>
</name>
<panel guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Oberleiste" />
</tabs>
</dock_bar>
<dock_bar guid="044781f9-dc80-4398-8498-e14c27c44423">
<placement dock_band_size="230,-1" dock_band_item_size="-1,0.5" docked_placement="0,1" float_point="1235,903" float_size="200,200" visible="True" />
<tabs name="Ebenen" selected_item="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" display_style="Bitmap">
<name>
<locale_1033>Ebenen</locale_1033>
</name>
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Zeichnungsebenen" />
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Ebenen" />
<panel guid="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Ausschnitte" />
<panel guid="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Layouts" />
</tabs>
</dock_bar>
<dock_bar guid="171011a9-a956-41ee-853e-3ccc0c0db1d8" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="ac3566f9-fe75-4258-9210-b1e9c05a5881">
<placement dock_location="Floating" recent_dock_location="Top" dock_band_size="36,48" dock_band_item_size="1,-1" docked_placement="0,0" float_point="317,336" float_size="1000,75" />
<tabs name="Standard Toolbars" selected_item="4bb9c817-d19f-45fd-8af2-39e9805f3e9f" display_style="BitmapAndText">
<name>
<locale_1033>Standard Toolbars</locale_1033>
<locale_1029>Standardní palety nástrojů</locale_1029>
<locale_1031>Standard-Werkzeugleisten</locale_1031>
<locale_1034>Barras de herramientas estándar</locale_1034>
<locale_1036>Barres d'outils Standard</locale_1036>
<locale_1040>Barre degli strumenti standard</locale_1040>
<locale_1041>標準ツールバー</locale_1041>
<locale_1042>표준 도구모음</locale_1042>
<locale_1045>Standardowe paski narzędzi</locale_1045>
<locale_2070>Barras de Ferramentas Standard</locale_2070>
<locale_2052>标准工具列</locale_2052>
<locale_1028>標準工具列</locale_1028>
<locale_1049>Стандартные панели инструментов</locale_1049>
</name>
<tool_bar guid="4bb9c817-d19f-45fd-8af2-39e9805f3e9f" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="32318c40-46e9-4aa3-8f73-09371ec27a4d" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="2ed87e2b-d225-4625-aeda-b11a115c9a14" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="720d3154-34fc-4177-8e52-9f417d4b5af3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="79d0d952-85af-4fe3-8444-46596bbe22fd" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="9772529f-ea7a-4e2f-9dab-5beff3ce96e8" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="0608110b-184c-443f-97ec-0612d9d2b605" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="9f21066b-eb0f-46f5-adeb-d62f80e04fd7" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="d767ebe9-eebd-4e75-a217-03f7431f71bf" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar_colletion="4b37b788-3a60-4c54-a4be-67e756115945" />
<tool_bar guid="b977d038-c9b6-4a9b-b097-9592a4117052" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="c8064867-a611-4b55-b747-fc2aee5d9bf3" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="4cd9a071-9337-4389-aa40-2a20f570da3b" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="625e34b2-3b19-4aba-91c2-94f79e2e1d91" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="44619cf6-b73a-46ea-93f8-46f1fa333115" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="12428ca8-b9f4-4954-8f6e-8a139226b383" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="a5379863-ee51-4b77-ab0c-44ee93c92ca3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="fa6d5bcc-8cd8-416b-8701-f89bd697e94d" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="d0a817a1-dea9-4e03-89e3-0d63d99b5e51" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="54dcdb3a-f098-49a4-9947-d5605d675be3" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="90fd89fc-e41f-49cc-bcf0-29e0d58017a1" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar_colletion="4b37b788-3a60-4c54-a4be-67e756115945" />
<tool_bar guid="16770b13-f7fb-4060-a6e6-607f90dd8bb3" file="c7da84fc-2991-4824-832a-4f2509bd0ede" side_bar="42b36785-e767-4c66-8704-4d828bcb0225" side_bar_file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="26b86ed9-12ff-4198-b49e-83bd3dfa7480" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="12428ca8-b9f4-4954-8f6e-8a139226b383">
<placement dock_location="Floating" recent_dock_location="Left" float_point="-1431655808,0" float_size="266,255" />
<tabs name="SubD Sidebar" selected_item="aaa213c9-f422-477a-9782-a6ae60104171" display_style="BitmapAndText">
<name>
<locale_1033>SubD Sidebar</locale_1033>
<locale_1029>Postranní panel SubD</locale_1029>
<locale_1031>SubD-Seitenleiste</locale_1031>
<locale_1034>SubD (lateral)</locale_1034>
<locale_1036>Volet SubD</locale_1036>
<locale_1040>Barra laterale SubD</locale_1040>
<locale_1041>SubDサイドバー</locale_1041>
<locale_1042>SubD 사이드바</locale_1042>
<locale_1045>SubD - pasek boczny</locale_1045>
<locale_2070>Barra Lateral SubD</locale_2070>
<locale_2052>细分边栏</locale_2052>
<locale_1028>SubD 邊欄</locale_1028>
<locale_1049>SubD - боковая</locale_1049>
</name>
<tool_bar guid="aaa213c9-f422-477a-9782-a6ae60104171" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="2e388a71-8d04-4d88-b545-9c714b2cc497">
<placement dock_location="Floating" float_point="952,182" float_size="400,800" />
<tabs name="Layers" selected_item="3610bf83-047d-4f7f-93fd-163ea305b493" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Layers</locale_1033>
</name>
<panel guid="3610bf83-047d-4f7f-93fd-163ea305b493" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Layers" />
</tabs>
</dock_bar>
<dock_bar guid="303293c9-aad0-4419-994d-6765718a58ed" unmanaged="True" text="Command">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="260,200" dock_band_item_size="-1,0.33333334" docked_placement="0,0" float_point="1084,409" float_size="200,200" visible="True" />
<tabs name="Container" selected_item="971fdb61-b9c6-4080-b38f-d5c72ce7a577" display_style="Bitmap">
<name>
<locale_1033>Container</locale_1033>
</name>
<tool_bar guid="971fdb61-b9c6-4080-b38f-d5c72ce7a577" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="3443edbb-66f4-460f-97b8-1008a2a31bc5">
<placement dock_location="Floating" float_point="1059,182" float_size="400,800" />
<tabs name="Display" selected_item="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Display</locale_1033>
</name>
<panel guid="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" name="Display" />
</tabs>
</dock_bar>
<dock_bar guid="39716459-9788-492d-adfd-8d89a7585363">
<placement dock_band_size="494,-1" dock_band_item_size="-1,1" docked_placement="0,0" float_point="2113,787" float_size="396,796" />
<tabs name="Display &amp; Rendering" selected_item="d9ac0269-811b-47d1-aa33-777986b13715" can_be_empty="True" display_style="Bitmap">
<name>
<locale_1033>Display &amp; Rendering</locale_1033>
<locale_1029>Zobrazení a renderování</locale_1029>
<locale_1031>Anzeige und Rendering</locale_1031>
<locale_1034>Visualización y renderizado</locale_1034>
<locale_1036>Affichage et rendu</locale_1036>
<locale_1040>Visualizzazione e rendering</locale_1040>
<locale_1041>表示 &amp; レンダリング</locale_1041>
<locale_1042>표시와 렌더링</locale_1042>
<locale_1045>Wyświetlanie i rendering</locale_1045>
<locale_2070>Visualização e Renderização</locale_2070>
<locale_2052>显示 &amp; 渲染</locale_2052>
<locale_1028>顯示 &amp; 彩現</locale_1028>
<locale_1049>Отображение и визуализация</locale_1049>
</name>
<panel guid="d9ac0269-811b-47d1-aa33-777986b13715" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Rendering" />
<panel guid="6df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Materials" />
</tabs>
</dock_bar>
<dock_bar guid="4711e685-da6b-40e7-8da8-725c8f104065" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="625e34b2-3b19-4aba-91c2-94f79e2e1d91">
<placement dock_location="Floating" recent_dock_location="Top" float_point="-1431655808,0" float_size="276,216" />
<tabs name="Solids Sidebar" selected_item="f1c8658b-c36e-4835-9492-14fce56924db" display_style="BitmapAndText">
<name>
<locale_1033>Solids Sidebar</locale_1033>
<locale_1029>Postranní panel Tělesa</locale_1029>
<locale_1031>Volumenkörper-Seitenleiste</locale_1031>
<locale_1034>Sólidos (lateral)</locale_1034>
<locale_1036>Volet Solides</locale_1036>
<locale_1040>Barra laterale Solidi</locale_1040>
<locale_1041>ソリッドサイドバー</locale_1041>
<locale_1042>솔리드 사이드바</locale_1042>
<locale_1045>Bryły - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Sólidos</locale_2070>
<locale_2052>实体边栏</locale_2052>
<locale_1028>實體邊欄</locale_1028>
<locale_1049>Тела - боковая</locale_1049>
</name>
<tool_bar guid="f1c8658b-c36e-4835-9492-14fce56924db" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="4794a6f6-4985-4003-937c-3e6498131d0c">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Elemente" selected_item="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" display_style="BitmapAndText">
<name>
<locale_1033>Elemente</locale_1033>
</name>
<panel guid="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Elemente" />
</tabs>
</dock_bar>
<dock_bar guid="4b37b788-3a60-4c54-a4be-67e756115945">
<placement dock_location="Floating" float_point="-1400,688" float_size="200,200" />
<tabs name="Curve drawing sidebar" selected_item="1990cfb7-2241-4dd1-b01d-735fe3be65fb" can_be_empty="True" display_style="BitmapAndText">
<name>
<locale_1033>Curve drawing sidebar</locale_1033>
<locale_1029>Postranní panel Křivka</locale_1029>
<locale_1031>Kurvenzeichnung-Seitenleiste</locale_1031>
<locale_1034>Dibujo de curvas (lateral)</locale_1034>
<locale_1036>Volet Dessin de courbes</locale_1036>
<locale_1040>Barra laterale disegno curve</locale_1040>
<locale_1041>曲線作成サイドバー</locale_1041>
<locale_1042>커브 그리기 사이드바</locale_1042>
<locale_1045>Rysowanie krzywych - pasek boczny</locale_1045>
<locale_2070>Barra lateral de desenhar curva</locale_2070>
<locale_2052>曲线绘制边栏</locale_2052>
<locale_1028>繪製曲線邊欄</locale_1028>
<locale_1049>Кривые - боковая</locale_1049>
</name>
<tool_bar guid="1990cfb7-2241-4dd1-b01d-735fe3be65fb" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="50eb6c7b-0be0-4166-ad17-ea694c97a6f4" text="Drag Strength">
<placement dock_location="Floating" dock_band_size="222,69" float_point="500,400" float_size="230,77" />
</dock_bar>
<dock_bar guid="5558eff8-695e-4524-9474-ae4a8c8ad07a">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Oberleiste" selected_item="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" display_style="BitmapAndText">
<name>
<locale_1033>Oberleiste</locale_1033>
</name>
<panel guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Oberleiste" />
</tabs>
</dock_bar>
<dock_bar guid="56e27247-204f-410f-879e-defcfd414a05">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Werkzeuge" selected_item="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" display_style="BitmapAndText">
<name>
<locale_1033>Werkzeuge</locale_1033>
</name>
<panel guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Werkzeuge" />
</tabs>
</dock_bar>
<dock_bar guid="5a5b89a0-830f-c90d-5f60-35c2e907e18c">
<placement dock_band_size="230,200" dock_band_item_size="-1,0.5" docked_placement="0,0" float_point="1612,998" float_size="400,800" visible="True" />
<tabs name="Right Container" selected_item="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" display_style="Bitmap">
<name>
<locale_1033>Right Container</locale_1033>
<locale_1029>Pravý kontejner</locale_1029>
<locale_1031>Rechter Container</locale_1031>
<locale_1034>Contenedor derecho</locale_1034>
<locale_1036>Conteneur droit</locale_1036>
<locale_1040>Contenitore destro</locale_1040>
<locale_1041>右コンテナ</locale_1041>
<locale_1042>오른쪽 컨테이너</locale_1042>
<locale_1045>Prawy zbiornik</locale_1045>
<locale_2070>Contentor Direito</locale_2070>
<locale_2052>右侧容器</locale_2052>
<locale_1028>右側容器</locale_1028>
<locale_1049>Правый контейнер</locale_1049>
</name>
<removed_item Item1="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" Item2="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" />
<removed_item Item1="3610bf83-047d-4f7f-93fd-163ea305b493" Item2="3610bf83-047d-4f7f-93fd-163ea305b493" />
<removed_item Item1="34ffb674-c504-49d9-9fcd-99cc811dcda2" Item2="34ffb674-c504-49d9-9fcd-99cc811dcda2" />
<removed_item Item1="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" Item2="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" />
<panel guid="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Dimensionen" />
</tabs>
</dock_bar>
<dock_bar guid="5bea5d56-1152-426e-9dcc-b542508bc7e6">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Gestaltung" selected_item="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" display_style="BitmapAndText">
<name>
<locale_1033>Gestaltung</locale_1033>
</name>
<panel guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Gestaltung" />
</tabs>
</dock_bar>
<dock_bar guid="6c5bbb6d-2e64-426b-b956-dc4a8d1e90eb">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Zeichnungsebenen" selected_item="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" display_style="BitmapAndText">
<name>
<locale_1033>Zeichnungsebenen</locale_1033>
</name>
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Zeichnungsebenen" />
</tabs>
</dock_bar>
<dock_bar guid="78ce0f5a-73ce-4268-b6c9-7d815cfc9f04" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="54dcdb3a-f098-49a4-9947-d5605d675be3">
<placement dock_location="Floating" recent_dock_location="Top" float_point="500,215" float_size="276,258" />
<tabs name="Render Sidebar" selected_item="ee7a5f30-1d7c-4a0f-9ebc-138d9c096aeb" display_style="BitmapAndText">
<name>
<locale_1033>Render Sidebar</locale_1033>
<locale_1029>Postranní panel Render</locale_1029>
<locale_1031>Render-Seitenleiste</locale_1031>
<locale_1034>Renderizado (lateral)</locale_1034>
<locale_1036>Volet Rendu</locale_1036>
<locale_1040>Barra laterale Rendering</locale_1040>
<locale_1041>レンダリングサイドバー</locale_1041>
<locale_1042>렌더링 사이드바</locale_1042>
<locale_1045>Rendering - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Renderizar</locale_2070>
<locale_2052>渲染边栏</locale_2052>
<locale_1028>彩現邊欄</locale_1028>
<locale_1049>Визуализация - боковая</locale_1049>
</name>
<tool_bar guid="ee7a5f30-1d7c-4a0f-9ebc-138d9c096aeb" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="78dafcee-5369-4c51-8325-ce2334941ad3">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Ebenen" selected_item="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" display_style="BitmapAndText">
<name>
<locale_1033>Ebenen</locale_1033>
</name>
<panel guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Ebenen" />
</tabs>
</dock_bar>
<dock_bar guid="84ddf9ff-a9d5-42a3-ac2e-6a28f2cc2f11">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Ausschnitte" selected_item="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" display_style="BitmapAndText">
<name>
<locale_1033>Ausschnitte</locale_1033>
</name>
<panel guid="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Ausschnitte" />
</tabs>
</dock_bar>
<dock_bar guid="87c7528a-a0a6-4245-837a-753237bfe57d">
<placement dock_location="Floating" float_point="869,182" float_size="400,800" />
<tabs name="Help 01" selected_item="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Help 01</locale_1033>
</name>
<panel guid="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Help" />
</tabs>
</dock_bar>
<dock_bar guid="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c">
<placement dock_band_size="351,-1" dock_band_item_size="-1,0.5" docked_placement="1,1" float_point="2179,1370" float_size="396,796" />
<tabs name="Named Views" selected_item="7df2a957-f12d-42ea-9fa6-95d7920c1b76" torn_off="True" display_style="Bitmap">
<name>
<locale_1033>Named Views</locale_1033>
<locale_1029>Pojmenované pohledy</locale_1029>
<locale_1031>Benannte Ansichten</locale_1031>
<locale_1034>Vistas guardadas</locale_1034>
<locale_1036>Vues nommées</locale_1036>
<locale_1040>Viste con nome</locale_1040>
<locale_1041>名前の付いたビュー</locale_1041>
<locale_1042>명명된 뷰</locale_1042>
<locale_1045>Nazwane widoki</locale_1045>
<locale_2070>Vistas Com Nome</locale_2070>
<locale_2052>已命名视图</locale_2052>
<locale_1028>已命名視圖</locale_1028>
<locale_1049>Именованные виды</locale_1049>
</name>
<panel guid="77d33034-194d-4cd5-957c-730d9a9eac50" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Named Views" />
<panel guid="7df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Environments" />
<panel guid="987b1930-ecde-4e62-8282-97ab4ad325fe" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Ground Plane" />
<panel guid="1012681e-d276-49d3-9cd9-7de92dc2404a" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Sun" />
<panel guid="8df2a957-f12d-42ea-9fa6-95d7920c1b76" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Textures" />
<panel guid="f4424a46-8281-430a-b03d-911dc9b40294" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Snapshots" />
<panel guid="86777b3d-3d68-4965-84f8-9e019c402433" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Lights" />
<panel guid="b70a4973-99ca-40c0-b2b2-f03417a5ff1d" plug_in="638a0098-0511-482b-95bf-8cf47fd32c17" name="Render Libraries" />
</tabs>
</dock_bar>
<dock_bar guid="950872d7-94f3-41cb-a58e-ad95d3b7bde9">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Dimensionen" selected_item="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" display_style="BitmapAndText">
<name>
<locale_1033>Dimensionen</locale_1033>
</name>
<panel guid="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Dimensionen" />
</tabs>
</dock_bar>
<dock_bar guid="96a58830-8ba7-4e11-aecc-86793727b1b5">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="260,-1" dock_band_item_size="-1,0.64855075" docked_placement="0,1" float_point="-115,931" float_size="200,200" visible="True" />
<tabs name="Gestaltung" selected_item="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" display_style="Bitmap">
<name>
<locale_1033>Gestaltung</locale_1033>
</name>
<panel guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Gestaltung" />
<panel guid="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Elemente" />
<panel guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Werkzeuge" />
</tabs>
</dock_bar>
<dock_bar guid="a4206be9-c9bf-4cbc-a80d-00369bbb5392" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="c8064867-a611-4b55-b747-fc2aee5d9bf3">
<placement dock_location="Floating" recent_dock_location="Top" float_point="-1431655808,0" float_size="276,258" />
<tabs name="Surface Sidebar" selected_item="f6534c6e-b451-4c7a-b8ec-8c3ff3596913" display_style="BitmapAndText">
<name>
<locale_1033>Surface Sidebar</locale_1033>
<locale_1029>Postranní panel Plocha</locale_1029>
<locale_1031>Flächen-Seitenleiste</locale_1031>
<locale_1034>Superficies (lateral)</locale_1034>
<locale_1036>Volet Surface</locale_1036>
<locale_1040>Barra laterale Superfici</locale_1040>
<locale_1041>サーフェスサイドバー</locale_1041>
<locale_1042>서피스 사이드바</locale_1042>
<locale_1045>Powierzchnia - pasek boczny</locale_1045>
<locale_2070>Superfícies</locale_2070>
<locale_2052>曲面边栏</locale_2052>
<locale_1028>曲面邊欄</locale_1028>
<locale_1049>Поверхности - боковая</locale_1049>
</name>
<tool_bar guid="f6534c6e-b451-4c7a-b8ec-8c3ff3596913" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd">
<placement dock_location="Left" recent_dock_location="Left" dock_band_size="260,16" dock_band_item_size="1,0.018115938" docked_placement="0,2" float_point="56,924" float_size="200,200" visible="True" />
<tabs name="Command History" selected_item="1d3d1785-2332-428b-a838-b2fe39ec50f4" display_style="Bitmap">
<name>
<locale_1033>Command History</locale_1033>
<locale_1042>명령 히스토리</locale_1042>
<locale_1041>コマンドヒストリ</locale_1041>
<locale_1049>История команд</locale_1049>
<locale_1031>Befehlsverlauf</locale_1031>
<locale_2052>指令历史</locale_2052>
<locale_1034>Historial de comandos</locale_1034>
<locale_1036>Historique des commandes</locale_1036>
<locale_1028>指令歷史</locale_1028>
<locale_1045>Historia poleceń</locale_1045>
<locale_1040>Storico comandi</locale_1040>
<locale_1029>Historie příkazů</locale_1029>
<locale_2070>Histórico de Comandos</locale_2070>
</name>
<panel guid="1d3d1785-2332-428b-a838-b2fe39ec50f4" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Command History" />
</tabs>
</dock_bar>
<dock_bar guid="be2f31c8-f79d-4ca9-953e-8ab3b8cd10e6">
<placement dock_location="Floating" float_point="910,176" float_size="92,116" />
<tabs name="Help" selected_item="2337f242-b576-41a4-aace-4a74772bc72e" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Help</locale_1033>
<locale_1042>도움말</locale_1042>
<locale_1041>ヘルプ</locale_1041>
<locale_1049>Справка</locale_1049>
<locale_1031>Hilfe</locale_1031>
<locale_2052>说明</locale_2052>
<locale_1034>Ayuda</locale_1034>
<locale_1036>Aide</locale_1036>
<locale_1028>說明</locale_1028>
<locale_1045>Pomoc</locale_1045>
<locale_1040>Aiuti</locale_1040>
<locale_1029>Nápověda</locale_1029>
<locale_2070>Ajuda</locale_2070>
</name>
<tool_bar guid="2337f242-b576-41a4-aace-4a74772bc72e" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="cdf21a5a-de32-48b1-8a6a-148a3564719a">
<placement dock_location="Floating" float_point="268,72" float_size="200,200" />
<tabs name="Layouts" selected_item="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" display_style="BitmapAndText">
<name>
<locale_1033>Layouts</locale_1033>
</name>
<panel guid="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" plug_in="814d908a-e25c-493d-97e9-ee3861957f49" name="Layouts" />
</tabs>
</dock_bar>
<dock_bar guid="d1225ee0-cf16-4f05-b053-60aa46183ac0" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="42b36785-e767-4c66-8704-4d828bcb0225">
<placement dock_location="Left" recent_dock_location="Left" float_point="-1431655808,0" float_size="276,300" />
<tabs name="Main" selected_item="971fdb61-b9c6-4080-b38f-d5c72ce7a577" display_style="Bitmap">
<name>
<locale_1033>Main</locale_1033>
<locale_1029>Hlavní</locale_1029>
<locale_1031>Haupt</locale_1031>
<locale_1034>Principal</locale_1034>
<locale_1036>Principale</locale_1036>
<locale_1040>Principale</locale_1040>
<locale_1041>メイン</locale_1041>
<locale_1042>메인</locale_1042>
<locale_1045>Główne</locale_1045>
<locale_2070>Principal</locale_2070>
<locale_2052>主要</locale_2052>
<locale_1028>主要</locale_1028>
<locale_1049>Главная</locale_1049>
</name>
<tool_bar guid="971fdb61-b9c6-4080-b38f-d5c72ce7a577" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
<dock_bar guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38">
<placement dock_location="Floating" recent_dock_location="Left" dock_band_size="126,22" dock_band_item_size="1,0.24908425" docked_placement="0,1" float_point="370,371" float_size="219,279" />
<tabs name="OSnap" selected_item="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" display_style="BitmapAndText">
<name>
<locale_1033>OSnap</locale_1033>
<locale_1029>Uchop</locale_1029>
<locale_1031>Ofang</locale_1031>
<locale_1034>RefObj</locale_1034>
<locale_1036>Accrochages</locale_1036>
<locale_1040>Osnap</locale_1040>
<locale_1041>OSnap</locale_1041>
<locale_1042>개체스냅</locale_1042>
<locale_1045>UchwytOb</locale_1045>
<locale_2070>OSnap</locale_2070>
<locale_2052>物件锁点</locale_2052>
<locale_1028>物件鎖點</locale_1028>
<locale_1049>Привязка</locale_1049>
</name>
<panel guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Osnap" />
<panel guid="918191ca-1105-43f9-a34a-dda4276883c1" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Selection Filters" />
</tabs>
</dock_bar>
<dock_bar guid="e6d78882-4e26-47c1-9e3e-4be7574dec59">
<placement dock_location="Floating" float_point="1065,182" float_size="400,800" />
<tabs name="Properties" selected_item="34ffb674-c504-49d9-9fcd-99cc811dcda2" torn_off="True" display_style="BitmapAndText">
<name>
<locale_1033>Properties</locale_1033>
</name>
<panel guid="34ffb674-c504-49d9-9fcd-99cc811dcda2" plug_in="a6f26df9-1ffd-4cf3-b1ec-a11a28a40892" name="Properties" />
</tabs>
</dock_bar>
<dock_bar guid="f8acd7e3-6464-4fe1-93ee-87c36d31880f" source_group_file="c7da84fc-2991-4824-832a-4f2509bd0ede" source_group="fa6d5bcc-8cd8-416b-8701-f89bd697e94d">
<placement dock_location="Floating" recent_dock_location="Top" float_point="128,256" float_size="234,174" />
<tabs name="Mesh Sidebar" selected_item="c3eab84e-9994-4c04-b2ef-aa4060f96168" display_style="BitmapAndText">
<name>
<locale_1033>Mesh Sidebar</locale_1033>
<locale_1029>Postranní panel Síť</locale_1029>
<locale_1031>Polygonnetz-Seitenleiste</locale_1031>
<locale_1034>Malla (lateral)</locale_1034>
<locale_1036>Volet Maillage</locale_1036>
<locale_1040>Barra laterale Mesh</locale_1040>
<locale_1041>メッシュサイドバー</locale_1041>
<locale_1042>메쉬 사이드바</locale_1042>
<locale_1045>Siatka - pasek boczny</locale_1045>
<locale_2070>Barra lateral de Malha</locale_2070>
<locale_2052>网格边栏</locale_2052>
<locale_1028>網格邊欄</locale_1028>
<locale_1049>Сети - боковая</locale_1049>
</name>
<tool_bar guid="c3eab84e-9994-4c04-b2ef-aa4060f96168" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="c4f7e3af-09ff-4844-b6dc-8f7a65e2f908" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="8f714f50-afd9-4e90-88a8-1432cdcfb431" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
<tool_bar guid="11b1e4f8-1e1d-4caa-9796-cee7fad3ee3b" file="c7da84fc-2991-4824-832a-4f2509bd0ede" />
</tabs>
</dock_bar>
</dock_bars>
<last_collection_panel_was_in>
<item guid="0dfcac10-303b-48a3-b541-88316b2a719c" dock_bar="71a6e2aa-d426-4fcf-aac6-0b5761762f1b" />
<item guid="0f8fb4f9-c213-4a6e-8e79-0bece02df82a" dock_bar="87c7528a-a0a6-4245-837a-753237bfe57d" />
<item guid="1012681e-d276-49d3-9cd9-7de92dc2404a" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="1d3d1785-2332-428b-a838-b2fe39ec50f4" dock_bar="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd" />
<item guid="1d55d702-028c-4aab-99cc-acfdd441fe5f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="34ffb674-c504-49d9-9fcd-99cc811dcda2" dock_bar="e6d78882-4e26-47c1-9e3e-4be7574dec59" />
<item guid="3610bf83-047d-4f7f-93fd-163ea305b493" dock_bar="2e388a71-8d04-4d88-b545-9c714b2cc497" />
<item guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" dock_bar="044781f9-dc80-4398-8498-e14c27c44423" />
<item guid="3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60719" dock_bar="044781f9-dc80-4398-8498-e14c27c44423" />
<item guid="3d1dfae0-8786-46a3-94dc-130c6a6e78bf" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="4a985c3a-ad29-4f8d-927f-6629dd8d355a" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" dock_bar="96a58830-8ba7-4e11-aecc-86793727b1b5" />
<item guid="4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" dock_bar="044781f9-dc80-4398-8498-e14c27c44423" />
<item guid="52606b82-9bc4-493a-b2b4-d2073d995529" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="562bda2a-184f-4b22-9607-79d992f28557" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" dock_bar="96a58830-8ba7-4e11-aecc-86793727b1b5" />
<item guid="5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" dock_bar="044781f9-dc80-4398-8498-e14c27c44423" />
<item guid="679af970-96d0-4c3a-831d-b4ff878e2884" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="6b6ffd64-c279-4b45-9959-e7e5a8eef806" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="6d9f5040-7e1f-4f2b-c4d5-f6071829304a" dock_bar="96a58830-8ba7-4e11-aecc-86793727b1b5" />
<item guid="6df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="39716459-9788-492d-adfd-8d89a7585363" />
<item guid="6df55e69-e102-4a72-b181-11664046c93f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="77d33034-194d-4cd5-957c-730d9a9eac50" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="7df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" dock_bar="035c14a5-6fa6-4e54-9769-5db41f770a3c" />
<item guid="86777b3d-3d68-4965-84f8-9e019c402433" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="8df2a957-f12d-42ea-9fa6-95d7920c1b76" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="8f23551a-a05b-4a03-a8d5-3e2fc55e4d8a" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="8fa84eff-1da5-4788-a983-e1ec3785e6a8" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="91799cf0-059a-46f8-854c-cc1c1419e29f" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="918191ca-1105-43f9-a34a-dda4276883c1" dock_bar="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" />
<item guid="987b1930-ecde-4e62-8282-97ab4ad325fe" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="9a0fa999-295d-4d77-b160-074fa2cd8e6d" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="a650e8dd-3896-43a8-9359-0e7ad8daf38e" dock_bar="5a5b89a0-830f-c90d-5f60-35c2e907e18c" />
<item guid="b68e9e9f-c79c-473c-a7ef-846a11dc4e7b" dock_bar="3443edbb-66f4-460f-97b8-1008a2a31bc5" />
<item guid="b70a4973-99ca-40c0-b2b2-f03417a5ff1d" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
<item guid="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" dock_bar="d3c4a392-88de-4c4f-88a4-ba5636ef7f38" />
<item guid="d9ac0269-811b-47d1-aa33-777986b13715" dock_bar="39716459-9788-492d-adfd-8d89a7585363" />
<item guid="f4424a46-8281-430a-b03d-911dc9b40294" dock_bar="8e0d2079-7bb7-4ba0-b6ed-f16b9a33e67c" />
</last_collection_panel_was_in>
<!--Visible items in each dock site-->
<dock_sites major_ver="1" minor_ver="0">
<dock_site location="Left" auto_hide="False">
<band size="260">
<dock_bar guid="303293c9-aad0-4419-994d-6765718a58ed" size="0.3333333432674408" />
<dock_bar guid="96a58830-8ba7-4e11-aecc-86793727b1b5" size="0.6485507488250732" />
<dock_bar guid="a6e1bdd1-20c0-4768-8fbf-49f8ca435efd" size="0.01811593770980835" />
</band>
</dock_site>
<dock_site location="Right" auto_hide="False">
<band size="230">
<dock_bar guid="5a5b89a0-830f-c90d-5f60-35c2e907e18c" size="0.5" />
<dock_bar guid="044781f9-dc80-4398-8498-e14c27c44423" size="0.5" />
</band>
</dock_site>
<dock_site location="Top" auto_hide="False">
<band size="62">
<dock_bar guid="035c14a5-6fa6-4e54-9769-5db41f770a3c" />
</band>
</dock_site>
<dock_site location="Bottom" auto_hide="False" />
</dock_sites>
</RhinoUI>
+1 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect, useMemo } from 'react'
import EbenenManager from './components/EbenenManager'
import EbenenManager from './components/LayerManager'
import {
applyAll, setActiveEbene,
onMessage, notifyReady, applyVisibility,
+178
View File
@@ -0,0 +1,178 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react'
import { notifyReady, send as bridgeSend, onMessage } from './lib/rhinoBridge'
import Icon from './components/Icon'
const LANGS = [
{ id: 'de', label: 'Deutsch' },
{ id: 'en', label: 'English' },
]
export default function DossierSettingsApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
const [lang, setLang] = useState(initial.lang || 'de')
const [launcherOk, setLauncherOk] = useState(initial.launcherOk ?? null)
const [needsRestart, setNeedsRestart] = useState(false)
useEffect(() => {
notifyReady()
onMessage('SETTINGS', (p) => {
if (p.lang) setLang(p.lang)
if (p.launcherOk != null) setLauncherOk(p.launcherOk)
})
}, [])
function handleLang(id) {
if (id === lang) return
setLang(id)
bridgeSend('SAVE_LANG', { lang: id })
setNeedsRestart(true)
}
const label = (de, en) => lang === 'en' ? en : de
return (
<div style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
background: 'var(--bg-base)',
color: 'var(--text-primary)',
fontFamily: 'var(--font)',
fontSize: 12,
overflow: 'hidden',
}}>
{/* Header */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '14px 16px 10px',
borderBottom: '1px solid var(--border-light)',
flexShrink: 0,
}}>
<span style={{ fontWeight: 600, fontSize: 13, letterSpacing: '0.01em' }}>
{label('Dossier-Einstellungen', 'Dossier Settings')}
</span>
{needsRestart && (
<span style={{
fontSize: 10,
color: 'var(--warn)',
display: 'flex',
alignItems: 'center',
gap: 4,
}}>
<Icon name="restart_alt" size={13} />
{label('Neustart erforderlich', 'Restart required')}
</span>
)}
</div>
{/* Body */}
<div style={{ flex: 1, overflowY: 'auto', padding: '16px' }}>
{/* Language */}
<Section label={label('Sprache', 'Language')} icon="language">
<div style={{ display: 'flex', gap: 6 }}>
{LANGS.map(l => (
<button
key={l.id}
onClick={() => handleLang(l.id)}
style={{
flex: 1,
padding: '7px 0',
borderRadius: 999,
border: lang === l.id
? '1.5px solid var(--accent)'
: '1px solid var(--border)',
background: lang === l.id ? 'var(--accent-dim)' : 'var(--bg-item)',
color: lang === l.id ? 'var(--accent)' : 'var(--text-secondary)',
fontFamily: 'var(--font)',
fontSize: 11,
fontWeight: lang === l.id ? 600 : 400,
cursor: 'pointer',
transition: 'all 120ms ease',
letterSpacing: '0.02em',
}}
>
{l.label}
</button>
))}
</div>
<p style={{ marginTop: 8, fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.4 }}>
{label('Gilt für alle Panels — Rhino neu laden um alle anzuwenden.', 'Applies to all panels — reload Rhino to apply everywhere.')}
</p>
</Section>
{/* Launcher status */}
<Section label={label('Launcher', 'Launcher')} icon="hub">
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 10px',
borderRadius: 8,
background: 'var(--bg-item)',
border: '1px solid var(--border-light)',
}}>
<div style={{
width: 7, height: 7, borderRadius: '50%', flexShrink: 0,
background: launcherOk === true
? 'var(--accent)'
: launcherOk === false
? 'var(--danger)'
: 'var(--text-muted)',
}} />
<span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>
{launcherOk === true
? label('Launcher verbunden', 'Launcher connected')
: launcherOk === false
? label('Launcher nicht gefunden', 'Launcher not found')
: label('Unbekannt', 'Unknown')}
</span>
</div>
</Section>
</div>
{/* Footer */}
<div style={{
padding: '10px 16px',
borderTop: '1px solid var(--border-light)',
display: 'flex',
justifyContent: 'flex-end',
flexShrink: 0,
}}>
<button
className="btn-outlined"
onClick={() => bridgeSend('CANCEL', {})}
>
{label('Schließen', 'Close')}
</button>
</div>
</div>
)
}
function Section({ label, icon, children }) {
return (
<div style={{ marginBottom: 20 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 6,
marginBottom: 10,
color: 'var(--text-muted)',
fontSize: 10,
fontWeight: 600,
letterSpacing: '0.08em',
textTransform: 'uppercase',
}}>
{icon && <Icon name={icon} size={13} />}
{label}
</div>
{children}
</div>
)
}
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect, useMemo } from 'react'
import GeschossManager from './components/GeschossManager'
import GeschossManager from './components/FloorManager'
import {
applyAll, setActiveZeichnungsebene,
onMessage, notifyReady, applyVisibility,
@@ -37,6 +37,7 @@ export default function ElementePropertiesApp() {
hatchPatterns={state.hatchPatterns}
fonts={state.fonts || []}
oeffStyles={state.oeffStyles || []}
wandStyles={state.wandStyles || []}
raumStempelStile={state.raumStempelStile || []}
stempelStile={state.stempelStile || []}
/>
+397 -103
View File
@@ -6,7 +6,7 @@ import { BarToggle, BarButton, BarCombo } from './components/BarControls'
import {
onMessage, notifyReady,
createWall, createDecke, createDach,
createFenster, createTuer, createAussparung, createTreppe,
createFenster, createTuer, createAussparung, createTreppe, setTreppe2DShow,
createStuetze, createTraeger, createRaum, createStempel,
openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
@@ -21,6 +21,31 @@ const labelXs = {
letterSpacing: '0.06em', textTransform: 'uppercase',
}
// Anzeige-Modus der Kacheln, geteilt mit Werkzeuge-Panel via localStorage.
const TILE_MODE_KEY = 'dossier_tile_mode' // 'both' | 'icon' | 'text'
function readTileMode() {
try { return localStorage.getItem(TILE_MODE_KEY) || 'both' } catch { return 'both' }
}
function writeTileMode(m) {
try { localStorage.setItem(TILE_MODE_KEY, m) } catch { /* WebView ohne Storage */ }
}
const ICON_GRID_MIN = 42 // min Spaltenbreite im reinen Symbol-Raster
function TileModeDropdown({ mode, onChange }) {
return (
<select
value={mode}
onChange={(e) => onChange(e.target.value)}
title="Anzeige: Symbol / Text / Symbol + Text"
style={{ fontSize: 10, padding: '3px 6px', maxWidth: 130 }}
>
<option value="icon">Symbol</option>
<option value="text">Text</option>
<option value="both">Symbol + Text</option>
</select>
)
}
function fmtNum(v) {
if (v == null || v === '') return ''
const n = Number(v)
@@ -49,9 +74,14 @@ function ReferenzSelector({ value, onChange }) {
)
}
// Pill-Button kompakt, Icon + Label horizontal, abgerundet
// Raster-Kachel Icon oben, Label darunter, einheitliche Zellgroesse.
// hasMenu zeigt ein kleines Chevron neben dem Label (Rechtsklick = Untertypen).
// mode: 'both' (Icon+Text) | 'icon' (nur Symbol) | 'text' (nur Text)
function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
hasMenu, badge }) {
hasMenu, badge, mode = 'both' }) {
const showIcon = mode !== 'text'
const showLabel = mode !== 'icon'
const isGrid = mode === 'icon' // reines Symbol = gleichmaessiges Raster
return (
<button
onClick={onClick}
@@ -59,18 +89,24 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
disabled={disabled}
title={hint}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '5px 10px 5px 8px',
display: 'flex', flexDirection: 'row',
alignItems: 'center',
justifyContent: isGrid ? 'center' : 'flex-start',
gap: showIcon && showLabel ? 7 : 0,
padding: isGrid ? '7px' : '5px 11px',
minHeight: isGrid ? 34 : 30,
background: 'var(--bg-input)',
border: '1px solid var(--border-light)',
borderRadius: 999,
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.4 : 1,
transition: 'background 0.1s, border-color 0.1s',
transition: 'background 0.12s, border-color 0.12s',
fontSize: 11, fontWeight: 500,
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
position: 'relative',
width: isGrid ? '100%' : 'auto',
whiteSpace: 'nowrap',
appearance: 'none', WebkitAppearance: 'none',
}}
onMouseEnter={(e) => { if (!disabled) {
e.currentTarget.style.background = 'var(--bg-item-hover)'
@@ -81,25 +117,43 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
e.currentTarget.style.borderColor = 'var(--border-light)'
}}
>
<Icon name={icon} size={14} style={{ color: 'var(--accent)' }} />
<span>{label}</span>
{showIcon && (
<Icon name={icon} size={isGrid ? 18 : 16}
style={{ color: 'var(--accent)', flexShrink: 0 }} />
)}
{showLabel && (
<span style={{
display: 'flex', alignItems: 'center', gap: 2,
overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{label}
{hasMenu && (
<Icon name="expand_more" size={12}
style={{ color: 'var(--text-muted)', marginLeft: -2 }} />
<Icon name="expand_more" size={11}
style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
)}
</span>
)}
{/* Im Icon-Modus signalisiert ein kleines Eck-Chevron das Dropdown */}
{hasMenu && !showLabel && (
<Icon name="expand_more" size={10}
style={{ position: 'absolute', bottom: 1, right: 3,
color: 'var(--text-muted)' }} />
)}
{badge && (
<span style={{
fontSize: 9, padding: '1px 5px', borderRadius: 8,
position: 'absolute', top: 3, right: 3,
fontSize: 8, padding: '0px 4px', borderRadius: 8,
background: 'var(--bg-section)', color: 'var(--text-muted)',
marginLeft: 2,
}}>{badge}</span>
)}
</button>
)
}
// Vertikale Kategorie-Gruppe mit Label + Pills, die wrappen
function PillGroup({ label, children }) {
// Kategorie-Gruppe: Symbol-Modus = Raster, sonst frei umbrechende Pills
function PillGroup({ label, mode = 'both', children }) {
const isGrid = mode === 'icon'
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<span style={{
@@ -109,7 +163,13 @@ function PillGroup({ label, children }) {
}}>
{label}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
<div style={isGrid ? {
display: 'grid',
gridTemplateColumns: `repeat(auto-fill, minmax(${ICON_GRID_MIN}px, 1fr))`,
gap: 5,
} : {
display: 'flex', flexWrap: 'wrap', gap: 5,
}}>
{children}
</div>
</div>
@@ -136,9 +196,15 @@ function PopupMenu({ items, onClose }) {
zIndex: 100,
minWidth: 140,
}}>
{items.map((it, i) => (
{items.map((it, i) => it._divider ? (
<div key={i} style={{
height: 1, margin: '4px 2px',
background: 'var(--border)', opacity: 0.6,
}} />
) : (
<button key={i}
onClick={(e) => { e.stopPropagation(); it.onClick(); onClose() }}
onClick={(e) => { e.stopPropagation(); it.onClick();
if (!it.keepOpen) onClose() }}
disabled={it.disabled}
title={it.hint || ''}
style={{
@@ -157,7 +223,10 @@ function PopupMenu({ items, onClose }) {
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
}}>
{it.icon && <Icon name={it.icon} size={12}
{it.checked !== undefined ? (
<Icon name={it.checked ? 'check_box' : 'check_box_outline_blank'}
size={12} style={{ color: 'var(--accent)' }} />
) : it.icon && <Icon name={it.icon} size={12}
style={{ color: 'var(--accent)' }} />}
<span style={{ flex: 1 }}>{it.label}</span>
{it.badge && (
@@ -331,10 +400,12 @@ function ElementListRow({ el, meta }) {
}
function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DShow }) {
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
const [tileMode, setTileMode] = useState(readTileMode)
const changeTileMode = (m) => { setTileMode(m); writeTileMode(m) }
const treppeWrapperRef = useRef(null)
const dis = noGeschoss
const baseHint = (label) =>
@@ -348,6 +419,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
const openStuetzeMenu = (e) => { e.preventDefault(); setStuetzeMenuOpen(true) }
const openTraegerMenu = (e) => { e.preventDefault(); setTraegerMenuOpen(true) }
const treppe2DOn = treppe2DShow !== false
const treppeItems = [
{ icon: 'stairs', label: 'Gerade Treppe',
hint: 'Lauflinie mit 2 Punkten',
@@ -358,6 +430,10 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
{ icon: 'rotate_right', label: 'Wendeltreppe',
hint: '3 Punkte: Mittelpunkt, Start-Lauflinie, End-Lauflinie',
onClick: () => createTreppe({ treppeArt: 'wendel' }) },
{ _divider: true },
{ checked: treppe2DOn, label: '2D-Plansymbol',
hint: 'Trittlinien + Auf-Pfeil auf Schnittebene zeichnen',
onClick: () => setTreppe2DShow(!treppe2DOn) },
]
const profilItems = (factory) => [
@@ -407,37 +483,41 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
)}
</div>
<PillGroup label="Konstruktion">
<PillButton icon="view_week" label="Wand"
<div style={{ display: 'flex', justifyContent: 'flex-start' }}>
<TileModeDropdown mode={tileMode} onChange={changeTileMode} />
</div>
<PillGroup label="Konstruktion" mode={tileMode}>
<PillButton mode={tileMode} icon="view_week" label="Wand"
hint={baseHint('Wand zeichnen')} disabled={dis}
onClick={() => createWall({ geschoss: '' })} />
<PillButton icon="layers" label="Decke"
<PillButton mode={tileMode} icon="layers" label="Decke"
hint={baseHint('Decke zeichnen')} disabled={dis}
onClick={() => createDecke({ geschoss: '' })} />
<PillButton icon="roofing" label="Dach"
<PillButton mode={tileMode} icon="roofing" label="Dach"
hint={baseHint('Pultdach zeichnen — Traufe = 1. Kante')} disabled={dis}
onClick={() => createDach({ geschoss: '' })} />
</PillGroup>
<PillGroup label="Öffnungen">
<PillButton icon="window" label="Fenster"
<PillGroup label="Öffnungen" mode={tileMode}>
<PillButton mode={tileMode} icon="window" label="Fenster"
hint={dis ? baseHint('Fenster') :
'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis}
onClick={() => createFenster({})} />
<PillButton icon="sensor_door" label="Tür"
<PillButton mode={tileMode} icon="sensor_door" label="Tür"
hint={dis ? baseHint('Tür') :
'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis}
onClick={() => createTuer({})} />
<PillButton icon="rectangle" label="Aussparung"
<PillButton mode={tileMode} icon="rectangle" label="Aussparung"
hint={dis ? baseHint('Aussparung') :
'Outline auf einer Decke zeichnen — wird automatisch ausgeschnitten'}
disabled={dis}
onClick={() => createAussparung({})} />
</PillGroup>
<PillGroup label="Erschliessung">
<PillGroup label="Erschliessung" mode={tileMode}>
<div ref={treppeWrapperRef} style={{ position: 'relative' }}>
<PillButton icon="stairs" label="Treppe" hasMenu
<PillButton mode={tileMode} icon="stairs" label="Treppe" hasMenu
hint={dis ? baseHint('Treppe') :
'Klick: gerade Treppe · Rechtsklick: Typ wählen'}
disabled={dis}
@@ -450,9 +530,9 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
</div>
</PillGroup>
<PillGroup label="Tragwerk">
<PillGroup label="Tragwerk" mode={tileMode}>
<div style={{ position: 'relative' }}>
<PillButton icon="square_foot" label="Stütze" hasMenu
<PillButton mode={tileMode} icon="square_foot" label="Stütze" hasMenu
hint={dis ? baseHint('Stütze') :
'Klick: Quadrat-Stütze · Rechtsklick: Profil wählen'}
disabled={dis}
@@ -464,7 +544,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
)}
</div>
<div style={{ position: 'relative' }}>
<PillButton icon="horizontal_rule" label="Träger" hasMenu
<PillButton mode={tileMode} icon="horizontal_rule" label="Träger" hasMenu
hint={dis ? baseHint('Träger') :
'Klick: Rechteck-Träger · Rechtsklick: Profil wählen'}
disabled={dis}
@@ -477,32 +557,32 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
</div>
</PillGroup>
<PillGroup label="Raeume">
<PillButton icon="crop_free" label="Raum"
<PillGroup label="Raeume" mode={tileMode}>
<PillButton mode={tileMode} icon="crop_free" label="Raum"
hint={dis ? baseHint('Raum') :
'Outline zeichnen · Stempel zeigt Name + Fläche'}
disabled={dis}
onClick={() => createRaum({})} />
<PillButton icon="receipt_long" label="Stempel"
<PillButton mode={tileMode} icon="receipt_long" label="Stempel"
hint={dis ? baseHint('Stempel') :
'SIA-Bilanz-Stempel platzieren · Default = aktives Geschoss · Properties: Total/Geschoss umstellen'}
disabled={dis}
onClick={() => createStempel({})} />
</PillGroup>
<PillGroup label="Library">
<PillButton icon="inventory_2" label="Symbol"
<PillGroup label="Library" mode={tileMode}>
<PillButton mode={tileMode} icon="inventory_2" label="Symbol"
hint={dis ? baseHint('Symbol') :
'Library-Picker oeffnen · Item waehlen · im Viewport Punkt klicken zum Platzieren'}
disabled={dis}
onClick={() => listLibrary()} />
</PillGroup>
<PillGroup label="Importer">
<PillButton icon="map" label="Swisstopo"
<PillGroup label="Importer" mode={tileMode}>
<PillButton mode={tileMode} icon="map" label="Swisstopo"
hint="Vollautomatischer Import via swisstopo STAC-API: Adresse suchen, Radius wählen, Gebäude + Terrain + Luftbild holen"
onClick={() => openSwisstopoDialog()} />
<PillButton icon="public" label="OSM"
<PillButton mode={tileMode} icon="public" label="OSM"
hint="OpenStreetMap-Daten via Overpass-API als 2D-Linien: Strassen, Gebäudeumrisse, Wasser, Grünflächen, Wege"
onClick={() => openOsmDialog()} />
</PillGroup>
@@ -513,12 +593,13 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
// PropertiesView: gemeinsame Komponente, rendert die passende Property-
// Form je nach Element-Typ. Wiederverwendbar in Inline + Satellite-Window.
export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles, fonts, raumStempelStile, stempelStile }) {
export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles, wandStyles, fonts, raumStempelStile, stempelStile }) {
if (!selected) return null
const upd = (p) => updateElement(selected.id, p)
const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) }
if (selected.kind === 'wand')
return <WallProperties wall={selected} geschosse={geschosse} materials={materials || []}
wandStyles={wandStyles || []}
onUpdate={upd} onDelete={del('Wand')} />
if (selected.kind === 'decke')
return <DeckenProperties decke={selected} geschosse={geschosse}
@@ -596,6 +677,7 @@ export default function ElementeApp() {
hatchPatterns={state.hatchPatterns}
fonts={state.fonts || []}
oeffStyles={state.oeffStyles || []}
wandStyles={state.wandStyles || []}
raumStempelStile={state.raumStempelStile || []}
stempelStile={state.stempelStile || []} />
</div>
@@ -604,6 +686,7 @@ export default function ElementeApp() {
noGeschoss={noGeschoss}
activeName={activeName}
elementsCount={elements.length}
treppe2DShow={state.treppe2DShow}
/>
</div>
</div>
@@ -1454,7 +1537,7 @@ function AussparungProperties({ aussp, onDelete }) {
)
}
function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
function WallProperties({ wall, geschosse, materials, wandStyles, onUpdate, onDelete }) {
const [dicke, setDicke] = useState(String(wall.dicke))
const [ukOver, setUkOver] = useState(wall.ukOverride)
const [okOver, setOkOver] = useState(wall.okOverride)
@@ -1466,6 +1549,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
const ukAuto = ukOver === '' || ukOver == null
const okAuto = okOver === '' || okOver == null
const styles = wandStyles || []
return (
<div style={{
@@ -1493,6 +1577,38 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
</select>
</div>
{styles.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Stil</span>
<select value={wall.styleId || ''}
onChange={(e) => onUpdate({ styleId: e.target.value })}
style={{ flex: 1, fontSize: 11 }}
title="Wand-Stil aus den Projekt-Einstellungen. Stil-Wechsel uebernimmt dicke aus dem Stil.">
<option value=""> kein Stil </option>
{styles.map(s => (
<option key={s.id} value={s.id}>
{s.name || s.id} ({(s.dicke || 0).toFixed(2)} m, prio {s.prio || 0})
</option>
))}
</select>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Wer verbindet sich an T-Stössen? Auto = Prio entscheidet. Durchgehend = diese Wand läuft durch. Anstossend = diese Wand stösst an die andere.">
Joint
</span>
<select value={wall.jointRolle || 'auto'}
onChange={(e) => onUpdate({ jointRolle: e.target.value })}
style={{ flex: 1, fontSize: 11 }}
title="Joint-Verhalten am T-Stoss. Auto folgt der Style-Prio, Durchgehend / Anstossend überschreiben.">
<option value="auto">Auto (Prio entscheidet)</option>
<option value="durchgehend">Durchgehend</option>
<option value="anstossend">Anstossend</option>
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Aufbau</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
@@ -2020,6 +2136,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
const [nStufen, setNStufen] = useState(String(treppe.nStufen ?? 15))
const [laufD, setLaufD] = useState(String(treppe.laufD ?? 0.18))
const [hStr, setHStr] = useState('')
const [ukStr, setUkStr] = useState('')
useEffect(() => {
setBreite(String(treppe.breite ?? 1.0))
setNStufen(String(treppe.nStufen ?? 15))
@@ -2034,9 +2151,27 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
const sa = 2 * S + A
const soll = treppe.soll || DEFAULT_TREPPE_SOLL
const hasHOver = treppe.hOver != null && treppe.hOver !== ''
const hasUkOver = treppe.ukOver != null && treppe.ukOver !== ''
useEffect(() => {
setHStr(hasHOver ? String(treppe.hOver) : fmtNum(H))
}, [treppe.id, treppe.hOver, H, hasHOver])
useEffect(() => {
setUkStr(hasUkOver ? String(treppe.ukOver) : '')
}, [treppe.id, treppe.ukOver, hasUkOver])
const onCommitUk = () => {
const trimmed = (ukStr || '').trim()
if (trimmed === '') {
if (hasUkOver) onUpdate({ ukOver: '' })
return
}
const v = parseFloat(trimmed)
if (Number.isNaN(v)) { setUkStr(hasUkOver ? String(treppe.ukOver) : ''); return }
if (Math.abs(v) < 1e-6) {
if (hasUkOver) onUpdate({ ukOver: '' })
} else if (Math.abs(v - (parseFloat(treppe.ukOver) || 0)) > 1e-5) {
onUpdate({ ukOver: v })
}
}
const allOK = (
(!soll.s[2] || (S >= soll.s[0] && S <= soll.s[1])) &&
@@ -2059,18 +2194,41 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
}
const ref = treppe.treppeReferenz ?? 'mid'
const REF_OPTIONS = [
{ code: 'links', label: 'Links' },
{ code: 'mid', label: 'Mittig' },
{ code: 'rechts', label: 'Rechts' },
// L-Treppen: nur Aussen-Lage erlaubt (sonst kollidieren die Laeufe am Eck)
const REF_OPTIONS_ALL = [
{ code: 'links', label: 'links' },
{ code: 'mid', label: 'mittig' },
{ code: 'rechts', label: 'rechts' },
]
const REF_OPTIONS = treppe.treppeArt === 'l'
? REF_OPTIONS_ALL.filter(o => o.code !== 'mid')
: REF_OPTIONS_ALL
const modus = treppe.treppeModus ?? 'flach'
const MODUS_OPTIONS = [
{ code: 'massiv', label: 'massiv', hint: 'Block bis zum Boden — wie eine Mauer unter der Treppe' },
{ code: 'flach', label: 'flach', hint: 'Schräge Plattenunterseite parallel zum Treppenlauf (realistisch)' },
{ code: 'flach', label: 'flach', hint: 'Schräge Plattenunterseite parallel zum Treppenlauf' },
{ code: 'plattenrand', label: 'gestuft', hint: 'Plattenunterseite folgt den Stufen, vertikal versetzt' },
]
// Konsistentes Grid: label(50) | control(1fr) | unit(14)
const rowStyle = {
display: 'grid',
gridTemplateColumns: '50px 1fr 14px',
alignItems: 'center', gap: 6,
}
const labelStyle = {
fontSize: 10, color: 'var(--text-secondary)',
}
const unitStyle = {
fontSize: 10, color: 'var(--text-muted)', textAlign: 'left',
}
const inputStyle = {
fontSize: 11, fontFamily: 'DM Mono, monospace', width: '100%',
}
const selectStyle = {
fontSize: 11, width: '100%', // Dropdowns nutzen System-Font (lesbar bei Worten)
}
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
@@ -2088,38 +2246,105 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Start</span>
<div style={rowStyle}>
<span style={labelStyle}>Start</span>
<select value={treppe.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
{geschosse.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
style={selectStyle}
title="Start-Geschoss — kann nicht hoeher als das Ziel-Geschoss sein">
{(() => {
// Ziel-Z bestimmen: aus Ziel-Geschoss oder aus hOver+startOkff+ukOver
let zielZ = null
if (treppe.geschossEnd) {
const g = geschosse.find(x => x.id === treppe.geschossEnd)
if (g) zielZ = Number(g.okff || 0)
} else if (treppe.hOver) {
const startG = geschosse.find(x => x.id === treppe.geschoss)
const startOkff = startG ? Number(startG.okff || 0) : 0
const ukO = Number(treppe.ukOver || 0)
zielZ = startOkff + ukO + Number(treppe.hOver)
}
return geschosse
.filter(g => zielZ === null || Number(g.okff || 0) < zielZ)
.map(g => <option key={g.id} value={g.id}>{g.name}</option>)
})()}
</select>
<span style={unitStyle}></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Ziel</span>
<div style={rowStyle}
title="Vertikaler Versatz des Treppen-Anfangs (relativ zum Geschoss-OKFF)">
<span style={labelStyle}>Versatz</span>
{hasUkOver ? (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={ukStr}
placeholder="0.00"
onChange={(e) => setUkStr(e.target.value)}
onBlur={onCommitUk}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
title="Versatz relativ zum Geschoss-OKFF"
style={{ ...inputStyle, flex: 1,
border: '1px solid var(--accent)' }} />
<button onClick={() => onUpdate({ ukOver: '' })}
title="Zurueck zu Geschoss-OKFF"
style={{ fontSize: 11, padding: '0 6px',
background: 'transparent', border: 'none',
color: 'var(--text-muted)', cursor: 'pointer' }}>×</button>
</div>
) : (
<select value=""
onChange={(e) => {
if (e.target.value === '__custom__') onUpdate({ ukOver: 0 })
}}
style={selectStyle}>
<option value="">(Geschoss-OKFF)</option>
<option value="__custom__">eigenes Z</option>
</select>
)}
<span style={unitStyle}>{hasUkOver ? 'm' : ''}</span>
</div>
<div style={rowStyle}>
<span style={labelStyle}>Ziel</span>
{hasHOver ? (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={hStr}
placeholder="1.50"
onChange={(e) => setHStr(e.target.value)}
onBlur={onCommitH}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
title="Treppen-Höhe (Delta Start → Ende)"
style={{ ...inputStyle, flex: 1,
border: '1px solid var(--accent)' }} />
<button onClick={() => onUpdate({ hOver: '' })}
title="Zurueck zu Geschoss-Verknuepfung"
style={{ fontSize: 11, padding: '0 6px',
background: 'transparent', border: 'none',
color: 'var(--text-muted)', cursor: 'pointer' }}>×</button>
</div>
) : (
<select
value={hasHOver ? '__custom__' : (treppe.geschossEnd || '')}
value={treppe.geschossEnd || ''}
onChange={(e) => {
const v = e.target.value
if (v === '__custom__') {
// Eigene Hoehe falls noch nicht gesetzt, mit aktuellem H starten
onUpdate({ hOver: H, geschossEnd: '' })
} else {
onUpdate({ geschossEnd: v, hOver: '' })
}
}}
style={{ flex: 1, fontSize: 11 }}>
style={selectStyle}>
<option value="">(auto: Start + Höhe)</option>
{geschosse.filter(g => g.id !== treppe.geschoss)
.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
<option value="__custom__">eigene Höhe</option>
<option value="__custom__">eigene Höhe</option>
</select>
)}
<span style={unitStyle}>{hasHOver ? 'm' : ''}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
<div style={rowStyle}>
<span style={labelStyle}>Breite</span>
<input type="text" value={breite}
onChange={(e) => setBreite(e.target.value)}
onBlur={() => {
@@ -2128,62 +2353,68 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
else setBreite(String(treppe.breite))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
style={inputStyle} />
<span style={unitStyle}>m</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Stufen</span>
<input type="text" value={nStufen}
onChange={(e) => setNStufen(e.target.value)}
onBlur={() => {
const v = parseInt(nStufen, 10)
if (Number.isFinite(v) && v >= 2 && v <= 40) onUpdate({ nStufen: v })
else setNStufen(String(treppe.nStufen))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>×</span>
<div style={rowStyle} title={treppe.lockS
? 'Mit Trittmaß-Lock: nur Anzahlen die ein S-Werte nahe der Sollhöhe ergeben'
: 'Anzahl Tritte (2-40)'}>
<span style={labelStyle}>Stufen</span>
<select value={treppe.nStufen}
onChange={(e) => onUpdate({ nStufen: parseInt(e.target.value, 10) })}
style={selectStyle}>
{(() => {
// Mit Lock: filtere die N-Werte deren resultierendes S nahe an
// target_S liegt (±10%). Sonst 2-40.
const range = []
for (let i = 2; i <= 40; i++) range.push(i)
if (treppe.lockS && treppe.targetS > 0.05 && H > 0.1) {
const tgt = Number(treppe.targetS)
const tol = tgt * 0.10
return range
.filter(n => Math.abs(H / n - tgt) <= tol)
.map(n => (
<option key={n} value={n}>
{n} (S={(H / n).toFixed(3)} m)
</option>
))
}
return range.map(n => <option key={n} value={n}>{n}</option>)
})()}
</select>
<span style={unitStyle}>×</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Lage</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<div style={rowStyle}>
<span style={labelStyle}>Lage</span>
<select value={ref}
onChange={(e) => onUpdate({ treppeReferenz: e.target.value })}
style={selectStyle}>
{REF_OPTIONS.map(o => (
<BarToggle key={o.code}
label={o.label}
active={ref === o.code}
onClick={() => onUpdate({ treppeReferenz: o.code })} />
<option key={o.code} value={o.code}>{o.label}</option>
))}
</div>
</select>
<span style={unitStyle}></span>
</div>
<div style={{ height: 1, background: 'var(--border-light)', margin: '2px 0' }} />
{/* Unterseite-Modus */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Form der Treppen-Unterseite">
Unten
</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<div style={rowStyle} title="Form der Treppen-Unterseite">
<span style={labelStyle}>Unten</span>
<select value={modus}
onChange={(e) => onUpdate({ treppeModus: e.target.value })}
style={selectStyle}>
{MODUS_OPTIONS.map(o => (
<BarToggle key={o.code}
label={o.label}
active={modus === o.code}
onClick={() => onUpdate({ treppeModus: o.code })}
title={o.hint} />
<option key={o.code} value={o.code} title={o.hint}>{o.label}</option>
))}
</div>
</select>
<span style={unitStyle}></span>
</div>
{/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */}
{modus !== 'massiv' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
Platte
</span>
<div style={rowStyle} title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
<span style={labelStyle}>Platte</span>
<input type="text" value={laufD}
onChange={(e) => setLaufD(e.target.value)}
onBlur={() => {
@@ -2192,11 +2423,65 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
else setLaufD(String(treppe.laufD))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
style={inputStyle} />
<span style={unitStyle}>m</span>
</div>
)}
{/* 2D-Plansymbol Bestandteile */}
<div style={{ height: 1, background: 'var(--border-light)', margin: '2px 0' }} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<span style={{ fontSize: 9, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
2D-Plansymbol
</span>
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer',
}}>
<input type="checkbox" checked={treppe.showLauflinie !== false}
onChange={(e) => onUpdate({ showLauflinie: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>Lauflinie</span>
</label>
{treppe.showLauflinie !== false && (
<div style={{
display: 'grid', gridTemplateColumns: '50px 1fr 14px',
alignItems: 'center', gap: 6, marginLeft: 18,
}}>
<span style={labelStyle}>Pfeil</span>
<select value={treppe.arrowStyle || 'klassisch'}
onChange={(e) => onUpdate({ arrowStyle: e.target.value })}
style={selectStyle}>
<option value="klassisch">klassisch</option>
<option value="filled">gefüllt</option>
<option value="breit">breit</option>
<option value="voll">voll (bis zu den Seiten)</option>
</select>
<span style={unitStyle}></span>
</div>
)}
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer',
}}>
<input type="checkbox" checked={treppe.showBruch !== false}
onChange={(e) => onUpdate({ showBruch: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>Bruchsymbol</span>
</label>
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 10, cursor: 'pointer',
marginLeft: 18, color: 'var(--text-secondary)',
}} title="Tritte und Außenlinie oberhalb der Schnittebene gestrichelt anzeigen">
<input type="checkbox" checked={treppe.obereDashed !== false}
onChange={(e) => onUpdate({ obereDashed: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>Obere Stufen gestrichelt</span>
</label>
</div>
{/* Schrittmass-Tabelle: H (editierbar), S, A, 2S+A mit on/off + range */}
<div style={{
display: 'flex', flexDirection: 'column', gap: 3,
@@ -2231,6 +2516,15 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
}}>auto</button>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}
title="Wenn an: Beim Aendern der Hoehe (oder Versatz/Ziel) wird die Anzahl Stufen automatisch nachgerechnet, damit S konstant bleibt.">
<input type="checkbox" checked={!!treppe.lockS}
onChange={(e) => onUpdate({ lockS: e.target.checked })}
style={{ accentColor: 'var(--accent)', width: 12, height: 12 }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
Trittmaß fixiert (S{treppe.targetS ? ' = ' + Number(treppe.targetS).toFixed(3) + ' m' : ''})
</span>
</div>
<SollRow label="S" value={S} unit="m" soll={soll} sollKey="s"
onUpdateSoll={onUpdateSoll} />
<SollRow label="A" value={A} unit="m" soll={soll} sollKey="a"
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react'
import GeschossDialog from './components/GeschossDialog'
import GeschossDialog from './components/FloorDialog'
import { notifyReady, send as bridgeSend } from './lib/rhinoBridge'
// recalcOkff direkt hier gleiche Logik wie in ZeichnungsebenenApp.jsx,
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react'
import GeschossSettingsDialog from './components/GeschossSettingsDialog'
import GeschossSettingsDialog from './components/FloorSettingsDialog'
import { notifyReady, send as bridgeSend } from './lib/rhinoBridge'
export default function GeschossSettingsApp() {
+1 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react'
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
import AusschnittLayerDialog from './components/ViewportLayerDialog'
import { onMessage, notifyReady } from './lib/rhinoBridge'
function send(type, payload) {
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useState, useRef } from 'react'
import EbenenSettingsDialog from './components/EbenenSettingsDialog'
import EbenenSettingsDialog from './components/LayerSettingsDialog'
import { notifyReady, onMessage, send as bridgeSend } from './lib/rhinoBridge'
export default function EbenenSettingsApp() {
+16 -18
View File
@@ -3,6 +3,7 @@
import { useEffect, useState, useRef } from 'react'
import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu'
import { t } from './i18n/index.js'
import { BarButton, BarCombo, BAR_H } from './components/BarControls'
import {
onMessage, notifyReady,
@@ -181,69 +182,66 @@ export default function LayoutsApp() {
// Kontextmenue-Items pro Layout
const layoutCtxItems = (l) => [
{ label: 'In Rhino oeffnen', icon: 'open_in_new',
{ label: t('layouts.open_in_rhino'), icon: 'open_in_new',
onClick: () => activateLayout(l.id) },
{ label: 'Umbenennen', icon: 'edit',
{ label: t('common.rename'), icon: 'edit',
onClick: () => setRenamingId(l.id) },
{ divider: true },
{ label: 'Als PDF exportieren', icon: 'picture_as_pdf',
{ label: t('layouts.export_pdf'), icon: 'picture_as_pdf',
onClick: () => exportPdf(l.id, 300) },
{ label: 'Papierformat ändern', icon: 'aspect_ratio',
{ label: t('layouts.change_paper'), icon: 'aspect_ratio',
onClick: () => openLayoutDialog('edit', {
id: l.id, name: l.name, width: l.widthMm, height: l.heightMm,
}) },
{ divider: true },
...(folders.length > 0 ? [
...folders.map(f => ({
label: `Verschieben → ${f}`,
label: t('layouts.move_to', { folder: f }),
icon: 'folder',
disabled: l.folder === f,
onClick: () => setLayoutFolder(l.id, f),
})),
{ label: 'Aus Ordner entfernen',
{ label: t('layouts.remove_from_folder'),
icon: 'folder_off',
disabled: !l.folder,
onClick: () => setLayoutFolder(l.id, '') },
{ divider: true },
] : []),
{ label: 'Löschen', icon: 'delete', danger: true,
{ label: t('common.delete'), icon: 'delete', danger: true,
onClick: () => {
if (window.confirm(`Layout "${l.name}" löschen?`)) deleteLayout(l.id)
if (window.confirm(`${t('common.delete')} "${l.name}"?`)) deleteLayout(l.id)
} },
]
// Kontextmenue-Items pro Ordner
const folderCtxItems = (folderName) => {
const items = grouped[folderName] || []
return [
{ label: collapsedFolders.has(folderName) ? 'Aufklappen' : 'Einklappen',
{ label: collapsedFolders.has(folderName) ? t('common.expand') : t('common.collapse'),
icon: collapsedFolders.has(folderName) ? 'expand_more' : 'expand_less',
onClick: () => toggleFolderCollapse(folderName) },
{ divider: true },
{ label: 'Alle ankreuzen / abwählen',
{ label: t('layouts.check_all'),
icon: 'check_box',
onClick: () => checkAllInFolder(items) },
{ label: `Ordner als PDF (${items.length})`,
{ label: t('layouts.folder_pdf', { count: items.length }),
icon: 'picture_as_pdf',
disabled: items.length === 0,
onClick: () => handleExportFolder(folderName) },
{ divider: true },
{ label: 'Ordner umbenennen',
{ label: t('common.rename_folder'),
icon: 'edit',
onClick: () => {
const next = window.prompt('Neuer Ordner-Name:', folderName)
const next = window.prompt(t('layouts.new_folder_name'), folderName)
if (next && next.trim() && next !== folderName) {
// Atomar via Server-Side waere besser; simpler: alle Layouts
// umhaengen + alten loeschen + neuen anlegen.
addLayoutFolder(next.trim())
items.forEach(l => setLayoutFolder(l.id, next.trim()))
removeLayoutFolder(folderName)
}
} },
{ label: 'Ordner loeschen',
{ label: t('common.delete_folder'),
icon: 'folder_off', danger: true,
onClick: () => {
if (window.confirm(`Ordner "${folderName}" loeschen? Layouts werden zur Wurzel verschoben.`))
if (window.confirm(`${t('common.delete_folder')} "${folderName}"? ${t('layouts.delete_folder_confirm')}`))
removeLayoutFolder(folderName)
} },
]
+35 -9
View File
@@ -493,16 +493,42 @@ function SectionBlock({ sel }) {
}
function EmptyState() {
// Leerer Zustand = "Stift fuer neue Objekte". Dieselben Pen-Controls wie bei
// einer Selektion (PenBlock), aber ohne Auswahl routet das Backend die Setter
// auf den Default-Pen, der dann auf frisch gezeichnete Kurven gestempelt wird.
function NewObjectPen({ pen }) {
const sel = pen || {
linetypes: [], geometryKind: 'curveOpen',
colorSource: 'layer', lwSource: 'layer', linetypeSource: 'layer',
}
return (
<div style={{
padding: '60px 24px', textAlign: 'center',
color: 'var(--text-muted)', fontSize: 11,
display: 'flex', flexDirection: 'column', gap: 14, alignItems: 'center',
}}>
<Icon name="touch_app" size={36} style={{ color: 'var(--text-muted)' }} />
<div>Waehle ein oder mehrere Objekte in Rhino aus.</div>
<>
<div style={{ padding: '12px 14px 4px', display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Icon name="draw" size={16} style={{ color: 'var(--accent)' }} />
<span style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-primary)' }}>
Stift für neue Objekte
</span>
{sel.active && (
<span style={{
fontSize: 9, fontWeight: 600, letterSpacing: '0.06em',
textTransform: 'uppercase', color: 'var(--accent)',
border: '1px solid var(--accent)', borderRadius: 999,
padding: '1px 7px',
}}>aktiv</span>
)}
</div>
<div style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.4 }}>
{sel.active
? 'Neu gezeichnete Linien & Kurven bekommen diesen Stift. Überall „Nach Ebene" = aus.'
: 'Farbe, Stärke oder Linientyp wählen — gilt für alle neu gezeichneten Objekte, bis du wieder auf „Nach Ebene" stellst.'}
</div>
</div>
<PenBlock sel={sel} />
<div style={{ padding: '2px 14px 14px', fontSize: 9, color: 'var(--text-muted)', fontStyle: 'italic' }}>
Oder wähle ein Objekt in Rhino, um dessen Stift zu ändern.
</div>
</>
)
}
@@ -538,7 +564,7 @@ export default function GestaltungApp() {
position: 'relative',
}}>
<div style={{ flex: 1, overflowY: 'auto' }}>
{empty ? <EmptyState /> : (
{empty ? <NewObjectPen pen={sel.newObjectPen} /> : (
<>
{showFill && (
<>
+4 -4
View File
@@ -14,7 +14,7 @@ import {
deleteLayerCombination, openLayerCombinationsDialog,
openDossierSettings, openKameraPanel,
setMasseActive, openMasseSettings,
openAbout, createText, setTextSettings,
openAbout, openCheatsheet, createText, setTextSettings,
applyTextStyle, saveTextStyle, deleteTextStyle,
setDarstellung,
arrangeSelection,
@@ -255,10 +255,10 @@ export default function OberleisteApp() {
overflowX: 'auto', overflowY: 'hidden',
flexShrink: 0,
}}>
{/* Logo: DOSSIER. + Version darunter (Klick = About-Fenster) */}
{/* Logo: DOSSIER. + Version darunter (Klick = Cheatsheet, Shift+Klick = About) */}
<button
onClick={() => openAbout()}
title="Über Dossier"
onClick={(e) => e.shiftKey ? openAbout() : openCheatsheet()}
title="Shortcuts (Shift+Klick = Über Dossier)"
style={{
display: 'flex', flexDirection: 'column',
alignItems: 'flex-start', gap: 0,
+65 -12
View File
@@ -1,9 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import Icon from './components/Icon'
import { notifyReady, runRhinoCommand } from './lib/rhinoBridge'
// Anzeige-Modus der Kacheln, geteilt mit Elemente-Panel via localStorage.
const TILE_MODE_KEY = 'dossier_tile_mode' // 'both' | 'icon' | 'text'
function readTileMode() {
try { return localStorage.getItem(TILE_MODE_KEY) || 'both' } catch { return 'both' }
}
function writeTileMode(m) {
try { localStorage.setItem(TILE_MODE_KEY, m) } catch { /* WebView ohne Storage */ }
}
const ICON_GRID_MIN = 42 // min Spaltenbreite im reinen Symbol-Raster
// Tool-Definitionen: [icon, label, rhino-command, tooltip]
// Material-Symbol-Namen siehe https://fonts.google.com/icons
const TOOLS = {
@@ -54,7 +64,10 @@ const TOOLS = {
// ---------------------------------------------------------------------------
function ToolPill({ icon, label, cmd, tip }) {
function ToolTile({ icon, label, cmd, tip, mode }) {
const showIcon = mode !== 'text'
const showLabel = mode !== 'icon'
const isGrid = mode === 'icon' // reines Symbol = gleichmaessiges Raster
return (
<button
onClick={() => runRhinoCommand(cmd)}
@@ -68,26 +81,40 @@ function ToolPill({ icon, label, cmd, tip }) {
e.currentTarget.style.background = 'var(--bg-input)'
}}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '5px 10px 5px 8px',
display: 'flex', flexDirection: 'row',
alignItems: 'center',
justifyContent: isGrid ? 'center' : 'flex-start',
gap: showIcon && showLabel ? 7 : 0,
padding: isGrid ? '7px' : '5px 11px',
minHeight: isGrid ? 34 : 30,
width: isGrid ? '100%' : 'auto',
background: 'var(--bg-input)',
border: '1px solid var(--border-light)',
borderRadius: 999,
cursor: 'pointer',
transition: 'background 0.1s, border-color 0.1s',
transition: 'background 0.12s, border-color 0.12s',
fontSize: 11, fontWeight: 500,
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
appearance: 'none', WebkitAppearance: 'none',
}}
>
<Icon name={icon} size={14} style={{ color: 'var(--accent)', flexShrink: 0 }} />
<span>{label}</span>
{showIcon && (
<Icon name={icon} size={isGrid ? 18 : 16}
style={{ color: 'var(--accent)', flexShrink: 0 }} />
)}
{showLabel && (
<span style={{
overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{label}</span>
)}
</button>
)
}
function PillGroup({ label, children }) {
function GridSection({ label, mode, children }) {
const isGrid = mode === 'icon'
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<span style={{
@@ -97,18 +124,41 @@ function PillGroup({ label, children }) {
}}>
{label}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
<div style={isGrid ? {
display: 'grid',
gridTemplateColumns: `repeat(auto-fill, minmax(${ICON_GRID_MIN}px, 1fr))`,
gap: 5,
} : {
display: 'flex', flexWrap: 'wrap', gap: 5,
}}>
{children}
</div>
</div>
)
}
function TileModeDropdown({ mode, onChange }) {
return (
<select
value={mode}
onChange={(e) => onChange(e.target.value)}
title="Anzeige: Symbol / Text / Symbol + Text"
style={{ fontSize: 10, padding: '3px 6px', maxWidth: 130 }}
>
<option value="icon">Symbol</option>
<option value="text">Text</option>
<option value="both">Symbol + Text</option>
</select>
)
}
// ---------------------------------------------------------------------------
export default function WerkzeugeApp() {
const [mode, setMode] = useState(readTileMode)
useEffect(() => { notifyReady() }, [])
const changeMode = (m) => { setMode(m); writeTileMode(m) }
const groups = Object.entries(TOOLS)
return (
@@ -121,12 +171,15 @@ export default function WerkzeugeApp() {
boxSizing: 'border-box',
overflowY: 'auto', overflowX: 'hidden',
}}>
<div style={{ display: 'flex', justifyContent: 'flex-start' }}>
<TileModeDropdown mode={mode} onChange={changeMode} />
</div>
{groups.map(([title, items]) => (
<PillGroup key={title} label={title}>
<GridSection key={title} label={title} mode={mode}>
{items.map(([icon, label, cmd, tip]) => (
<ToolPill key={cmd} icon={icon} label={label} cmd={cmd} tip={tip} />
<ToolTile key={cmd} icon={icon} label={label} cmd={cmd} tip={tip} mode={mode} />
))}
</PillGroup>
</GridSection>
))}
</div>
)
+12 -11
View File
@@ -3,6 +3,7 @@
import { useState, useEffect, useMemo } from 'react'
import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu'
import { t } from './i18n/index.js'
import {
onMessage, notifyReady,
listAusschnitte, saveAusschnitt, updateAusschnitt,
@@ -287,25 +288,25 @@ export default function AusschnitteApp() {
}
const handleAddFolder = () => {
const name = window.prompt('Name für neuen Ordner:')
const name = window.prompt(t('viewports.new_folder_name'))
if (name && name.trim()) addAusschnittFolder(name.trim())
}
const ctxItems = (id) => [
{ label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) },
{ label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
{ label: t('viewports.restore'), icon: 'restore', onClick: () => restoreAusschnitt(id) },
{ label: t('viewports.apply_to_detail'),icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
{ divider: true },
{ label: 'Ausschnittseinstellungen…', icon: 'tune', onClick: () => openAusschnittSettings(id) },
{ label: t('viewports.settings'), icon: 'tune', onClick: () => openAusschnittSettings(id) },
{ divider: true },
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) },
{ label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) },
{ label: t('common.duplicate'), icon: 'content_copy', onClick: () => duplicateAusschnitt(id) },
{ label: t('viewports.update'), icon: 'sync', onClick: () => updateAusschnitt(id) },
{ divider: true },
{ label: 'Löschen', icon: 'delete', danger: true, onClick: () => deleteAusschnitt(id) },
{ label: t('common.delete'), icon: 'delete', danger: true, onClick: () => deleteAusschnitt(id) },
]
const folderCtxItems = (folderName) => [
{ label: 'Ordner umbenennen', icon: 'edit', onClick: () => {
const newName = window.prompt('Neuer Ordnername:', folderName)
{ label: t('common.rename_folder'), icon: 'edit', onClick: () => {
const newName = window.prompt(t('common.new_folder_name'), folderName)
if (newName && newName.trim() && newName !== folderName) {
snaps.filter(s => s.folder === folderName).forEach(s => setAusschnittFolder(s.id, newName.trim()))
addAusschnittFolder(newName.trim())
@@ -313,8 +314,8 @@ export default function AusschnitteApp() {
}
}},
{ divider: true },
{ label: 'Ordner löschen', icon: 'folder_off', danger: true, onClick: () => {
if (window.confirm(`Ordner "${folderName}" löschen? Ausschnitte werden zur Wurzel verschoben.`)) {
{ label: t('common.delete_folder'), icon: 'folder_off', danger: true, onClick: () => {
if (window.confirm(`${t('common.delete_folder')} "${folderName}"? ${t('viewports.delete_folder_confirm')}`)) {
removeAusschnittFolder(folderName)
}
}},
+122
View File
@@ -0,0 +1,122 @@
.ctx-menu {
position: fixed;
padding: 5px;
min-width: 200px;
background: var(--bg-dialog);
border: 1px solid var(--border);
border-radius: 13px;
box-shadow:
0 2px 6px rgba(0,0,0,0.10),
0 8px 24px rgba(0,0,0,0.18),
0 0 0 0.5px var(--border);
z-index: 300;
animation: ctx-in 100ms cubic-bezier(0.2, 0, 0.13, 1) both;
}
@media (prefers-color-scheme: dark) {
.ctx-menu {
box-shadow:
0 2px 8px rgba(0,0,0,0.4),
0 12px 32px rgba(0,0,0,0.55),
0 0 0 0.5px var(--border);
}
}
@keyframes ctx-in {
from { opacity: 0; transform: scale(0.94) translateY(-5px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.ctx-title {
padding: 6px 12px 4px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ctx-divider {
height: 1px;
background: var(--border-light);
margin: 4px 2px;
}
.ctx-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 5px 12px;
font-size: 11.5px;
letter-spacing: 0.01em;
font-family: var(--font);
font-weight: 400;
color: var(--text-primary);
background: transparent;
border: none;
border-radius: 999px;
text-align: left;
cursor: pointer;
transition: background 100ms ease, color 100ms ease;
}
.ctx-item:hover:not(:disabled) {
background: var(--accent-dim);
color: var(--accent);
}
.ctx-item:hover:not(:disabled) .ctx-item__icon {
color: var(--accent);
}
.ctx-item:active:not(:disabled) {
background: var(--active-dim);
}
.ctx-item:disabled {
opacity: 0.38;
cursor: default;
}
.ctx-item--danger {
color: var(--danger);
}
.ctx-item--danger .ctx-item__icon {
color: var(--danger) !important;
}
.ctx-item--danger:hover:not(:disabled) {
color: var(--danger);
background: color-mix(in srgb, var(--danger) 10%, transparent);
}
.ctx-item__icon {
color: var(--text-secondary);
flex-shrink: 0;
transition: color 100ms ease;
}
.ctx-item__icon-gap {
width: 14px;
flex-shrink: 0;
}
.ctx-item__label {
flex: 1;
}
.ctx-item__shortcut {
font-size: 9px;
font-family: var(--font-mono);
color: var(--text-muted);
background: var(--bg-item);
border: 1px solid var(--border-light);
border-radius: 999px;
padding: 1px 6px;
letter-spacing: 0.04em;
}
+13 -34
View File
@@ -2,12 +2,12 @@
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useRef, useState } from 'react'
import Icon from './Icon'
import './ContextMenu.css'
export default function ContextMenu({ x, y, items, onClose }) {
export default function ContextMenu({ x, y, items, onClose, title }) {
const ref = useRef(null)
const [pos, setPos] = useState({ left: x, top: y })
// Falls Menue rechts/unten ueberlaufen wuerde, links/oben verschieben
useEffect(() => {
if (!ref.current) return
const rect = ref.current.getBoundingClientRect()
@@ -41,47 +41,26 @@ export default function ContextMenu({ x, y, items, onClose }) {
}, [onClose])
return (
<div
ref={ref}
style={{
position: 'fixed',
top: pos.top, left: pos.left,
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r)',
boxShadow: 'var(--shadow-3)',
padding: '4px 0',
minWidth: 180,
zIndex: 300,
}}
>
<div ref={ref} className="ctx-menu" style={{ top: pos.top, left: pos.left }}>
{title && <>
<div className="ctx-title">{title}</div>
<div className="ctx-divider" />
</>}
{items.map((it, i) => (
it.divider ? (
<div key={i} style={{ height: 1, background: 'var(--border-light)', margin: '4px 0' }} />
<div key={i} className="ctx-divider" />
) : (
<button
key={i}
disabled={it.disabled}
className={`ctx-item${it.danger ? ' ctx-item--danger' : ''}`}
onClick={() => { if (!it.disabled) { it.onClick(); onClose() } }}
onMouseEnter={(ev) => { if (!it.disabled) ev.currentTarget.style.background = 'var(--overlay-hover)' }}
onMouseLeave={(ev) => { ev.currentTarget.style.background = 'transparent' }}
style={{
width: '100%', textAlign: 'left',
padding: '6px 14px', fontSize: 11,
color: it.disabled ? 'var(--text-muted)'
: it.danger ? 'var(--danger)'
: 'var(--text-primary)',
display: 'flex', alignItems: 'center', gap: 10,
borderRadius: 0,
cursor: it.disabled ? 'default' : 'pointer',
background: 'transparent',
}}
>
{it.icon
? <Icon name={it.icon} size={14} style={{ color: it.disabled ? 'var(--text-muted)' : it.danger ? 'var(--danger)' : 'var(--text-secondary)' }} />
: <span style={{ width: 14 }} />}
<span style={{ flex: 1 }}>{it.label}</span>
{it.shortcut && <span style={{ fontSize: 9, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>{it.shortcut}</span>}
? <span className="ctx-item__icon"><Icon name={it.icon} size={14} /></span>
: <span className="ctx-item__icon-gap" />}
<span className="ctx-item__label">{it.label}</span>
{it.shortcut && <span className="ctx-item__shortcut">{it.shortcut}</span>}
</button>
)
))}
@@ -3,6 +3,7 @@
import { useState } from 'react'
import Icon from './Icon'
import ContextMenu from './ContextMenu'
import { t } from '../i18n/index.js'
import { BarCombo, BarButton } from './BarControls'
import { openGeschossSettings, openGeschossDialog, createSchnitt } from '../lib/rhinoBridge'
@@ -150,12 +151,12 @@ function ZeichnungsebeneRow({
)
}
const MODES = [
{ value: 'all_force', label: 'Alle anzeigen' },
{ value: 'all', label: 'Ausgewählte' },
{ value: 'active', label: 'Nur aktive' },
{ value: 'grey', label: 'Andere grau' },
{ value: 'grey_locked', label: 'Andere grau & gesperrt' },
const MODES = () => [
{ value: 'all_force', label: t('layers.show_all_mode') },
{ value: 'all', label: t('layers.selected_mode') },
{ value: 'active', label: t('layers.active_only_mode') },
{ value: 'grey', label: t('layers.others_gray') },
{ value: 'grey_locked', label: t('layers.others_gray_locked') },
]
export default function GeschossManager({
@@ -435,18 +436,19 @@ export default function GeschossManager({
const openContextMenu = (ev, id) => {
ev.preventDefault(); ev.stopPropagation()
setCtxMenu({ x: ev.clientX, y: ev.clientY, id })
const label = zeichnungsebenen.find(x => x.id === id)?.name || id
setCtxMenu({ x: ev.clientX, y: ev.clientY, id, label })
}
const ctxItems = (id) => {
const z = zeichnungsebenen.find(x => x.id === id)
if (!z) return []
return [
{ label: 'Einstellungen…', icon: 'settings', onClick: () => openGeschossSettings(z) },
{ label: t('common.settings') + '…', icon: 'settings', onClick: () => openGeschossSettings(z) },
{ divider: true },
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicate(id) },
{ label: t('common.duplicate'), icon: 'content_copy', onClick: () => duplicate(id) },
{ divider: true },
{ label: 'Löschen', icon: 'delete', danger: true,
{ label: t('common.delete'), icon: 'delete', danger: true,
disabled: zeichnungsebenen.length <= 1,
onClick: () => remove(id) },
]
@@ -460,22 +462,22 @@ export default function GeschossManager({
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Sichtbarkeit</span>
<span className="label-xs">{t('layers.visibility_mode')}</span>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
title={t('layers.visibility_mode')}
onGear={() => openGeschossDialog(zeichnungsebenen)}
gearIcon="settings"
gearTitle="Einstellungen"
gearTitle={t('common.settings')}
onSecond={openAddMenu}
secondIcon="add"
secondTitle="Hinzufuegen: Geschoss / Schnitt / Zeichnung"
secondTitle={t('floors.add')}
>
{MODES.map(m => (
{MODES().map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</BarCombo>
@@ -500,8 +502,7 @@ export default function GeschossManager({
if (mode === 'active' || mode === 'all_force') onModeChange('all')
}}
title={zeichnungsebenen.every(z => z.visible !== false)
? 'Alle Zeichnungsebenen ausblenden'
: 'Alle Zeichnungsebenen einblenden'}
? t('floors.hide_all') : t('floors.show_all')}
style={{ width: 16, height: 16,
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
>
@@ -518,8 +519,7 @@ export default function GeschossManager({
onChange(zeichnungsebenen.map(z => ({ ...z, locked: !anyLocked })))
}}
title={zeichnungsebenen.every(z => z.locked === true)
? 'Alle Zeichnungsebenen entsperren'
: 'Alle Zeichnungsebenen sperren'}
? t('floors.unlock_all') : t('floors.lock_all')}
style={{ width: 14, height: 14 }}
>
<Icon
@@ -557,6 +557,7 @@ export default function GeschossManager({
{ctxMenu && (
<ContextMenu
x={ctxMenu.x} y={ctxMenu.y}
title={ctxMenu.label}
items={ctxItems(ctxMenu.id)}
onClose={() => setCtxMenu(null)}
/>
@@ -2,19 +2,20 @@
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useRef, useMemo, useEffect } from 'react'
import Icon from './Icon'
import ConfirmDeleteEbene from './ConfirmDeleteEbene'
import ConfirmDeleteEbene from './ConfirmDeleteLayer'
import ContextMenu from './ContextMenu'
import { BarCombo, BarButton } from './BarControls'
import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings,
pickLayerCombination, saveLayerCombination, deleteLayerCombination,
openLayerCombinationsDialog } from '../lib/rhinoBridge'
import { t } from '../i18n/index.js'
const MODES = [
{ value: 'all_force', label: 'Alle anzeigen' },
{ value: 'all', label: 'Ausgewählte' },
{ value: 'active', label: 'Nur aktive' },
{ value: 'grey', label: 'Andere grau' },
{ value: 'grey_locked', label: 'Andere grau & gesperrt' },
const MODES = () => [
{ value: 'all_force', label: t('layers.show_all_mode') },
{ value: 'all', label: t('layers.selected_mode') },
{ value: 'active', label: t('layers.active_only_mode') },
{ value: 'grey', label: t('layers.others_gray') },
{ value: 'grey_locked', label: t('layers.others_gray_locked') },
]
const LW_PRESETS = [0.02, 0.10, 0.13, 0.18, 0.25, 0.35, 0.50, 0.70, 1.00]
@@ -265,20 +266,17 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
minHeight: 24,
}}
>
{/* Chevron sitzt visuell weiter rechts (marginLeft) marginRight
kompensiert das, damit die nachfolgenden Elemente (Auge, Code,
Farbe, Name) nicht mitrutschen. Spacer fuer kinderlose Zeilen
spiegelt dasselbe Offset, sonst springt die Eye-Spalte zwischen
Parent- und Leaf-Zeilen. */}
{/* Chevron-Slot 12w identisch zu GeschossManager-Spacer, damit
die Eye-Spalten beider Panels auf gleicher Position liegen. */}
{hasChildren ? (
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }}
title={expanded ? 'Einklappen' : 'Aufklappen'}
style={{ width: 12, height: 12, marginLeft: 6, marginRight: -6 }}
style={{ width: 12, height: 12, flexShrink: 0 }}
><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={11} /></button>
) : (
<span style={{ width: 12, flexShrink: 0, marginLeft: 6, marginRight: -6 }} />
<span style={{ width: 12, flexShrink: 0 }} />
)}
<button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
@@ -530,23 +528,24 @@ export default function EbenenManager({
const openContextMenu = (ev, code) => {
ev.preventDefault()
ev.stopPropagation()
setCtxMenu({ x: ev.clientX, y: ev.clientY, code })
const label = _findInTree(ebenen, code)?.name || code
setCtxMenu({ x: ev.clientX, y: ev.clientY, code, label })
}
const ctxItems = (code) => [
{ label: 'Ebeneneinstellungen…', icon: 'settings', onClick: () => {
{ label: t('layers.settings'), icon: 'settings', onClick: () => {
const target = _findInTree(ebenen, code)
if (target) openEbenenSettings(target, hatchPatterns)
} },
{ divider: true },
{ label: 'Sub-Ebene hinzufügen…', icon: 'add', onClick: () => addChild(code) },
{ label: 'Selektion hierher übertragen', icon: 'move_down', onClick: () => moveSelectionToEbene(code) },
{ label: t('layers.add_sub'), icon: 'add', onClick: () => addChild(code) },
{ label: t('layers.move_selection'), icon: 'move_down', onClick: () => moveSelectionToEbene(code) },
{ divider: true },
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateEbene(code) },
{ label: 'Eigenschaften kopieren', icon: 'colorize', onClick: () => copyProps(code) },
{ label: 'Eigenschaften einfügen', icon: 'format_paint', onClick: () => pasteProps(code), disabled: !clipboard },
{ label: t('common.duplicate'), icon: 'content_copy', onClick: () => duplicateEbene(code) },
{ label: t('layers.copy_props'), icon: 'colorize', onClick: () => copyProps(code) },
{ label: t('layers.paste_props'), icon: 'format_paint', onClick: () => pasteProps(code), disabled: !clipboard },
{ divider: true },
{ label: 'Löschen', icon: 'delete', onClick: () => handleDelete(code), danger: true,
{ label: t('common.delete'), icon: 'delete', onClick: () => handleDelete(code), danger: true,
disabled: ebenen.length <= 1 },
]
@@ -558,7 +557,7 @@ export default function EbenenManager({
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Ebenenkombination</span>
<span className="label-xs">{t('layers.combination')}</span>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
@@ -584,21 +583,21 @@ export default function EbenenManager({
pickLayerCombination(v === '__none__' ? null : v)
}}
title={activeKombi
? `Aktive Kombi: ${activeKombi}`
: 'Keine Kombination — manuelle Sichtbarkeit'}
? `${t('layers.combination')}: ${activeKombi}`
: t('layers.no_combination')}
onGear={openLayerCombinationsDialog}
gearTitle="Ebenenkombinationen bearbeiten"
gearTitle={t('layers.edit_combinations')}
>
<option value="__none__"> Eigene </option>
<option value="__none__">{t('layers.custom')}</option>
{layerCombinations.map(n => (
<option key={n} value={n}>{n}</option>
))}
<option disabled></option>
<option value="__save__">+ Aktuelle speichern</option>
<option value="__save__">{t('layers.save_current')}</option>
{activeKombi && (
<option value="__delete__">🗑 Aktuelle löschen</option>
<option value="__delete__">🗑 {t('layers.delete_current')}</option>
)}
<option value="__configure__">Bearbeiten</option>
<option value="__configure__">{t('common.edit')}</option>
</BarCombo>
</div>
</div>
@@ -609,19 +608,19 @@ export default function EbenenManager({
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Sichtbarkeit</span>
<span className="label-xs">{t('layers.visibility_mode')}</span>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
title={t('layers.visibility_mode')}
onSecond={addNew}
secondIcon="add"
secondTitle="Ebene hinzufügen"
secondTitle={t('layers.add')}
>
{MODES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
{MODES().map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
</BarCombo>
</div>
</div>
@@ -644,7 +643,7 @@ export default function EbenenManager({
if (mode === 'active' || mode === 'all_force') onModeChange('all')
}}
title={ebenen.every(e => e.visible !== false)
? 'Alle Ebenen ausblenden' : 'Alle Ebenen einblenden'}
? t('layers.hide_all') : t('layers.show_all')}
style={{ width: 16, height: 16,
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
>
@@ -660,7 +659,7 @@ export default function EbenenManager({
const anyLocked = ebenen.some(e => e.locked === true)
onChange(ebenen.map(e => ({ ...e, locked: !anyLocked })))
}}
title={ebenen.every(e => e.locked === true) ? 'Alle Ebenen entsperren' : 'Alle Ebenen sperren'}
title={ebenen.every(e => e.locked === true) ? t('layers.unlock_all') : t('layers.lock_all')}
style={{ width: 14, height: 14 }}
>
<Icon name={ebenen.every(e => e.locked === true) ? 'lock' : 'lock_open'} size={11} />
@@ -710,6 +709,7 @@ export default function EbenenManager({
{ctxMenu && (
<ContextMenu
x={ctxMenu.x} y={ctxMenu.y}
title={ctxMenu.label}
items={ctxItems(ctxMenu.code)}
onClose={() => setCtxMenu(null)}
/>

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