From 8f691e37c4f323c73ac04d3e37cc9eef9508283b Mon Sep 17 00:00:00 2001 From: karim Date: Mon, 25 May 2026 03:21:19 +0200 Subject: [PATCH] Projektdaten in Project-Settings + Swisstopo-Adress-Prefill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schema-Erweiterung: - _PROJECT_SETTINGS_DEFAULTS hat jetzt 'project'-Block mit name / number / address / bauherr / architekt / notes / projectZeroMum - _normalize_project_meta stripped Strings + clampt mum als float - load/save_project_settings handeln das 'project'-feld - save_project_settings spiegelt projectZeroMum auch in den Legacy-Key dossier_project_zero_mum (fuer Geschoss-Settings-Dialog) - load_project_settings liest Legacy-Key als Fallback wenn neuer Wert noch nicht gesetzt UI: - InlineTextField + TextareaField Helpers (Pill-Stil) - Projektdaten-Section in Voreinstellungen-Tab: Name, Projekt-Nr., Adresse, Bauherrschaft, Architekt:in, EG-Nullpunkt m.ü.M (mit Hinweis auf Swisstopo-Nutzung), Notizen Swisstopo: - _cmd_open_swisstopo_dialog laedt Projekt-Adresse + sendet projectAddress im SWISSTOPO_STATE - SwisstopoApp: vorbelegt searchText mit projectAddress wenn Feld leer ist (User-Input wird nicht ueberschrieben) Co-Authored-By: Claude Opus 4.7 --- rhino/elemente.py | 9 +++ rhino/rhinopanel.py | 94 +++++++++++++++++++----- src/SwisstopoApp.jsx | 7 +- src/components/ProjectSettingsDialog.jsx | 76 ++++++++++++++++++- 4 files changed, 164 insertions(+), 22 deletions(-) diff --git a/rhino/elemente.py b/rhino/elemente.py index b642e41..05ea173 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -8217,6 +8217,14 @@ class ElementeBridge(panel_base.BaseBridge): e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None ebenen = json.loads(e_raw) if e_raw else [] except Exception: ebenen = [] + # Projekt-Adresse als Vorschlag fuer die Adress-Suche + project_address = "" + try: + import rhinopanel + ps = rhinopanel.load_project_settings(doc) if doc else None + if isinstance(ps, dict): + project_address = (ps.get("project", {}) or {}).get("address") or "" + except Exception: pass class _SwisstopoBridge(panel_base.BaseBridge): def __init__(self): @@ -8225,6 +8233,7 @@ class ElementeBridge(panel_base.BaseBridge): self.send("SWISSTOPO_STATE", { "ebenen": ebenen, "cacheDir": __import__("swisstopo").CACHE_DIR, + "projectAddress": project_address, }) def _push_log(self, msg): try: self.send("SWISSTOPO_LOG", {"msg": str(msg)}) diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index 98a34df..9bd98b7 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -283,11 +283,39 @@ _PROJECT_SETTINGS_DEFAULTS = { "schnittHeightMin": -1.0, "schnittHeightMax": 12.0, }, - "materials": [], # User-erweiterte Materialien (zusaetzlich zur - # hardcoded _MATERIAL_LIBRARY in elemente.py) + "materials": [], + "project": { + "name": "", + "number": "", + "address": "", + "bauherr": "", + "architekt": "", + "notes": "", + # m.ü.M (Meter ueber Meer) des Rhino-z=0 (= EG-Nullpunkt). Wird + # von Swisstopo/Terrain-Import zur Hoehen-Kalibrierung genutzt. + "projectZeroMum": 0.0, + }, } +def _normalize_project_meta(p): + """Garantiert das project-Schema. Strings werden gestripped, mum als + float (default 0.0 wenn nicht parsebar).""" + if not isinstance(p, dict): p = {} + try: mum = float(p.get("projectZeroMum", 0.0) or 0.0) + except Exception: mum = 0.0 + def _s(k): return str(p.get(k) or "") + return { + "name": _s("name"), + "number": _s("number"), + "address": _s("address"), + "bauherr": _s("bauherr"), + "architekt": _s("architekt"), + "notes": _s("notes"), + "projectZeroMum": mum, + } + + def _normalize_material(m): """Garantiert Material-Schema. Material ist REIN 3D — Section-Hatch (2D-Schnitt) wird via Ebenen-Settings am Layer konfiguriert. @@ -343,39 +371,64 @@ def _clamp(v, lo, hi): def load_project_settings(doc): """Liefert die Project-Settings als dict — mit Defaults-Merge wenn - Felder fehlen. Garantiert dass `defaults` und `materials` immer da - sind, und Materialien normalisiert (source + libraryId).""" + Felder fehlen. Garantiert dass `defaults`, `materials` und `project` + immer da sind.""" raw = None try: raw = doc.Strings.GetValue(_PROJECT_SETTINGS_KEY) if doc else None except Exception: raw = None out = { "defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]), "materials": list(_PROJECT_SETTINGS_DEFAULTS["materials"]), + "project": dict(_PROJECT_SETTINGS_DEFAULTS["project"]), } - if not raw: return out + if raw: + try: + data = json.loads(raw) + if isinstance(data, dict): + d = data.get("defaults") + if isinstance(d, dict): + for k, v in d.items(): + out["defaults"][k] = v + m = data.get("materials") + if isinstance(m, list): + out["materials"] = [ + _normalize_material(x) for x in m + if _normalize_material(x) is not None + ] + pr = data.get("project") + if isinstance(pr, dict): + out["project"] = _normalize_project_meta(pr) + except Exception as ex: + print("[PROJECT-SETTINGS] load:", ex) + # Legacy-Sync: alter dossier_project_zero_mum-Wert in project.projectZeroMum + # spiegeln wenn Project-Settings noch keinen hat. Geschoss-Settings-Dialog + # schreibt diesen Key separat — wir lesen ihn als fallback. try: - data = json.loads(raw) - if isinstance(data, dict): - d = data.get("defaults") - if isinstance(d, dict): - for k, v in d.items(): - out["defaults"][k] = v - m = data.get("materials") - if isinstance(m, list): - out["materials"] = [ - _normalize_material(x) for x in m - if _normalize_material(x) is not None - ] - except Exception as ex: - print("[PROJECT-SETTINGS] load:", ex) + if doc and out["project"]["projectZeroMum"] == 0.0: + legacy = doc.Strings.GetValue("dossier_project_zero_mum") + if legacy: + out["project"]["projectZeroMum"] = float(legacy) + except Exception: pass return out def save_project_settings(doc, settings): """Persistiert Settings in doc.Strings. settings: dict mit - 'defaults' + 'materials'. Caller broadcastet ggf. selbst.""" + 'defaults' + 'materials' + 'project'. Schreibt auch + `dossier_project_zero_mum` separat (legacy-key fuer Geschoss-Settings).""" if doc is None or not isinstance(settings, dict): return False try: + # project-Block normalisieren bevor wir speichern + if "project" in settings: + settings = dict(settings) + settings["project"] = _normalize_project_meta(settings["project"]) + # Legacy-Key spiegeln + try: + mum = settings["project"].get("projectZeroMum", 0.0) + doc.Strings.SetString("dossier_project_zero_mum", + "{:.6f}".format(float(mum))) + except Exception as ex: + print("[PROJECT-SETTINGS] mum sync:", ex) doc.Strings.SetString(_PROJECT_SETTINGS_KEY, json.dumps(settings, ensure_ascii=False)) return True @@ -849,6 +902,7 @@ class EbenenBridge(panel_base.BaseBridge): new_settings = { "defaults": updated.get("defaults", {}), "materials": updated.get("materials", []), + "project": updated.get("project", {}), } save_project_settings(doc2, new_settings) _broadcast_state(doc2) diff --git a/src/SwisstopoApp.jsx b/src/SwisstopoApp.jsx index fb7db40..789bc9d 100644 --- a/src/SwisstopoApp.jsx +++ b/src/SwisstopoApp.jsx @@ -88,8 +88,13 @@ export default function SwisstopoApp() { const logRef = useRef(null) useEffect(() => { - onMessage('SWISSTOPO_STATE', ({ ebenen }) => { + onMessage('SWISSTOPO_STATE', ({ ebenen, projectAddress }) => { if (Array.isArray(ebenen)) setEbenen(ebenen) + // Projekt-Adresse aus Project-Settings als Vorschlag — nur belegen + // wenn das Feld noch leer ist (User-Input nicht ueberschreiben). + if (projectAddress) { + setSearchText(prev => prev && prev.trim() ? prev : projectAddress) + } }) onMessage('GEOCODE_RESULT', ({ result }) => { setSearching(false) diff --git a/src/components/ProjectSettingsDialog.jsx b/src/components/ProjectSettingsDialog.jsx index 737c616..58d7bfc 100644 --- a/src/components/ProjectSettingsDialog.jsx +++ b/src/components/ProjectSettingsDialog.jsx @@ -25,6 +25,46 @@ function Field({ label, hint, children, style }) { ) } +/* InlineTextField — Label links, Text-Input rechts (kompakt) */ +function InlineTextField({ label, value, onChange, placeholder, width = 240 }) { + return ( +
+ + {label} + + onChange(ev.target.value)} + style={{ width, height: BAR_H, padding: '0 12px', + fontSize: 11 }} /> +
+ ) +} + +/* TextareaField — Label oben, mehrzeiliges Input darunter (full-width) */ +function TextareaField({ label, value, onChange, rows = 3, placeholder }) { + return ( +
+
{label}
+