Swisstopo + OSM Importer + Höhenlinien + Bulk-Op Performance
Swisstopo Iter 3:
- Ortho-Drape: TIN-Mesh aus Terrain-Grid mit per-vertex UVs + PictureFrame-Material
- Project-Cache: TIFs werden neben .3dm gespeichert (SMB-shareable)
- Layer-Restruktur: 80_swisstopo/{Terrain, Luftbild} Sub-Ebenen
- TIFs direkt (kein PNG-Downsampling) für volle Auflösung
- UV-Inset gegen weisse Streifen zwischen Kacheln
- Hoehenlinien (2D, swissALTI3D) auf aktives Geschoss OKFF projiziert
- TIN-Mesh + Schichtenmodell aus Contours (separate Optionen)
- TLM3D entfernt (swisstopo liefert nur GDB/SHP, kein DXF)
OSM Importer (neu):
- rhino/osm.py: Overpass-API-Client
- src/OsmApp.jsx: React-Dialog mit Adresse + Radius + 7 Kategorien
- Strassen/Gebäude/Wasser/Wasserläufe/Parks/Wald/Fusswege (Codes 7101-7107)
- ElementeApp: PillGroup "Importer" mit Swisstopo + OSM Buttons
Sub-Ebenen — rekursiv durch hierarchische Ebenen:
- Visibility-Toggle: slimEbene rekursiv (children bleiben erhalten)
- Settings-Dialog: _find_sublayer_by_code_recursive + _replace_in_tree
- Hatch Auto-Fill: refresh_layer_fills + _fill_signature + _ebene_fill_for_layer
alle rekursiv durch children
- EbenenSettingsApp: flattenEbenen-Helper
Bulk-Op Performance (Delete/Cut/etc.):
- _USER_BULK_CMDS + _BULK_ACTIVE_KEY Sticky-Flag
- CommandBegin: doc.Views.RedrawEnabled = False + Listener-Bail aktiv
- CommandEnd: RedrawEnabled restore + 1× Redraw + Selection-Refresh
- Bail-outs in dimensionen.on_idle/on_select, elemente._on_idle_selection,
gestaltung.on_idle_flush/on_delete
- Verhindert das sichtbare "Runterzählen" pro Element bei Bulk-Delete
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+62
-23
@@ -302,9 +302,20 @@ def _ebene_fill_for_layer(doc, layer):
|
||||
print("[GESTALTUNG] _ebene_fill_for_layer: json-Fehler:", ex)
|
||||
return None
|
||||
if not isinstance(ebenen, list): return None
|
||||
for e in ebenen:
|
||||
if not isinstance(e, dict): continue
|
||||
if e.get("code") != code: continue
|
||||
# Rekursiv durch Tree — Sub-Ebenen sind in children verschachtelt
|
||||
def _find_by_code(lst, target):
|
||||
for e in lst:
|
||||
if not isinstance(e, dict): continue
|
||||
if e.get("code") == target: return e
|
||||
kids = e.get("children")
|
||||
if isinstance(kids, list) and kids:
|
||||
hit = _find_by_code(kids, target)
|
||||
if hit is not None: return hit
|
||||
return None
|
||||
found = _find_by_code(ebenen, code)
|
||||
if found is None: return None
|
||||
e = found
|
||||
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))
|
||||
@@ -471,19 +482,26 @@ def refresh_layer_fills(doc):
|
||||
if not isinstance(ebenen, list):
|
||||
return 0
|
||||
|
||||
# Code -> fill-dict fuer schnellen Lookup
|
||||
# Code -> fill-dict fuer schnellen Lookup. Rekursiv durch Children, damit
|
||||
# Sub-Ebenen-Schraffuren auch wirken (sonst landen Polygone auf z.B.
|
||||
# 70_osm/7102_Gebaeudeumrisse nie in der Auto-Fill-Logik).
|
||||
def _walk_fills(lst, out):
|
||||
for e in lst:
|
||||
if not isinstance(e, dict): continue
|
||||
f = e.get("fill")
|
||||
if isinstance(f, dict) and f.get("pattern") not in (None, "None"):
|
||||
out[e.get("code")] = {
|
||||
"pattern": f.get("pattern"),
|
||||
"source": f.get("source", "layer"),
|
||||
"color": f.get("color"),
|
||||
"scale": float(f.get("scale", 1.0)) if f.get("scale") is not None else 1.0,
|
||||
"rotation": float(f.get("rotation", 0.0)) if f.get("rotation") is not None else 0.0,
|
||||
}
|
||||
kids = e.get("children")
|
||||
if isinstance(kids, list) and kids:
|
||||
_walk_fills(kids, out)
|
||||
fill_by_code = {}
|
||||
for e in ebenen:
|
||||
if not isinstance(e, dict): continue
|
||||
f = e.get("fill")
|
||||
if isinstance(f, dict) and f.get("pattern") not in (None, "None"):
|
||||
fill_by_code[e.get("code")] = {
|
||||
"pattern": f.get("pattern"),
|
||||
"source": f.get("source", "layer"),
|
||||
"color": f.get("color"),
|
||||
"scale": float(f.get("scale", 1.0)) if f.get("scale") is not None else 1.0,
|
||||
"rotation": float(f.get("rotation", 0.0)) if f.get("rotation") is not None else 0.0,
|
||||
}
|
||||
_walk_fills(ebenen, fill_by_code)
|
||||
if not fill_by_code:
|
||||
return 0
|
||||
|
||||
@@ -1367,23 +1385,38 @@ def _install_selection_listener(bridge):
|
||||
if sc.sticky.get(flag):
|
||||
return
|
||||
|
||||
# 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'
|
||||
# zugemuellt. Wir setzen nur ein Dirty-Flag und feuern EINMAL beim
|
||||
# naechsten Idle-Tick.
|
||||
def refresh(*args):
|
||||
# Waehrend Move/Rotate/Mirror/Scale schweigen — Rhino oszilliert die
|
||||
# Selection pro transformiertem Object mehrfach (deselect→delete→add→
|
||||
# reselect). Bei 7 Objekten sind das ~100 IPC-Sends in den WebView,
|
||||
# was sich als „Regen" anfuehlt. elemente._on_command_end refresht
|
||||
# nach dem Command einmalig.
|
||||
# Waehrend Swisstopo-Import: Rhino selektiert jedes neu importierte
|
||||
# Objekt → 5000 selection-changes → 5000 send-Calls in den WebView →
|
||||
# erstickt den UI-Thread. Sticky-Flag => bail.
|
||||
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||
if sc.sticky.get("_dossier_undo_active"): return
|
||||
sc.sticky["_gestaltung_selection_dirty"] = True
|
||||
|
||||
def on_idle_flush(sender, args):
|
||||
if not sc.sticky.get("_gestaltung_selection_dirty"): return
|
||||
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||
if sc.sticky.get("_dossier_undo_active"): return
|
||||
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||
sc.sticky["_gestaltung_selection_dirty"] = False
|
||||
b = sc.sticky.get("gestaltung_bridge")
|
||||
if b is not None:
|
||||
try: b._send_selection()
|
||||
except Exception: pass
|
||||
|
||||
# Idle-Hook nur einmal aufhaengen (sticky guard)
|
||||
if not sc.sticky.get("_gestaltung_idle_attached"):
|
||||
try:
|
||||
Rhino.RhinoApp.Idle += on_idle_flush
|
||||
sc.sticky["_gestaltung_idle_attached"] = True
|
||||
except Exception as ex:
|
||||
print("[GESTALTUNG] Idle-Hook fail:", ex)
|
||||
|
||||
def on_replace(sender, args):
|
||||
"""Sync Curve↔Hatch bei Move/Replace:
|
||||
- Curve hat _FILL_KEY (= hatch_id) → Hatch via Hatch.Create neu auf die
|
||||
@@ -1484,6 +1517,12 @@ def _install_selection_listener(bridge):
|
||||
Curve aufraeumen damit beim naechsten Toggle keine Geister-Referenz steht."""
|
||||
if sc.sticky.get("_dossier_undo_active"): return
|
||||
if sc.sticky.get("_elemente_regen_busy"): return
|
||||
# Bulk-Delete (SelAll + Delete): pro-Object Hatch-Sync ueberspringen
|
||||
# — bei 6000 Objekten waere das massive Overhead. Hatch-Verweise
|
||||
# wuerden zwar nicht aufgeraeumt aber das ist tolerierbar
|
||||
# (Sticky-Cache laeuft auch ohne Cleanup ab, alte Eintraege bleiben
|
||||
# nur unsichtbar liegen).
|
||||
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||
obj = args.TheObject
|
||||
if obj is None or obj.Id in _processing:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user