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 ( +