Ortho-Foto sichtbar (PictureFrame) + Oberleiste-Polish

Swisstopo Ortho
- AddPictureFrame statt Mesh+Material — Rhinos eigener Picture-Pfad mit
  embedBitmap=True + selfIllumination=True macht die Textur in allen
  Display-Modi sichtbar (Wireframe / Shaded / Rendered / Raytraced)
- asMesh=False (Brep-Variante) — asMesh=True ist auf Mac Rhino 8 broken
  (alle Pictures landen am gleichen Punkt unabhaengig von der Plane)
- Per-Tile Sub-Ebenen unter 80_swisstopo (z.B. 80_swisstopo/2763-1254_Ortho)
  via dossier_ebenen JSON registriert → erscheinen im Dossier-Ebenen-Manager
  mit eigener Visibility
- target_layer_idx wird vor AddPictureFrame als Active-Layer gesetzt,
  Picture landet direkt auf richtigem Sub-Layer (Move-danach broeselte
  das Material)
- Regex-Fix in _parse_swisstopo_tile_bbox: Separator zwischen den beiden
  Coords MUSS Hyphen sein, sonst matcht es faelschlich auf `_YEAR_EAST_`
  Patterns wie `_2025_2763_`

Oberleiste
- DOSSIER. Logo (Krungthep + Petrol-Punkt) + Launcher-Version
  (via __LAUNCHER_VERSION__ Vite-Define aus launcher/package.json)
- Overrides + Kombi vertikal gestapelt, gleiche Label-Spalte + Dropdown-
  Breite → Dropdowns auf gleicher X-Linie
- View-Icons neu zugeordnet:
    Top=view_quilt (Raster), Front=north (Pfeil), Persp=view_in_ar (3D)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 00:44:19 +02:00
