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: 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
View File
@@ -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
View File
@@ -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
View File
@@ -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),
}, },
}) })