From 0b4b25cf4781839f979d268b2ea5a460a43b4345 Mon Sep 17 00:00:00 2001 From: karim Date: Wed, 20 May 2026 21:18:15 +0200 Subject: [PATCH] Mass-Style Preset (Raum-Rundung + Dim-Format) + Rundung als Dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend: - RaumProperties Rundung: 5er-Button-Reihe → Dropdown mit "Aus Mass-Style" als erstem Eintrag (leer = Default aus aktivem Preset uebernehmen) - Dach-Typ + Mansarde-Variante: text-only Button-Reihen → Dropdowns - Mass-Style-Section neu im DimensionenApp ganz oben: - Picker fuer aktives Preset - + (neu mit aktuellen Werten als Vorlage) / Loeschen - Inline-Editor: Name, Raum-Rundung, Mass-Dezimalstellen, Mass-Einheit Backend (rhino/mass_style.py — neu): - doc.Strings["dossier_mass_styles"]: JSON-Liste der Presets - doc.Strings["dossier_mass_style_active"]: aktive Preset-ID - list_presets/save_preset/delete_preset/get_active_id/set_active_id - Convenience: raum_rundung_default(doc), dim_dezimalstellen_default(doc) - Default-Presets bei erster Initialisierung: 1:50 / 1:100 / 1:500 elemente.py: - _read_meta: raum_rundung leer wenn UserString fehlt (vorher gezwungen "0.1") - _resolve_raum_rundung(meta, doc): per-Raum-Override > Mass-Style-Default - _make_raum_stamp_text + state-send nutzen Resolver - State sendet rundung (raw, kann "" sein) + rundungEffective + areaFmt damit React-Panel "Aus Mass-Style" anzeigen kann dimensionen.py: - Bridge-Endpoints MASS_STYLE_SET_ACTIVE / SAVE / DELETE - _broadcast_raum_regen: bei Preset-Wechsel alle Raeume queuen → Stempel- Flaechen kommen mit neuer Default-Rundung - _compute_state liefert massStyles + massStyleActive + signature update NOCH NICHT verdrahtet: Mass-Linien-Formatierung (dimDezimalstellen, dimEinheit) — Datenmodell ist da, Anwendung auf Rhino-Dimension-Renderer folgt in einem naechsten Schritt. Co-Authored-By: Claude Opus 4.7 --- rhino/dimensionen.py | 33 ++++++++ rhino/elemente.py | 31 +++++-- rhino/mass_style.py | 188 +++++++++++++++++++++++++++++++++++++++++ src/DimensionenApp.jsx | 149 ++++++++++++++++++++++++++++++++ src/ElementeApp.jsx | 74 ++++++---------- src/lib/rhinoBridge.js | 5 ++ 6 files changed, 426 insertions(+), 54 deletions(-) create mode 100644 rhino/mass_style.py diff --git a/rhino/dimensionen.py b/rhino/dimensionen.py index bf6b610..9fbb009 100644 --- a/rhino/dimensionen.py +++ b/rhino/dimensionen.py @@ -20,6 +20,7 @@ if _HERE not in sys.path: sys.path.insert(0, _HERE) import panel_base +import mass_style PANEL_GUID_STR = "9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" @@ -342,6 +343,34 @@ class DimensionenBridge(panel_base.BaseBridge): elif t == "SET_CIRCLE_RADIUS":self._set_circle_radius(p) elif t == "SET_LINE_LENGTH": self._set_line_length(p) elif t == "SET_RECTANGLE": self._set_rectangle(p) + elif t == "MASS_STYLE_SET_ACTIVE": + mass_style.set_active_id(Rhino.RhinoDoc.ActiveDoc, p.get("id")) + self._send_state(force=True) + self._broadcast_raum_regen() + elif t == "MASS_STYLE_SAVE": + mass_style.save_preset(Rhino.RhinoDoc.ActiveDoc, p.get("preset") or {}) + self._send_state(force=True) + self._broadcast_raum_regen() + elif t == "MASS_STYLE_DELETE": + mass_style.delete_preset(Rhino.RhinoDoc.ActiveDoc, p.get("id")) + self._send_state(force=True) + self._broadcast_raum_regen() + + def _broadcast_raum_regen(self): + """Beim Preset-Wechsel: alle Raeume regen damit die Stempel-Flaechen + in der neuen Default-Rundung erscheinen. Eingehaengt in elemente.""" + try: + import elemente + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + for obj in doc.Objects: + try: + m = elemente._read_meta(obj) + if m and m.get("type") == "raum_outline": + elemente._queue_regen(m["id"]) + except Exception: pass + except Exception as ex: + print("[DIMENSIONEN] mass_style raum-regen:", ex) # --- State-Snapshot ----------------------------------------------------- @@ -376,6 +405,8 @@ class DimensionenBridge(panel_base.BaseBridge): "refPoint": self._ref, "coordSystem": self._coord_sys, "planeName": "CPlane" if self._coord_sys == "cplane" else "Welt", + "massStyles": mass_style.list_presets(doc), + "massStyleActive": mass_style.get_active_id(doc), } shape = _detect_shape(objs) out["shape"] = shape @@ -414,6 +445,8 @@ class DimensionenBridge(panel_base.BaseBridge): tuple(sorted((state.get("position") or {}).items())), tuple(sorted((state.get("dimensions") or {}).items())), tuple(sorted((state.get("shape") or {}).items())) if state.get("shape") else None, + state.get("massStyleActive"), + len(state.get("massStyles") or []), ) if not force and sig == self._last_sig: return diff --git a/rhino/elemente.py b/rhino/elemente.py index c5c5f93..3cdf70f 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -21,6 +21,7 @@ if _HERE not in sys.path: sys.path.insert(0, _HERE) import panel_base +import mass_style PANEL_GUID_STR = "5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" @@ -96,6 +97,21 @@ _KEY_RAUM_SIA = "dossier_raum_sia" # "" | "hnf" | "nnf" | "vf" | "ff" _KEY_RAUM_FUELL = "dossier_raum_fuellung" # "" (keine) | "Solid" | Pattern-Name | "ByLayer" _RAUM_RUNDUNGEN = ("exakt", "0.01", "0.1", "0.5", "1") + + +def _resolve_raum_rundung(meta, doc=None): + """Loest die Raum-Rundung auf. Wenn am Raum eine explizite UserString- + Rundung gesetzt ist (raum_rundung != ""), gewinnt die. Sonst Default aus + dem aktiven Mass-Style. Doc-Default fallback "0.1" wenn nichts gesetzt.""" + explicit = (meta or {}).get("raum_rundung") or "" + if explicit in _RAUM_RUNDUNGEN: return explicit + if doc is None: doc = Rhino.RhinoDoc.ActiveDoc + try: + return mass_style.raum_rundung_default(doc) + except Exception: + return "0.1" + + _RAUM_ALIGN = ("links", "mid", "rechts") _RAUM_SIA_KINDS = ("", "hnf", "nnf", "vf", "ff") _RAUM_FUNKTIONEN = ( @@ -1958,8 +1974,9 @@ def _read_meta(obj): r_name = a.GetUserString(_KEY_RAUM_NAME) or "" r_num = a.GetUserString(_KEY_RAUM_NUMMER) or "" r_fkt = a.GetUserString(_KEY_RAUM_FUNKTION) or "" - r_rnd = a.GetUserString(_KEY_RAUM_RUNDUNG) or "0.1" - if r_rnd not in _RAUM_RUNDUNGEN: r_rnd = "0.1" + # Leer = "Default aus Mass-Style" — wird in _resolve_rundung() aufgeloest. + r_rnd = a.GetUserString(_KEY_RAUM_RUNDUNG) or "" + if r_rnd and r_rnd not in _RAUM_RUNDUNGEN: r_rnd = "" try: r_th = float(a.GetUserString(_KEY_RAUM_TXT_H) or "0.20") except Exception: r_th = 0.20 r_align = a.GetUserString(_KEY_RAUM_ALIGN) or "mid" @@ -4536,7 +4553,7 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name meta.get("raum_nummer", ""), meta.get("raum_funktion", ""), area, - meta.get("raum_rundung", "0.1"), + _resolve_raum_rundung(meta, doc), meta.get("raum_txt_h", 0.20), z=z_uk, align=meta.get("raum_align", "mid")) @@ -4813,19 +4830,21 @@ class ElementeBridge(panel_base.BaseBridge): area, perim, _ctr = _raum_amp(g_obj) except Exception: area, perim = 0.0, 0.0 - rnd = meta.get("raum_rundung", "0.1") + rnd_raw = meta.get("raum_rundung") or "" # "" = aus Mass-Style + rnd_eff = _resolve_raum_rundung(meta, doc) base.update({ "kind": "raum", "name": meta.get("raum_name", "Raum"), "nummer": meta.get("raum_nummer", ""), "funktion": meta.get("raum_funktion", ""), - "rundung": rnd, + "rundung": rnd_raw, + "rundungEffective": rnd_eff, "txtH": meta.get("raum_txt_h", 0.20), "align": meta.get("raum_align", "mid"), "sia": meta.get("raum_sia", ""), "fuellung": bool(meta.get("raum_fuellung", False)), "area": area, - "areaFmt": _format_area(area, rnd), + "areaFmt": _format_area(area, rnd_eff), "umfang": perim, }) elif meta["type"] in ("stuetze_point", "traeger_axis"): diff --git a/rhino/mass_style.py b/rhino/mass_style.py new file mode 100644 index 0000000..e2bf189 --- /dev/null +++ b/rhino/mass_style.py @@ -0,0 +1,188 @@ +#! python 3 +# -*- coding: utf-8 -*- +""" +mass_style.py +Globale Mass-Stil-Presets fuer Dossier — speichert pro Dokument benannte +Konfigurationen fuer: +- Raum-Flaechen-Rundung +- Mass-Linien-Dezimalstellen +- (erweiterbar) + +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. +""" +import json +import uuid +import Rhino + +_KEY_STYLES = "dossier_mass_styles" +_KEY_ACTIVE = "dossier_mass_style_active" + +_RAUM_RUNDUNGEN = ("exakt", "0.01", "0.1", "0.5", "1") +_DIM_DEZIMALSTELLEN = (0, 1, 2, 3, 4) +_DIM_EINHEITEN = ("m", "cm", "mm") + + +# --------------------------------------------------------------------------- +# Default-Presets — werden lazy beim ersten Lesen erzeugt wenn das Doc +# noch keine Mass-Styles kennt. Decken die gaengigen Massstaebe ab. + +_DEFAULT_PRESETS = [ + { + "name": "Plan 1:50", + "raumRundung": "0.1", + "dimDezimalstellen": 2, + "dimEinheit": "m", + "default": True, + }, + { + "name": "Plan 1:100", + "raumRundung": "0.5", + "dimDezimalstellen": 2, + "dimEinheit": "m", + "default": False, + }, + { + "name": "Übersicht 1:500", + "raumRundung": "1", + "dimDezimalstellen": 1, + "dimEinheit": "m", + "default": False, + }, +] + + +def _normalize(preset): + """Validiert + bereinigt einen Preset-Eintrag (Defaults setzen, + nicht-erlaubte Werte korrigieren).""" + if not isinstance(preset, dict): + preset = {} + rr = preset.get("raumRundung") or "0.1" + if rr not in _RAUM_RUNDUNGEN: rr = "0.1" + try: + dd = int(preset.get("dimDezimalstellen", 2)) + except (TypeError, ValueError): + dd = 2 + if dd not in _DIM_DEZIMALSTELLEN: dd = 2 + de = preset.get("dimEinheit") or "m" + if de not in _DIM_EINHEITEN: de = "m" + return { + "id": preset.get("id") or ("ms_" + uuid.uuid4().hex[:8]), + "name": preset.get("name") or "Unbenannt", + "raumRundung": rr, + "dimDezimalstellen": dd, + "dimEinheit": de, + } + + +def list_presets(doc): + if doc is None: return [] + try: + raw = doc.Strings.GetValue(_KEY_STYLES) + if not raw: + # Erst-Initialisierung: Default-Liste schreiben + items = [_normalize(p) for p in _DEFAULT_PRESETS] + _save_all(doc, items) + # Default-Aktiv setzen falls noch nichts gesetzt + if not doc.Strings.GetValue(_KEY_ACTIVE): + doc.Strings.SetString(_KEY_ACTIVE, items[0]["id"]) + return items + parsed = json.loads(raw) + if not isinstance(parsed, list): return [] + return [_normalize(p) for p in parsed] + except Exception as ex: + print("[MASS_STYLE] list:", ex) + return [] + + +def _save_all(doc, items): + try: + doc.Strings.SetString(_KEY_STYLES, json.dumps(items)) + except Exception as ex: + print("[MASS_STYLE] save:", ex) + + +def get_active_id(doc): + if doc is None: return None + try: + v = doc.Strings.GetValue(_KEY_ACTIVE) + return v or None + except Exception: + return None + + +def set_active_id(doc, preset_id): + if doc is None: return + items = list_presets(doc) + if preset_id and not any(p["id"] == preset_id for p in items): + return # Unbekannte ID — nicht setzen + try: + doc.Strings.SetString(_KEY_ACTIVE, preset_id or "") + except Exception as ex: + print("[MASS_STYLE] set active:", ex) + + +def get_active(doc): + """Liefert das aktive Preset (dict) oder None.""" + items = list_presets(doc) + aid = get_active_id(doc) + if aid: + for p in items: + if p["id"] == aid: return p + # Fallback: erstes Preset (oder None wenn leer) + return items[0] if items else None + + +def save_preset(doc, preset): + """Speichert/aktualisiert ein Preset. Wenn `id` im preset existiert + und in der Liste ist → Update, sonst Append. Returns die finale ID.""" + if doc is None: return None + items = list_presets(doc) + norm = _normalize(preset) + pid = preset.get("id") if isinstance(preset, dict) else None + if pid: + replaced = False + for i, p in enumerate(items): + if p["id"] == pid: + norm["id"] = pid + items[i] = norm + replaced = True + break + if not replaced: + items.append(norm) + else: + items.append(norm) + _save_all(doc, items) + return norm["id"] + + +def delete_preset(doc, preset_id): + if doc is None or not preset_id: return + items = [p for p in list_presets(doc) if p["id"] != preset_id] + _save_all(doc, items) + # Wenn aktives Preset geloescht: auf erstes uebriges umschalten + if get_active_id(doc) == preset_id: + set_active_id(doc, items[0]["id"] if items else "") + + +# --------------------------------------------------------------------------- +# Convenience: Defaults fuer Module die das Preset einlesen + +def raum_rundung_default(doc): + """Default-Rundung fuer Raum-Stempel wenn keine per-Raum-Override + gesetzt ist.""" + p = get_active(doc) + return p["raumRundung"] if p else "0.1" + + +def dim_dezimalstellen_default(doc): + p = get_active(doc) + return p["dimDezimalstellen"] if p else 2 + + +def dim_einheit_default(doc): + p = get_active(doc) + return p["dimEinheit"] if p else "m" diff --git a/src/DimensionenApp.jsx b/src/DimensionenApp.jsx index f4e9290..465f5e3 100644 --- a/src/DimensionenApp.jsx +++ b/src/DimensionenApp.jsx @@ -5,6 +5,7 @@ import { setRefPoint, setCoordSystem, setDimPosition, setDimDimension, setDimRotationZ, setCircleRadius, setLineLength, setRectangleDims, + setMassStyleActive, saveMassStyle, deleteMassStyle, } from './lib/rhinoBridge' // ---- Helpers -------------------------------------------------------------- @@ -140,6 +141,149 @@ function Field({ label, children, style }) { ) } +// ---- Mass-Style Section --------------------------------------------------- +// Globaler Preset-Picker fuer Raum-Rundung + Mass-Linien-Dezimalstellen. +// Hostet hier weil das thematisch zu Dimensionen passt, der Preset wird aber +// dokument-weit angewendet (Raum-Stempel lesen ihn auch). + +const RAUM_RUNDUNGS_LABELS = { + 'exakt': 'Exakt (2 Nachk.)', + '0.01': 'auf 0.01 m²', + '0.1': 'auf 0.1 m²', + '0.5': 'auf 0.5 m²', + '1': 'auf 1 m²', +} + +function MassStyleSection({ massStyles, activeId }) { + const styles = Array.isArray(massStyles) ? massStyles : [] + const active = styles.find(p => p.id === activeId) || styles[0] + const update = (patch) => { + if (!active) return + saveMassStyle({ ...active, ...patch }) + } + const addNew = () => { + const name = (window.prompt('Name für neuen Mass-Style:') || '').trim() + if (!name) return + // Aktuelle Werte als Vorlage uebernehmen (oder Defaults) + const tmpl = active || { raumRundung: '0.1', dimDezimalstellen: 2, dimEinheit: 'm' } + saveMassStyle({ + name, + raumRundung: tmpl.raumRundung, + dimDezimalstellen: tmpl.dimDezimalstellen, + dimEinheit: tmpl.dimEinheit, + }) + } + const remove = () => { + if (!active) return + if (styles.length <= 1) { + window.alert('Mindestens ein Mass-Style muss erhalten bleiben.') + return + } + if (!window.confirm(`Mass-Style "${active.name}" löschen?`)) return + deleteMassStyle(active.id) + } + return ( +
+
+ + + Mass-Style + +
+
+ + + +
+ {active && ( +
+
+ + Name + + update({ name: e.target.value })} + style={{ flex: 1, fontSize: 11, padding: '2px 6px' }} + /> +
+
+ + Raum-Rundung + + +
+
+ + Mass-Dezimalstellen + + +
+
+ + Mass-Einheit + + +
+
+ )} +
+ ) +} + + // ---- Hauptkomponente ------------------------------------------------------ export default function DimensionenApp() { @@ -195,6 +339,11 @@ export default function DimensionenApp() { }}>
+ + {/* Header: Selektions-Info + World/CPlane */}
Rundung -
- {RAUM_RUNDUNGEN.map(r => ( - - ))} -
+
@@ -1279,24 +1275,14 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) { {/* Dach-Typ */}
Typ -
- {[ - { code: 'pult', label: 'Pult' }, - { code: 'sattel', label: 'Sattel' }, - { code: 'walm', label: 'Walm' }, - { code: 'mansarde', label: 'Mansarde' }, - ].map(o => ( - - ))} -
+
@@ -1359,21 +1345,13 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) { <>
Variante -
- {[ - { code: 'walm', label: 'Walm', hint: 'Knick auf allen 4 Seiten (Pariser Stil)' }, - { code: 'giebel', label: 'Giebel', hint: 'Knick nur an langen Seiten, Schmalseiten als Giebelwand (DACH-Standard)' }, - { code: 'walm_giebel', label: 'W-G', hint: 'Unten Walm (Knick rundum), oben Giebel mit First über voller Länge' }, - ].map(o => ( - - ))} -
+
diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index 7feffa5..5ff6f66 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -171,6 +171,11 @@ export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) } export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) } +// --- Mass-Style (Dimensionen-Panel hostet) --- +export function setMassStyleActive(id) { send('MASS_STYLE_SET_ACTIVE', { id }) } +export function saveMassStyle(preset) { send('MASS_STYLE_SAVE', { preset }) } +export function deleteMassStyle(id) { send('MASS_STYLE_DELETE', { id }) } + // --- Kamera-Panel --- export function setKameraViewport(state) { send('SET_VIEWPORT', { ...state }) } export function setKameraProjection(parallel) { send('SET_PROJECTION', { parallel: !!parallel }) }