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:
+198
-23
@@ -378,16 +378,16 @@ def _parse_swisstopo_tile_bbox(filename):
|
|||||||
|
|
||||||
Filename-Pattern:
|
Filename-Pattern:
|
||||||
swissimage-dop10_2025_2763-1254_0.1_2056.tif (1km x 1km Tile)
|
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."""
|
Liefert (e_min, n_min, e_max, n_max) in Metern oder None."""
|
||||||
import re as _re
|
import re as _re
|
||||||
if not filename: return None
|
if not filename: return None
|
||||||
# Erst per-1km-Tile probieren: _NNNN-NNNN_ oder _NNNN_NNNN_
|
m = _re.search(r"[_-](\d{4})-(\d{2,4})(?:[-_]|\.)", filename)
|
||||||
m = _re.search(r"[_-](\d{4})[-_](\d{4})(?:[-_]|\.)", filename)
|
|
||||||
if not m: return None
|
if not m: return None
|
||||||
e_k = int(m.group(1)); n_k = int(m.group(2))
|
e_k = int(m.group(1)); n_k = int(m.group(2))
|
||||||
e_min = e_k * 1000.0
|
e_min = e_k * 1000.0
|
||||||
@@ -7026,36 +7026,115 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
plane_z = terr_max_z + z_offset
|
plane_z = terr_max_z + z_offset
|
||||||
self._push_log("→ {} Ortho-Tile(s), platziere Plane bei Z={:.3f}".format(
|
self._push_log("→ {} Ortho-Tile(s), platziere Plane bei Z={:.3f}".format(
|
||||||
len(ortho_paths), plane_z))
|
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 = []
|
ortho_objs = []
|
||||||
for ortho_path in ortho_paths:
|
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(
|
tile_bbox = _parse_swisstopo_tile_bbox(
|
||||||
os.path.basename(ortho_path))
|
os.path.basename(ortho_path))
|
||||||
if tile_bbox is None:
|
if tile_bbox is None:
|
||||||
self._push_log(" → Tile-bbox nicht ableitbar aus {}".format(
|
self._push_log(" → Tile-bbox nicht ableitbar aus {}".format(
|
||||||
os.path.basename(ortho_path)))
|
os.path.basename(ortho_path)))
|
||||||
continue
|
continue
|
||||||
|
tgt_idx = tile_layer_idx.get(ortho_path, -1)
|
||||||
try:
|
try:
|
||||||
obj = swisstopo.add_ortho_plane(
|
obj = swisstopo.add_ortho_plane(
|
||||||
d, ortho_path, tile_bbox,
|
d, ortho_path, tile_bbox,
|
||||||
origin_shift, m_to_unit, z_doc=plane_z)
|
origin_shift, m_to_unit, z_doc=plane_z,
|
||||||
if obj: ortho_objs.append(obj)
|
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:
|
except Exception as ex:
|
||||||
self._push_log("Ortho-Apply: {}".format(ex))
|
self._push_log("Ortho-Apply: {}".format(ex))
|
||||||
self._push_log("→ {} Ortho-Plane(s) erstellt".format(len(ortho_objs)))
|
self._push_log("→ {} Ortho-Plane(s) auf eigene Sub-Layer".format(
|
||||||
# Layer (gleicher Geschoss-Sublayer 80_swisstopo wie Terrain)
|
len(ortho_objs)))
|
||||||
if z_id and ortho_objs:
|
# End-Diagnose mit BBox-Koords damit wir sehen
|
||||||
sub_name = _find_ebene_sublayer_name(
|
# wo die Pictures tatsaechlich gelandet sind.
|
||||||
d, ["swisstopo", "gelaende_topo"],
|
try:
|
||||||
"80", "swisstopo",
|
diag = []
|
||||||
default_color="#909090", default_lw=0.18)
|
for o in d.Objects:
|
||||||
self._move_to_sublayer(d, ortho_objs, z_id,
|
if o is None or o.IsDeleted: continue
|
||||||
sub_name.split("_", 1)[0], tag="ortho",
|
tag = o.Attributes.GetUserString("dossier_swisstopo_kind")
|
||||||
fallback_name=sub_name,
|
if tag != "ortho": continue
|
||||||
fallback_color="#909090")
|
li = o.Attributes.LayerIndex
|
||||||
elif ortho_objs:
|
lay = d.Layers[li]
|
||||||
self._tag_objects(d, ortho_objs, "ortho")
|
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 ortho_objs)
|
||||||
new_obj_ids.extend(o.Id for o, _ in mesh_objects)
|
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)
|
doc.Objects.ModifyAttributes(o, attrs, True)
|
||||||
except Exception: pass
|
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,
|
def _ensure_sub_sublayer(self, doc, parent_id, name,
|
||||||
color_hex="#888888", lw=0.25):
|
color_hex="#888888", lw=0.25):
|
||||||
"""Findet oder erstellt einen Sub-Layer mit Name <name> direkt
|
"""Findet oder erstellt einen Sub-Layer mit Name <name> direkt
|
||||||
|
|||||||
+71
-34
@@ -18,6 +18,7 @@ import urllib.request
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import Rhino
|
import Rhino
|
||||||
import Rhino.Geometry as rg
|
import Rhino.Geometry as rg
|
||||||
|
import System
|
||||||
|
|
||||||
CACHE_DIR = os.path.expanduser("~/Library/Caches/Dossier/swisstopo")
|
CACHE_DIR = os.path.expanduser("~/Library/Caches/Dossier/swisstopo")
|
||||||
STAC_BASE = "https://data.geo.admin.ch/api/stac/v1"
|
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,
|
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,
|
"""Erzeugt eine planare Brep-Flaeche mit dem SWISSIMAGE-Foto als Material,
|
||||||
direkt sichtbar in Top/Shaded/Rendered Display-Mode.
|
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
|
x_max = (e_max - sx) * m_to_unit
|
||||||
y_min = (n_min - sy) * m_to_unit
|
y_min = (n_min - sy) * m_to_unit
|
||||||
y_max = (n_max - sy) * m_to_unit
|
y_max = (n_max - sy) * m_to_unit
|
||||||
# Mesh-Quad mit expliziten Per-Vertex-UV-Koordinaten — bombensicher
|
# Centered plane, mesh-based PictureFrame mit embedded Bitmap.
|
||||||
# fuer Cycles/Raytraced. Eine Brep-Plane braucht erst Render-Mesh-
|
# asMesh=True ist anders gerendert als Brep-Variante; bei Brep zeigte
|
||||||
# Erzeugung + TextureMapping, was diverse Fallstricke hat.
|
# Mac Rhino 8 die Textur in keinem Modus.
|
||||||
mesh = rg.Mesh()
|
cx = (x_min + x_max) / 2.0
|
||||||
mesh.Vertices.Add(x_min, y_min, z_doc) # 0 → UV (0,0)
|
cy = (y_min + y_max) / 2.0
|
||||||
mesh.Vertices.Add(x_max, y_min, z_doc) # 1 → UV (1,0)
|
width = abs(x_max - x_min)
|
||||||
mesh.Vertices.Add(x_max, y_max, z_doc) # 2 → UV (1,1)
|
height = abs(y_max - y_min)
|
||||||
mesh.Vertices.Add(x_min, y_max, z_doc) # 3 → UV (0,1)
|
plane = rg.Plane(rg.Point3d(cx, cy, z_doc),
|
||||||
mesh.Faces.AddFace(0, 1, 2, 3)
|
rg.Vector3d.XAxis, rg.Vector3d.YAxis)
|
||||||
mesh.TextureCoordinates.Add(0.0, 0.0)
|
try:
|
||||||
mesh.TextureCoordinates.Add(1.0, 0.0)
|
size_mb = os.path.getsize(ortho_path) / 1e6
|
||||||
mesh.TextureCoordinates.Add(1.0, 1.0)
|
print("[SWISSTOPO] PictureFrame src: {} ({:.1f} MB)".format(
|
||||||
mesh.TextureCoordinates.Add(0.0, 1.0)
|
os.path.basename(ortho_path), size_mb))
|
||||||
mesh.Normals.ComputeNormals()
|
except Exception:
|
||||||
mesh.Compact()
|
print("[SWISSTOPO] file nicht lesbar:", ortho_path)
|
||||||
gid = doc.Objects.AddMesh(mesh)
|
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)
|
obj = doc.Objects.Find(gid)
|
||||||
if obj is None: return None
|
if obj is None: return None
|
||||||
# Material: Legacy + ToPhysicallyBased + PBR_BaseColor-Texture.
|
# Auf Ziel-Layer schieben (nachträglich; Material bleibt auf Object).
|
||||||
# Bekannt instabil unter Mac Rhino 8 für Raytraced (Cycles greift den
|
if target_layer_idx >= 0:
|
||||||
# Shim nicht zuverlaessig); zumindest Shaded zeigt die Textur.
|
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:
|
try:
|
||||||
mat = Rhino.DocObjects.Material()
|
o2 = doc.Objects.Find(gid)
|
||||||
mat.Name = "swisstopo_ortho"
|
a = o2.Attributes
|
||||||
mat.SetBitmapTexture(ortho_path)
|
print("[SWISSTOPO] PictureFrame OK id={} layer='{}' MatSrc={} MatIdx={} hidden={}".format(
|
||||||
mat.ToPhysicallyBased()
|
gid, doc.Layers[a.LayerIndex].FullPath,
|
||||||
tex = Rhino.DocObjects.Texture()
|
a.MaterialSource, a.MaterialIndex, o2.IsHidden))
|
||||||
tex.FileName = ortho_path
|
# Material-Inspect
|
||||||
tex.Enabled = True
|
mat = None
|
||||||
mat.SetTexture(tex, Rhino.DocObjects.TextureType.PBR_BaseColor)
|
try:
|
||||||
midx = doc.Materials.Add(mat)
|
if a.MaterialIndex >= 0 and a.MaterialIndex < doc.Materials.Count:
|
||||||
attrs = obj.Attributes.Duplicate()
|
mat = doc.Materials[a.MaterialIndex]
|
||||||
attrs.MaterialSource = Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject
|
except Exception: pass
|
||||||
attrs.MaterialIndex = midx
|
if mat is not None:
|
||||||
doc.Objects.ModifyAttributes(obj, attrs, True)
|
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:
|
except Exception as ex:
|
||||||
print("[SWISSTOPO] ortho-material:", ex)
|
print("[SWISSTOPO] diag fail:", ex)
|
||||||
return obj
|
return obj
|
||||||
|
|||||||
+133
-103
@@ -21,10 +21,10 @@ const PRESETS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const VIEWS = [
|
const VIEWS = [
|
||||||
{ value: 'Top', icon: 'north', label: 'Top' },
|
{ value: 'Top', icon: 'view_quilt', label: 'Top' },
|
||||||
{ value: 'Front', icon: 'view_in_ar', label: 'Front' },
|
{ value: 'Front', icon: 'north', label: 'Front' },
|
||||||
{ value: 'Right', icon: 'east', label: 'Right' },
|
{ value: 'Right', icon: 'east', label: 'Right' },
|
||||||
{ value: 'Perspective', icon: 'view_quilt', label: 'Persp' },
|
{ value: 'Perspective', icon: 'view_in_ar', label: 'Persp' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function fmtScale(s) {
|
function fmtScale(s) {
|
||||||
@@ -207,21 +207,37 @@ export default function OberleisteApp() {
|
|||||||
{/* === Toolbar (View, Display, Massstab, Snap, Overrides) === */}
|
{/* === Toolbar (View, Display, Massstab, Snap, Overrides) === */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 8,
|
display: 'flex', alignItems: 'center', gap: 8,
|
||||||
padding: '6px 12px',
|
padding: '4px 12px 8px',
|
||||||
overflowX: 'auto', overflowY: 'hidden',
|
overflowX: 'auto', overflowY: 'hidden',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
<span
|
{/* Logo: DOSSIER. (Petrol-Punkt) + Launcher-Version */}
|
||||||
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: 11, fontWeight: 600, letterSpacing: '0.08em',
|
display: 'flex', alignItems: 'baseline', gap: 8,
|
||||||
color: 'var(--text-muted)',
|
|
||||||
fontFamily: 'DM Mono, monospace',
|
|
||||||
flexShrink: 0, userSelect: 'none',
|
flexShrink: 0, userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
title={`Dossier ${__APP_VERSION__} — Teil von OpenStudio`}
|
title={`Dossier ${__LAUNCHER_VERSION__} (Plugin ${__APP_VERSION__}) — Teil von OpenStudio`}
|
||||||
>
|
>
|
||||||
DOSSIER <span style={{ opacity: 0.55 }}>{__APP_VERSION__}</span>
|
<span style={{
|
||||||
</span>
|
fontFamily: "Krungthep, 'Archivo Black', sans-serif",
|
||||||
|
fontSize: 18,
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}>
|
||||||
|
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontFamily: 'DM Mono, monospace',
|
||||||
|
fontSize: 9,
|
||||||
|
letterSpacing: '0.14em',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
}}>
|
||||||
|
v{__LAUNCHER_VERSION__}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => openDossierSettings()}
|
onClick={() => openDossierSettings()}
|
||||||
title="Dossier-Einstellungen"
|
title="Dossier-Einstellungen"
|
||||||
@@ -344,98 +360,112 @@ export default function OberleisteApp() {
|
|||||||
{/* Snap-Toggles (Ortho/Grid/OSnap) sind in Rhinos eigener Footer-Bar
|
{/* Snap-Toggles (Ortho/Grid/OSnap) sind in Rhinos eigener Footer-Bar
|
||||||
schon vorhanden — hier rausgenommen um Doppelung zu vermeiden. */}
|
schon vorhanden — hier rausgenommen um Doppelung zu vermeiden. */}
|
||||||
|
|
||||||
{/* ====== GRUPPE: OVERRIDES ====== */}
|
{/* ====== STACK: Overrides + Kombi uebereinander ======
|
||||||
<span style={groupLabel}>Overrides</span>
|
Beide Zeilen haben identisches Spalten-Layout (Label-Spalte fix,
|
||||||
<ToolButton
|
Dropdown gleich breit), damit Dropdowns vertikal aligned sind. */}
|
||||||
onClick={() => toggleOverrides(!state.overridesEnabled)}
|
{(() => {
|
||||||
active={state.overridesEnabled}
|
const STACK_LABEL_W = 60 // gleich breit fuer beide Zeilen
|
||||||
icon="auto_fix_high"
|
const STACK_DROPDOWN_W = 150
|
||||||
label={state.overridesEnabled ? 'AN' : 'AUS'}
|
const stackLabel = { ...groupLabel, width: STACK_LABEL_W,
|
||||||
title={state.overridesEnabled
|
padding: 0, textAlign: 'left' }
|
||||||
? `Grafische Overrides aktiv — klick zum Ausschalten`
|
return (
|
||||||
: `Grafische Overrides ausgeschaltet`}
|
<div style={{
|
||||||
/>
|
display: 'flex', flexDirection: 'column', gap: 4,
|
||||||
{/* Preset-Dropdown: aktive Kombination waehlen. "—" = keine Kombination
|
flexShrink: 0,
|
||||||
(Doc-Rules sind frei editiert oder leer). "Konfigurieren…" oeffnet
|
}}>
|
||||||
den grossen Regel-Editor (OVERRIDES-Panel). */}
|
{/* Overrides */}
|
||||||
<select
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
value={state.overridesActivePreset || '__none__'}
|
<span style={stackLabel}>Overrides</span>
|
||||||
onChange={(e) => {
|
<ToolButton
|
||||||
const v = e.target.value
|
onClick={() => toggleOverrides(!state.overridesEnabled)}
|
||||||
if (v === '__configure__') { openOverridesPanel(); return }
|
active={state.overridesEnabled}
|
||||||
setOverridesPreset(v === '__none__' ? null : v)
|
icon="auto_fix_high"
|
||||||
}}
|
label={state.overridesEnabled ? 'AN' : 'AUS'}
|
||||||
style={{ ...pillSelect, width: 140 }}
|
title={state.overridesEnabled
|
||||||
title={state.overridesActivePreset
|
? `Grafische Overrides aktiv — klick zum Ausschalten`
|
||||||
? `Aktives Preset: ${state.overridesActivePreset} (${state.overridesCount} Regeln)`
|
: `Grafische Overrides ausgeschaltet`}
|
||||||
: `Kein Preset aktiv (${state.overridesCount} Regeln, frei editiert)`}
|
/>
|
||||||
>
|
<select
|
||||||
<option value="__none__">{state.overridesCount > 0 ? `— (${state.overridesCount} Regeln)` : '—'}</option>
|
value={state.overridesActivePreset || '__none__'}
|
||||||
{(state.overridesPresets || []).map(name => (
|
onChange={(e) => {
|
||||||
<option key={name} value={name}>{name}</option>
|
const v = e.target.value
|
||||||
))}
|
if (v === '__configure__') { openOverridesPanel(); return }
|
||||||
<option disabled>──────────</option>
|
setOverridesPreset(v === '__none__' ? null : v)
|
||||||
<option value="__configure__">Konfigurieren…</option>
|
}}
|
||||||
</select>
|
style={{ ...pillSelect, width: STACK_DROPDOWN_W }}
|
||||||
<ToolButton
|
title={state.overridesActivePreset
|
||||||
onClick={openOverridesPanel}
|
? `Aktives Preset: ${state.overridesActivePreset} (${state.overridesCount} Regeln)`
|
||||||
icon="settings"
|
: `Kein Preset aktiv (${state.overridesCount} Regeln, frei editiert)`}
|
||||||
title="Overrides-Regel-Editor öffnen"
|
>
|
||||||
/>
|
<option value="__none__">{state.overridesCount > 0 ? `— (${state.overridesCount} Regeln)` : '—'}</option>
|
||||||
|
{(state.overridesPresets || []).map(name => (
|
||||||
<div style={sep} />
|
<option key={name} value={name}>{name}</option>
|
||||||
|
))}
|
||||||
{/* ====== GRUPPE: EBENENKOMBINATION ====== */}
|
<option disabled>──────────</option>
|
||||||
<span style={groupLabel}>Kombi</span>
|
<option value="__configure__">Konfigurieren…</option>
|
||||||
<select
|
</select>
|
||||||
value={state.layerCombinationActive || '__none__'}
|
<ToolButton
|
||||||
onChange={(e) => {
|
onClick={openOverridesPanel}
|
||||||
const v = e.target.value
|
icon="settings"
|
||||||
if (v === '__configure__') { openLayerCombinationsDialog(); return }
|
title="Overrides-Regel-Editor öffnen"
|
||||||
if (v === '__delete__') {
|
/>
|
||||||
if (state.layerCombinationActive &&
|
</div>
|
||||||
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
|
{/* Kombi */}
|
||||||
deleteLayerCombination(state.layerCombinationActive)
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
return
|
<span style={stackLabel}>Kombi</span>
|
||||||
}
|
<ToolButton
|
||||||
pickLayerCombination(v === '__none__' ? null : v)
|
onClick={() => {
|
||||||
}}
|
const suggested = state.layerCombinationActive
|
||||||
style={{ ...pillSelect, width: 140 }}
|
|| `Kombi ${(state.layerCombinations || []).length + 1}`
|
||||||
title={state.layerCombinationActive
|
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
|
||||||
? `Aktive Kombi: ${state.layerCombinationActive}`
|
if (!name) return
|
||||||
: 'Keine Kombination — manuelle Sichtbarkeit'}
|
if ((state.layerCombinations || []).includes(name) &&
|
||||||
>
|
!window.confirm(`"${name}" überschreiben?`)) return
|
||||||
<option value="__none__">— Eigene —</option>
|
saveLayerCombination(name)
|
||||||
{(state.layerCombinations || []).map(name => (
|
}}
|
||||||
<option key={name} value={name}>{name}</option>
|
icon="add"
|
||||||
))}
|
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
||||||
{state.layerCombinationActive && (
|
/>
|
||||||
<>
|
<select
|
||||||
<option disabled>──────────</option>
|
value={state.layerCombinationActive || '__none__'}
|
||||||
<option value="__delete__">🗑 Aktuelle löschen</option>
|
onChange={(e) => {
|
||||||
</>
|
const v = e.target.value
|
||||||
)}
|
if (v === '__configure__') { openLayerCombinationsDialog(); return }
|
||||||
<option disabled>──────────</option>
|
if (v === '__delete__') {
|
||||||
<option value="__configure__">Bearbeiten…</option>
|
if (state.layerCombinationActive &&
|
||||||
</select>
|
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
|
||||||
<ToolButton
|
deleteLayerCombination(state.layerCombinationActive)
|
||||||
onClick={() => {
|
return
|
||||||
const suggested = state.layerCombinationActive
|
}
|
||||||
|| `Kombi ${(state.layerCombinations || []).length + 1}`
|
pickLayerCombination(v === '__none__' ? null : v)
|
||||||
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
|
}}
|
||||||
if (!name) return
|
style={{ ...pillSelect, width: STACK_DROPDOWN_W }}
|
||||||
if ((state.layerCombinations || []).includes(name) &&
|
title={state.layerCombinationActive
|
||||||
!window.confirm(`"${name}" überschreiben?`)) return
|
? `Aktive Kombi: ${state.layerCombinationActive}`
|
||||||
saveLayerCombination(name)
|
: 'Keine Kombination — manuelle Sichtbarkeit'}
|
||||||
}}
|
>
|
||||||
icon="add"
|
<option value="__none__">— Eigene —</option>
|
||||||
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
{(state.layerCombinations || []).map(name => (
|
||||||
/>
|
<option key={name} value={name}>{name}</option>
|
||||||
<ToolButton
|
))}
|
||||||
onClick={openLayerCombinationsDialog}
|
{state.layerCombinationActive && (
|
||||||
icon="edit"
|
<>
|
||||||
title="Ebenenkombinationen bearbeiten"
|
<option disabled>──────────</option>
|
||||||
/>
|
<option value="__delete__">🗑 Aktuelle löschen</option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
<option value="__configure__">Bearbeiten…</option>
|
||||||
|
</select>
|
||||||
|
<ToolButton
|
||||||
|
onClick={openLayerCombinationsDialog}
|
||||||
|
icon="edit"
|
||||||
|
title="Ebenenkombinationen bearbeiten"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Spacer am rechten Rand */}
|
{/* Spacer am rechten Rand */}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
|
|||||||
+3
-1
@@ -1,12 +1,14 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import pkg from './package.json' with { type: 'json' }
|
import pkg from './package.json' with { type: 'json' }
|
||||||
|
import launcherPkg from './launcher/package.json' with { type: 'json' }
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
base: './', // relative paths so file:// URLs in WKWebView funktionieren
|
base: './', // relative paths so file:// URLs in WKWebView funktionieren
|
||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(pkg.version),
|
__APP_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
__LAUNCHER_VERSION__: JSON.stringify(launcherPkg.version),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user