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:
|
||||
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
|
||||
|
||||
+71
-34
@@ -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:
|
||||
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)
|
||||
at = obj.Attributes.Duplicate()
|
||||
at.LayerIndex = target_layer_idx
|
||||
doc.Objects.ModifyAttributes(obj, at, True)
|
||||
except Exception as ex:
|
||||
print("[SWISSTOPO] ortho-material:", ex)
|
||||
print("[SWISSTOPO] Layer-Move fail:", ex)
|
||||
# Diagnose: hat das Material tatsaechlich eine Bitmap-Textur drin?
|
||||
try:
|
||||
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] diag fail:", ex)
|
||||
return obj
|
||||
|
||||
+65
-35
@@ -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) === */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 12px',
|
||||
padding: '4px 12px 8px',
|
||||
overflowX: 'auto', overflowY: 'hidden',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span
|
||||
{/* Logo: DOSSIER. (Petrol-Punkt) + Launcher-Version */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11, fontWeight: 600, letterSpacing: '0.08em',
|
||||
color: 'var(--text-muted)',
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
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={{
|
||||
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
|
||||
onClick={() => openDossierSettings()}
|
||||
title="Dossier-Einstellungen"
|
||||
@@ -344,8 +360,22 @@ export default function OberleisteApp() {
|
||||
{/* Snap-Toggles (Ortho/Grid/OSnap) sind in Rhinos eigener Footer-Bar
|
||||
schon vorhanden — hier rausgenommen um Doppelung zu vermeiden. */}
|
||||
|
||||
{/* ====== GRUPPE: OVERRIDES ====== */}
|
||||
<span style={groupLabel}>Overrides</span>
|
||||
{/* ====== STACK: Overrides + Kombi uebereinander ======
|
||||
Beide Zeilen haben identisches Spalten-Layout (Label-Spalte fix,
|
||||
Dropdown gleich breit), damit Dropdowns vertikal aligned sind. */}
|
||||
{(() => {
|
||||
const STACK_LABEL_W = 60 // gleich breit fuer beide Zeilen
|
||||
const STACK_DROPDOWN_W = 150
|
||||
const stackLabel = { ...groupLabel, width: STACK_LABEL_W,
|
||||
padding: 0, textAlign: 'left' }
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 4,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{/* Overrides */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={stackLabel}>Overrides</span>
|
||||
<ToolButton
|
||||
onClick={() => toggleOverrides(!state.overridesEnabled)}
|
||||
active={state.overridesEnabled}
|
||||
@@ -355,9 +385,6 @@ export default function OberleisteApp() {
|
||||
? `Grafische Overrides aktiv — klick zum Ausschalten`
|
||||
: `Grafische Overrides ausgeschaltet`}
|
||||
/>
|
||||
{/* Preset-Dropdown: aktive Kombination waehlen. "—" = keine Kombination
|
||||
(Doc-Rules sind frei editiert oder leer). "Konfigurieren…" oeffnet
|
||||
den grossen Regel-Editor (OVERRIDES-Panel). */}
|
||||
<select
|
||||
value={state.overridesActivePreset || '__none__'}
|
||||
onChange={(e) => {
|
||||
@@ -365,7 +392,7 @@ export default function OberleisteApp() {
|
||||
if (v === '__configure__') { openOverridesPanel(); return }
|
||||
setOverridesPreset(v === '__none__' ? null : v)
|
||||
}}
|
||||
style={{ ...pillSelect, width: 140 }}
|
||||
style={{ ...pillSelect, width: STACK_DROPDOWN_W }}
|
||||
title={state.overridesActivePreset
|
||||
? `Aktives Preset: ${state.overridesActivePreset} (${state.overridesCount} Regeln)`
|
||||
: `Kein Preset aktiv (${state.overridesCount} Regeln, frei editiert)`}
|
||||
@@ -382,11 +409,23 @@ export default function OberleisteApp() {
|
||||
icon="settings"
|
||||
title="Overrides-Regel-Editor öffnen"
|
||||
/>
|
||||
|
||||
<div style={sep} />
|
||||
|
||||
{/* ====== GRUPPE: EBENENKOMBINATION ====== */}
|
||||
<span style={groupLabel}>Kombi</span>
|
||||
</div>
|
||||
{/* Kombi */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={stackLabel}>Kombi</span>
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
const suggested = state.layerCombinationActive
|
||||
|| `Kombi ${(state.layerCombinations || []).length + 1}`
|
||||
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
|
||||
if (!name) return
|
||||
if ((state.layerCombinations || []).includes(name) &&
|
||||
!window.confirm(`"${name}" überschreiben?`)) return
|
||||
saveLayerCombination(name)
|
||||
}}
|
||||
icon="add"
|
||||
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
||||
/>
|
||||
<select
|
||||
value={state.layerCombinationActive || '__none__'}
|
||||
onChange={(e) => {
|
||||
@@ -400,7 +439,7 @@ export default function OberleisteApp() {
|
||||
}
|
||||
pickLayerCombination(v === '__none__' ? null : v)
|
||||
}}
|
||||
style={{ ...pillSelect, width: 140 }}
|
||||
style={{ ...pillSelect, width: STACK_DROPDOWN_W }}
|
||||
title={state.layerCombinationActive
|
||||
? `Aktive Kombi: ${state.layerCombinationActive}`
|
||||
: 'Keine Kombination — manuelle Sichtbarkeit'}
|
||||
@@ -418,24 +457,15 @@ export default function OberleisteApp() {
|
||||
<option disabled>──────────</option>
|
||||
<option value="__configure__">Bearbeiten…</option>
|
||||
</select>
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
const suggested = state.layerCombinationActive
|
||||
|| `Kombi ${(state.layerCombinations || []).length + 1}`
|
||||
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
|
||||
if (!name) return
|
||||
if ((state.layerCombinations || []).includes(name) &&
|
||||
!window.confirm(`"${name}" überschreiben?`)) return
|
||||
saveLayerCombination(name)
|
||||
}}
|
||||
icon="add"
|
||||
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
||||
/>
|
||||
<ToolButton
|
||||
onClick={openLayerCombinationsDialog}
|
||||
icon="edit"
|
||||
title="Ebenenkombinationen bearbeiten"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Spacer am rechten Rand */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import pkg from './package.json' with { type: 'json' }
|
||||
import launcherPkg from './launcher/package.json' with { type: 'json' }
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -8,5 +9,6 @@ export default defineConfig({
|
||||
base: './', // relative paths so file:// URLs in WKWebView funktionieren
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(pkg.version),
|
||||
__LAUNCHER_VERSION__: JSON.stringify(launcherPkg.version),
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user