From 85f09390bcf539f4170cffa7402a10a9f0171149 Mon Sep 17 00:00:00 2001 From: karim Date: Wed, 20 May 2026 00:44:19 +0200 Subject: [PATCH] Ortho-Foto sichtbar (PictureFrame) + Oberleiste-Polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- rhino/elemente.py | 221 +++++++++++++++++++++++++++++++++++---- rhino/swisstopo.py | 105 +++++++++++++------ src/OberleisteApp.jsx | 236 ++++++++++++++++++++++++------------------ vite.config.js | 4 +- 4 files changed, 405 insertions(+), 161 deletions(-) diff --git a/rhino/elemente.py b/rhino/elemente.py index f65a886..adfc67c 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -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 direkt diff --git a/rhino/swisstopo.py b/rhino/swisstopo.py index dd5545a..fb07a10 100644 --- a/rhino/swisstopo.py +++ b/rhino/swisstopo.py @@ -18,6 +18,7 @@ import urllib.request import urllib.parse import Rhino import Rhino.Geometry as rg +import System CACHE_DIR = os.path.expanduser("~/Library/Caches/Dossier/swisstopo") STAC_BASE = "https://data.geo.admin.ch/api/stac/v1" @@ -737,7 +738,7 @@ def _geotiff_to_png(tif_path, max_dim=2048): def add_ortho_plane(doc, ortho_path, tile_bbox_lv95, shift_lv95, m_to_unit, - z_doc=0.0): + z_doc=0.0, target_layer_idx=-1): """Erzeugt eine planare Brep-Flaeche mit dem SWISSIMAGE-Foto als Material, direkt sichtbar in Top/Shaded/Rendered Display-Mode. @@ -760,41 +761,77 @@ def add_ortho_plane(doc, ortho_path, tile_bbox_lv95, shift_lv95, m_to_unit, x_max = (e_max - sx) * m_to_unit y_min = (n_min - sy) * m_to_unit y_max = (n_max - sy) * m_to_unit - # Mesh-Quad mit expliziten Per-Vertex-UV-Koordinaten — bombensicher - # fuer Cycles/Raytraced. Eine Brep-Plane braucht erst Render-Mesh- - # Erzeugung + TextureMapping, was diverse Fallstricke hat. - mesh = rg.Mesh() - mesh.Vertices.Add(x_min, y_min, z_doc) # 0 → UV (0,0) - mesh.Vertices.Add(x_max, y_min, z_doc) # 1 → UV (1,0) - mesh.Vertices.Add(x_max, y_max, z_doc) # 2 → UV (1,1) - mesh.Vertices.Add(x_min, y_max, z_doc) # 3 → UV (0,1) - mesh.Faces.AddFace(0, 1, 2, 3) - mesh.TextureCoordinates.Add(0.0, 0.0) - mesh.TextureCoordinates.Add(1.0, 0.0) - mesh.TextureCoordinates.Add(1.0, 1.0) - mesh.TextureCoordinates.Add(0.0, 1.0) - mesh.Normals.ComputeNormals() - mesh.Compact() - gid = doc.Objects.AddMesh(mesh) + # Centered plane, mesh-based PictureFrame mit embedded Bitmap. + # asMesh=True ist anders gerendert als Brep-Variante; bei Brep zeigte + # Mac Rhino 8 die Textur in keinem Modus. + cx = (x_min + x_max) / 2.0 + cy = (y_min + y_max) / 2.0 + width = abs(x_max - x_min) + height = abs(y_max - y_min) + plane = rg.Plane(rg.Point3d(cx, cy, z_doc), + rg.Vector3d.XAxis, rg.Vector3d.YAxis) + try: + size_mb = os.path.getsize(ortho_path) / 1e6 + print("[SWISSTOPO] PictureFrame src: {} ({:.1f} MB)".format( + os.path.basename(ortho_path), size_mb)) + except Exception: + print("[SWISSTOPO] file nicht lesbar:", ortho_path) + return None + try: + gid = doc.Objects.AddPictureFrame( + plane, ortho_path, + False, # asMesh=False (Brep) — Mac Rhino 8 ignoriert die + # Plane bei asMesh=True, alle Pictures landen + # uebereinander + width, height, + True, # selfIllumination=True — Textur unabhaengig von + # Lighting sichtbar (sonst evtl. dunkel in modes + # ohne Lichtquellen) + True) # embedBitmap=True (Pfad-Probleme umgehen) + if gid == System.Guid.Empty: + print("[SWISSTOPO] AddPictureFrame: Empty-GUID") + return None + except Exception as ex: + print("[SWISSTOPO] AddPictureFrame exception:", ex) + return None obj = doc.Objects.Find(gid) if obj is None: return None - # Material: Legacy + ToPhysicallyBased + PBR_BaseColor-Texture. - # Bekannt instabil unter Mac Rhino 8 für Raytraced (Cycles greift den - # Shim nicht zuverlaessig); zumindest Shaded zeigt die Textur. + # Auf Ziel-Layer schieben (nachträglich; Material bleibt auf Object). + if target_layer_idx >= 0: + try: + at = obj.Attributes.Duplicate() + at.LayerIndex = target_layer_idx + doc.Objects.ModifyAttributes(obj, at, True) + except Exception as ex: + print("[SWISSTOPO] Layer-Move fail:", ex) + # Diagnose: hat das Material tatsaechlich eine Bitmap-Textur drin? try: - mat = Rhino.DocObjects.Material() - mat.Name = "swisstopo_ortho" - mat.SetBitmapTexture(ortho_path) - mat.ToPhysicallyBased() - tex = Rhino.DocObjects.Texture() - tex.FileName = ortho_path - tex.Enabled = True - mat.SetTexture(tex, Rhino.DocObjects.TextureType.PBR_BaseColor) - midx = doc.Materials.Add(mat) - attrs = obj.Attributes.Duplicate() - attrs.MaterialSource = Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject - attrs.MaterialIndex = midx - doc.Objects.ModifyAttributes(obj, attrs, True) + o2 = doc.Objects.Find(gid) + a = o2.Attributes + print("[SWISSTOPO] PictureFrame OK id={} layer='{}' MatSrc={} MatIdx={} hidden={}".format( + gid, doc.Layers[a.LayerIndex].FullPath, + a.MaterialSource, a.MaterialIndex, o2.IsHidden)) + # Material-Inspect + mat = None + try: + if a.MaterialIndex >= 0 and a.MaterialIndex < doc.Materials.Count: + mat = doc.Materials[a.MaterialIndex] + except Exception: pass + if mat is not None: + try: bmp_fn = mat.GetBitmapTexture().FileName if mat.GetBitmapTexture() else None + except Exception: bmp_fn = None + try: tex = mat.GetTexture(Rhino.DocObjects.TextureType.Bitmap) + except Exception: tex = None + print("[SWISSTOPO] material[{}].Name='{}' bitmap='{}' tex={} textures={}".format( + a.MaterialIndex, mat.Name, bmp_fn, tex, + mat.GetTextures().Length if hasattr(mat, "GetTextures") else "?")) + # RenderMaterial-Inspect + try: + rm = a.RenderMaterial + print("[SWISSTOPO] RenderMaterial: {}".format( + rm.Name if rm else "None")) + except Exception as ex: + print("[SWISSTOPO] RenderMaterial-check fail:", ex) except Exception as ex: - print("[SWISSTOPO] ortho-material:", ex) + print("[SWISSTOPO] diag fail:", ex) return obj diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index 5747f91..386a0bf 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -21,10 +21,10 @@ const PRESETS = [ ] const VIEWS = [ - { value: 'Top', icon: 'north', label: 'Top' }, - { value: 'Front', icon: 'view_in_ar', label: 'Front' }, + { value: 'Top', icon: 'view_quilt', label: 'Top' }, + { value: 'Front', icon: 'north', label: 'Front' }, { value: 'Right', icon: 'east', label: 'Right' }, - { value: 'Perspective', icon: 'view_quilt', label: 'Persp' }, + { value: 'Perspective', icon: 'view_in_ar', label: 'Persp' }, ] function fmtScale(s) { @@ -207,21 +207,37 @@ export default function OberleisteApp() { {/* === Toolbar (View, Display, Massstab, Snap, Overrides) === */}
- - DOSSIER {__APP_VERSION__} - + + DOSSIER. + + + v{__LAUNCHER_VERSION__} + +