parent afb59b6626
commit 85f09390bc
4 changed files with 405 additions and 161 deletions
+198 -23
View File
@@ -378,16 +378,16 @@ def _parse_swisstopo_tile_bbox(filename):
Filename-Pattern:
swissimage-dop10_2025_2763-1254_0.1_2056.tif (1km x 1km Tile)
swissimage-dop10_2025_2763-1254-12_0.5_2056.tif (250m Sub-Tile)
SWISSALTI3D_..._2763_1254.xyz (LV95-1km)
SWISSALTI3D_..._2763-1254.xyz (LV95-1km)
Wichtig: Separator zwischen den beiden Coords MUSS Hyphen sein
(`2763-1254`), sonst matcht der Regex faelschlich auf `_YEAR_EAST_`
Strukturen wie `_2025_2763_` und liefert Tile-Koords vom Jahr 2025.
Tile-Coords sind in 100m-Einheiten (E/N x 100). 2763-1254 = LV95
E=2'763'000, N=1'254'000 bbox = (2763000, 1254000, 2764000, 1255000).
Liefert (e_min, n_min, e_max, n_max) in Metern oder None."""
import re as _re
if not filename: return None
# Erst per-1km-Tile probieren: _NNNN-NNNN_ oder _NNNN_NNNN_
m = _re.search(r"[_-](\d{4})[-_](\d{4})(?:[-_]|\.)", filename)
m = _re.search(r"[_-](\d{4})-(\d{2,4})(?:[-_]|\.)", filename)
if not m: return None
e_k = int(m.group(1)); n_k = int(m.group(2))
e_min = e_k * 1000.0
@@ -7026,36 +7026,115 @@ class ElementeBridge(panel_base.BaseBridge):
plane_z = terr_max_z + z_offset
self._push_log("{} Ortho-Tile(s), platziere Plane bei Z={:.3f}".format(
len(ortho_paths), plane_z))
# Tile-IDs vorab extrahieren + Sub-Ebenen unter
# 80_swisstopo registrieren BEVOR die Pictures
# erzeugt werden. Damit kann jede _-Picture direkt
# auf dem richtigen Sub-Layer landen statt nach-
# traeglich verschoben zu werden (das brach die
# Textur).
import re as _re_o
tile_id_per_path = {}
for op in ortho_paths:
m = _re_o.search(r"(\d{3,4}-\d{2,4})",
os.path.basename(op))
if m: tile_id_per_path[op] = m.group(1)
if z_id and tile_id_per_path:
_find_ebene_sublayer_name(
d, ["swisstopo", "gelaende_topo"],
"80", "swisstopo",
default_color="#909090", default_lw=0.18)
self._ensure_ortho_tile_ebenen(
d, list(tile_id_per_path.values()))
# Target-Sub-Layer-Index pro Tile holen
import layer_builder as _lb_o
tile_layer_idx = {}
if z_id:
parent_idx = _lb_o._find_top_by_id(d, z_id)
if parent_idx >= 0:
parent_id_ = d.Layers[parent_idx].Id
base_idx = _lb_o._find_sublayer_by_code(
d, parent_id_, "80")
if base_idx >= 0:
base_id_ = d.Layers[base_idx].Id
for op, tid in tile_id_per_path.items():
idx = _lb_o._find_sublayer_by_code(
d, base_id_, tid)
if idx >= 0: tile_layer_idx[op] = idx
ortho_objs = []
for ortho_path in ortho_paths:
# tile_bbox aus Filename ableiten — swissimage
# tile_id = "1076-33" o.ae. → LV95 Tile-Origin
tile_bbox = _parse_swisstopo_tile_bbox(
os.path.basename(ortho_path))
if tile_bbox is None:
self._push_log(" → Tile-bbox nicht ableitbar aus {}".format(
os.path.basename(ortho_path)))
continue
tgt_idx = tile_layer_idx.get(ortho_path, -1)
try:
obj = swisstopo.add_ortho_plane(
d, ortho_path, tile_bbox,
origin_shift, m_to_unit, z_doc=plane_z)
if obj: ortho_objs.append(obj)
origin_shift, m_to_unit, z_doc=plane_z,
target_layer_idx=tgt_idx)
if obj:
ortho_objs.append(obj)
# Tag fuer Replace-Detection bei naechstem Import
try:
at = obj.Attributes.Duplicate()
at.SetUserString(
"dossier_swisstopo_kind", "ortho")
d.Objects.ModifyAttributes(obj, at, True)
except Exception: pass
except Exception as ex:
self._push_log("Ortho-Apply: {}".format(ex))
self._push_log("{} Ortho-Plane(s) erstellt".format(len(ortho_objs)))
# Layer (gleicher Geschoss-Sublayer 80_swisstopo wie Terrain)
if z_id and ortho_objs:
sub_name = _find_ebene_sublayer_name(
d, ["swisstopo", "gelaende_topo"],
"80", "swisstopo",
default_color="#909090", default_lw=0.18)
self._move_to_sublayer(d, ortho_objs, z_id,
sub_name.split("_", 1)[0], tag="ortho",
fallback_name=sub_name,
fallback_color="#909090")
elif ortho_objs:
self._tag_objects(d, ortho_objs, "ortho")
self._push_log("{} Ortho-Plane(s) auf eigene Sub-Layer".format(
len(ortho_objs)))
# End-Diagnose mit BBox-Koords damit wir sehen
# wo die Pictures tatsaechlich gelandet sind.
try:
diag = []
for o in d.Objects:
if o is None or o.IsDeleted: continue
tag = o.Attributes.GetUserString("dossier_swisstopo_kind")
if tag != "ortho": continue
li = o.Attributes.LayerIndex
lay = d.Layers[li]
try: bb = o.Geometry.GetBoundingBox(True)
except Exception: bb = None
diag.append({
"id": str(o.Id)[:8],
"lay": lay.FullPath,
"vis": lay.IsVisible,
"lck": lay.IsLocked,
"hid": o.IsHidden,
"typ": type(o.Geometry).__name__,
"bb": bb,
})
self._push_log("DIAG: {} Ortho-Objekte im Doc".format(len(diag)))
for s in diag[:4]:
bb = s["bb"]
bbstr = "bb=({:.0f},{:.0f},{:.0f})→({:.0f},{:.0f},{:.0f})".format(
bb.Min.X, bb.Min.Y, bb.Min.Z,
bb.Max.X, bb.Max.Y, bb.Max.Z) if bb else "bb=?"
self._push_log(" {} typ={} {} vis={} hid={} lay='{}'".format(
s["id"], s["typ"], bbstr,
s["vis"], s["hid"], s["lay"]))
# Building-bbox zum Vergleich
bb_b = rg.BoundingBox.Empty
n_b = 0
for o in d.Objects:
if o is None or o.IsDeleted: continue
tag = o.Attributes.GetUserString("dossier_swisstopo_kind")
if tag != "buildings": continue
try:
bb_b.Union(o.Geometry.GetBoundingBox(True))
n_b += 1
except Exception: pass
if bb_b.IsValid:
self._push_log("DIAG: Buildings ({} Obj) bb=({:.0f},{:.0f},{:.0f})→({:.0f},{:.0f},{:.0f})".format(
n_b,
bb_b.Min.X, bb_b.Min.Y, bb_b.Min.Z,
bb_b.Max.X, bb_b.Max.Y, bb_b.Max.Z))
except Exception as ex:
self._push_log("DIAG fail: {}".format(ex))
new_obj_ids.extend(o.Id for o in ortho_objs)
new_obj_ids.extend(o.Id for o, _ in mesh_objects)
@@ -7150,6 +7229,102 @@ class ElementeBridge(panel_base.BaseBridge):
doc.Objects.ModifyAttributes(o, attrs, True)
except Exception: pass
def _move_orthos_to_per_tile_layers(self, doc, objs_with_paths,
z_id):
"""Jedes Ortho-Tile bekommt eine eigene Sub-Ebene unter
80_swisstopo (Code=Tile-ID, z.B. '2763-1254'). Die Sub-Ebene
wird via dossier_ebenen JSON registriert erscheint sowohl
im Dossier-Ebenen-Manager als auch im Rhino-Layer-Panel.
User kann jedes Tile einzeln togglen."""
import re as _re
try:
# Schritt 1: alle tile_ids ermitteln
tiles = []
for obj, path in objs_with_paths:
m = _re.search(r"(\d{3,4}-\d{2,4})",
os.path.basename(path))
tile_id = m.group(1) if m else None
if tile_id: tiles.append((obj, tile_id))
if not tiles: return
# Schritt 2: alle als Children von 80_swisstopo registrieren
self._ensure_ortho_tile_ebenen(
doc, [t for _, t in tiles])
# Schritt 3: Objekte auf die jetzt existierenden Sublayer
import layer_builder
parent_idx = layer_builder._find_top_by_id(doc, z_id)
if parent_idx < 0: return
parent_id = doc.Layers[parent_idx].Id
base_idx = layer_builder._find_sublayer_by_code(
doc, parent_id, "80")
if base_idx < 0:
self._push_log(" 80_swisstopo nicht gefunden")
return
base_id = doc.Layers[base_idx].Id
moved = 0
for obj, tile_id in tiles:
sub_idx = layer_builder._find_sublayer_by_code(
doc, base_id, tile_id)
if sub_idx < 0:
self._push_log(" Sub-Layer fuer {} nicht gefunden".format(tile_id))
continue
try:
attrs = obj.Attributes.Duplicate()
attrs.LayerIndex = sub_idx
attrs.SetUserString("dossier_swisstopo_kind", "ortho")
doc.Objects.ModifyAttributes(obj, attrs, True)
moved += 1
except Exception as ex:
self._push_log(" ortho-move {}: {}".format(tile_id, ex))
self._push_log("{} Ortho-Tile(s) auf eigene Sub-Ebene".format(moved))
except Exception as ex:
self._push_log(" ortho-per-tile: {}".format(ex))
def _ensure_ortho_tile_ebenen(self, doc, tile_ids):
"""Registriert jeden Tile als Child unter '80_swisstopo' in
dossier_ebenen JSON, baut Layer einmal synchron, broadcastet
an die UI. Duplikate werden uebersprungen."""
if not tile_ids: return
raw = doc.Strings.GetValue("dossier_ebenen")
try: ebenen = json.loads(raw) if raw else []
except Exception: ebenen = []
if not isinstance(ebenen, list): ebenen = []
parent = next((e for e in ebenen if isinstance(e, dict)
and e.get("code") == "80"), None)
if parent is None:
parent = {
"code": "80", "name": "swisstopo",
"color": "#909090", "lw": 0.18,
"visible": True, "locked": False,
"children": [],
}
ebenen.append(parent)
if not isinstance(parent.get("children"), list):
parent["children"] = []
have = {c.get("code") for c in parent["children"]
if isinstance(c, dict)}
changed = False
for tile_id in set(tile_ids):
if tile_id in have: continue
parent["children"].append({
"code": tile_id, "name": "Ortho",
"color": "#909090", "lw": 0.13,
"visible": True, "locked": False,
})
changed = True
if not changed: return
try:
doc.Strings.SetString("dossier_ebenen",
json.dumps(ebenen, ensure_ascii=False))
import layer_builder
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
zlist = json.loads(z_raw) if z_raw else []
if zlist:
layer_builder.build_layers(doc, zlist, ebenen)
import rhinopanel
rhinopanel._broadcast_state(doc)
except Exception as ex:
self._push_log(" ortho-ebenen build: {}".format(ex))
def _ensure_sub_sublayer(self, doc, parent_id, name,
color_hex="#888888", lw=0.25):
"""Findet oder erstellt einen Sub-Layer mit Name <name> direkt