Projektdaten in Project-Settings + Swisstopo-Adress-Prefill

Schema-Erweiterung:
- _PROJECT_SETTINGS_DEFAULTS hat jetzt 'project'-Block mit
  name / number / address / bauherr / architekt / notes / projectZeroMum
- _normalize_project_meta stripped Strings + clampt mum als float
- load/save_project_settings handeln das 'project'-feld
- save_project_settings spiegelt projectZeroMum auch in den Legacy-Key
  dossier_project_zero_mum (fuer Geschoss-Settings-Dialog)
- load_project_settings liest Legacy-Key als Fallback wenn neuer Wert
  noch nicht gesetzt

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 03:21:19 +02:00
parent a597b58c93
commit 8f691e37c4
4 changed files with 164 additions and 22 deletions
+9
View File
@@ -8217,6 +8217,14 @@ class ElementeBridge(panel_base.BaseBridge):
e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None
ebenen = json.loads(e_raw) if e_raw else [] ebenen = json.loads(e_raw) if e_raw else []
except Exception: ebenen = [] 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): class _SwisstopoBridge(panel_base.BaseBridge):
def __init__(self): def __init__(self):
@@ -8225,6 +8233,7 @@ class ElementeBridge(panel_base.BaseBridge):
self.send("SWISSTOPO_STATE", { self.send("SWISSTOPO_STATE", {
"ebenen": ebenen, "ebenen": ebenen,
"cacheDir": __import__("swisstopo").CACHE_DIR, "cacheDir": __import__("swisstopo").CACHE_DIR,
"projectAddress": project_address,
}) })
def _push_log(self, msg): def _push_log(self, msg):
try: self.send("SWISSTOPO_LOG", {"msg": str(msg)}) try: self.send("SWISSTOPO_LOG", {"msg": str(msg)})
+74 -20
View File
@@ -283,11 +283,39 @@ _PROJECT_SETTINGS_DEFAULTS = {
"schnittHeightMin": -1.0, "schnittHeightMin": -1.0,
"schnittHeightMax": 12.0, "schnittHeightMax": 12.0,
}, },
"materials": [], # User-erweiterte Materialien (zusaetzlich zur "materials": [],
# hardcoded _MATERIAL_LIBRARY in elemente.py) "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): def _normalize_material(m):
"""Garantiert Material-Schema. Material ist REIN 3D — Section-Hatch """Garantiert Material-Schema. Material ist REIN 3D — Section-Hatch
(2D-Schnitt) wird via Ebenen-Settings am Layer konfiguriert. (2D-Schnitt) wird via Ebenen-Settings am Layer konfiguriert.
@@ -343,39 +371,64 @@ def _clamp(v, lo, hi):
def load_project_settings(doc): def load_project_settings(doc):
"""Liefert die Project-Settings als dict — mit Defaults-Merge wenn """Liefert die Project-Settings als dict — mit Defaults-Merge wenn
Felder fehlen. Garantiert dass `defaults` und `materials` immer da Felder fehlen. Garantiert dass `defaults`, `materials` und `project`
sind, und Materialien normalisiert (source + libraryId).""" immer da sind."""
raw = None raw = None
try: raw = doc.Strings.GetValue(_PROJECT_SETTINGS_KEY) if doc else None try: raw = doc.Strings.GetValue(_PROJECT_SETTINGS_KEY) if doc else None
except Exception: raw = None except Exception: raw = None
out = { out = {
"defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]), "defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]),
"materials": list(_PROJECT_SETTINGS_DEFAULTS["materials"]), "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: try:
data = json.loads(raw) if doc and out["project"]["projectZeroMum"] == 0.0:
if isinstance(data, dict): legacy = doc.Strings.GetValue("dossier_project_zero_mum")
d = data.get("defaults") if legacy:
if isinstance(d, dict): out["project"]["projectZeroMum"] = float(legacy)
for k, v in d.items(): except Exception: pass
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)
return out return out
def save_project_settings(doc, settings): def save_project_settings(doc, settings):
"""Persistiert Settings in doc.Strings. settings: dict mit """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 if doc is None or not isinstance(settings, dict): return False
try: 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, doc.Strings.SetString(_PROJECT_SETTINGS_KEY,
json.dumps(settings, ensure_ascii=False)) json.dumps(settings, ensure_ascii=False))
return True return True
@@ -849,6 +902,7 @@ class EbenenBridge(panel_base.BaseBridge):
new_settings = { new_settings = {
"defaults": updated.get("defaults", {}), "defaults": updated.get("defaults", {}),
"materials": updated.get("materials", []), "materials": updated.get("materials", []),
"project": updated.get("project", {}),
} }
save_project_settings(doc2, new_settings) save_project_settings(doc2, new_settings)
_broadcast_state(doc2) _broadcast_state(doc2)
+6 -1
View File
@@ -88,8 +88,13 @@ export default function SwisstopoApp() {
const logRef = useRef(null) const logRef = useRef(null)
useEffect(() => { useEffect(() => {
onMessage('SWISSTOPO_STATE', ({ ebenen }) => { onMessage('SWISSTOPO_STATE', ({ ebenen, projectAddress }) => {
if (Array.isArray(ebenen)) setEbenen(ebenen) 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 }) => { onMessage('GEOCODE_RESULT', ({ result }) => {
setSearching(false) setSearching(false)
+75 -1
View File
@@ -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 (
<div style={{ padding: '5px 0',
display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ flex: 1, fontSize: 11, color: 'var(--text-primary)' }}>
{label}
</span>
<input type="text" value={value || ''}
placeholder={placeholder || ''}
onChange={(ev) => onChange(ev.target.value)}
style={{ width, height: BAR_H, padding: '0 12px',
fontSize: 11 }} />
</div>
)
}
/* TextareaField — Label oben, mehrzeiliges Input darunter (full-width) */
function TextareaField({ label, value, onChange, rows = 3, placeholder }) {
return (
<div style={{ padding: '6px 0' }}>
<div style={{ fontSize: 11, color: 'var(--text-primary)',
marginBottom: 4 }}>{label}</div>
<textarea value={value || ''}
placeholder={placeholder || ''}
rows={rows}
onChange={(ev) => onChange(ev.target.value)}
style={{ width: '100%', boxSizing: 'border-box',
padding: '6px 12px',
fontSize: 11, fontFamily: 'var(--font)',
background: 'var(--bg-input)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
borderRadius: 12,
resize: 'vertical', outline: 'none',
lineHeight: 1.5 }} />
</div>
)
}
/* InlineNumberField — Label links, schmales Number-Input rechts (kompakt) */ /* InlineNumberField — Label links, schmales Number-Input rechts (kompakt) */
function InlineNumberField({ label, hint, value, onChange, step, min, max, suffix }) { function InlineNumberField({ label, hint, value, onChange, step, min, max, suffix }) {
return ( return (
@@ -579,7 +619,10 @@ export default function ProjectSettingsDialog({
const [draft, setDraft] = useState(() => ({ const [draft, setDraft] = useState(() => ({
defaults: { ...(initial.defaults || {}) }, defaults: { ...(initial.defaults || {}) },
materials: [...(initial.materials || [])], materials: [...(initial.materials || [])],
project: { ...(initial.project || {}) },
})) }))
const setProject = (k, v) =>
setDraft(d => ({ ...d, project: { ...(d.project || {}), [k]: v } }))
const [selMat, setSelMat] = useState(() => { const [selMat, setSelMat] = useState(() => {
// Default-Auswahl: erstes Builtin wenn vorhanden, sonst erstes Local // Default-Auswahl: erstes Builtin wenn vorhanden, sonst erstes Local
const b = initial.builtinMaterials || [] const b = initial.builtinMaterials || []
@@ -704,7 +747,38 @@ export default function ProjectSettingsDialog({
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', <div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
padding: '8px 14px' }}> padding: '8px 14px' }}>
{tab === 'defaults' && ( {tab === 'defaults' && (
<div style={{ maxWidth: 520 }}> <div style={{ maxWidth: 560 }}>
<DetailSection title="Projektdaten">
<InlineTextField label="Projektname"
value={draft.project?.name}
placeholder="z.B. Wohnhaus Müller"
onChange={(v) => setProject('name', v)} />
<InlineTextField label="Projekt-Nr."
value={draft.project?.number}
placeholder="z.B. 2026-014"
width={140}
onChange={(v) => setProject('number', v)} />
<TextareaField label="Adresse"
value={draft.project?.address}
placeholder="Strasse, PLZ Ort"
rows={2}
onChange={(v) => setProject('address', v)} />
<InlineTextField label="Bauherrschaft"
value={draft.project?.bauherr}
onChange={(v) => setProject('bauherr', v)} />
<InlineTextField label="Architekt:in"
value={draft.project?.architekt}
onChange={(v) => setProject('architekt', v)} />
<InlineNumberField label="EG-Nullpunkt m.ü.M"
value={draft.project?.projectZeroMum ?? 0.0}
step={0.01} suffix="m"
onChange={(v) => setProject('projectZeroMum', v || 0.0)}
hint="Höhe von Rhino z=0 (= EG OKFF) über Meeresspiegel. Wird vom Swisstopo-Terrain-Import zur Kalibrierung verwendet." />
<TextareaField label="Notizen"
value={draft.project?.notes}
rows={2}
onChange={(v) => setProject('notes', v)} />
</DetailSection>
<div style={{ fontSize: 10, color: 'var(--text-muted)', <div style={{ fontSize: 10, color: 'var(--text-muted)',
padding: '6px 0 10px', lineHeight: 1.5 }}> padding: '6px 0 10px', lineHeight: 1.5 }}>
Voreinstellungen fuer neue Elemente. Pro-Element editierte Voreinstellungen fuer neue Elemente. Pro-Element editierte