Treppen 2D-Plansymbol + Pure-Transform fuer hidden Layer

Frontend:
- 2D-Plansymbol pro Treppe (Tritte/Lauflinie/Aussenlinie/Bruchsymbol)
  mit per-Treppe-Toggles in Properties-Panel
- 'Obere Stufen gestrichelt'-Toggle splittet Tritte/Aussenlinie an
  Schnittebene; Lauflinie hat zwei Pfeile bei Bruch
- Wand-Polyline-Grips fuer alle Vertices (nicht nur Enden)
- PopupMenu unterstuetzt Divider + Checkbox-Items

Backend:
- Eigener Layer 41_Treppen_2D fuer Plansymbol, Layer-Default schwarz
- Aussenlinie-Polygone folgen der Bruch-Diagonale (kein Versatz mehr)
- Linetype-Fallback laedt Dashed bei Bedarf nach
- Tritten-immer-an (Toggle entfernt), Z auf Geschoss-OKFF
- TREPPEN/RAEUME Layer-Migration auf Capital-Case (Treppen/Raeume)
- Selection-Partnership: treppe_2d_symbol pairs in axis + volume

Pure-Transform fuer Treppen-Move:
- treppe_2d_symbol + treppe_volume in VOLUME_TYPES → cascade-Support
- Phase 1.5 Volume-only-Detection: wenn Source unbewegt aber Volumes
  uniform translated → synthetisiere canonical aus Avg-Delta der
  bewegten Volumes (unbewegte rausgefiltert sonst Verzerrung)
- Hidden-inclusive ObjectEnumerator in Snapshot + Apply-Loop damit
  hidden treppe_axis auf 40_Treppen mit-transformiert wird
- Properties-Fallback im _send_state findet hidden Sources via
  expliziter Iteration → Panel zeigt Treppe auch bei 3D-Layer aus
- Dimensionen-Panel skipt on_select/idle waehrend UT_ACTIVE oder
  Partnership-Cascade → keine Flicker beim Drag mehr
This commit is contained in:
2026-05-28 00:41:05 +02:00
parent d9589e99f5
commit bcf7d557b1
5 changed files with 1172 additions and 107 deletions
+13
View File
@@ -586,6 +586,10 @@ def _install_listeners(bridge):
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem # tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
# Tick zwischen den einzelnen Deletes. CommandEnd refresht. # Tick zwischen den einzelnen Deletes. CommandEnd refresht.
if sc.sticky.get("_dossier_bulk_op_active"): return if sc.sticky.get("_dossier_bulk_op_active"): return
# Waehrend Gumball/Move/Rotate: nicht pollen. Geometrie ist gerade
# in Transit (Live-Replace pro Frame), Werte wuerden mit ~5/s
# zwischen Frames flickern. CommandEnd triggert finalen _send_state.
if sc.sticky.get("_dossier_user_transform_active"): return
b = sc.sticky.get("dimensionen_bridge") b = sc.sticky.get("dimensionen_bridge")
if b is not None: if b is not None:
try: b.tick_idle() try: b.tick_idle()
@@ -595,6 +599,15 @@ def _install_listeners(bridge):
# Swisstopo-Import feuert tausende Selection-Events → bail. # Swisstopo-Import feuert tausende Selection-Events → bail.
if sc.sticky.get("dossier_swisstopo_busy"): return if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get("_dossier_bulk_op_active"): return if sc.sticky.get("_dossier_bulk_op_active"): return
# Waehrend elemente.py's Partnership-Cascade (Klick auf Wand/Treppe
# → 30+ Partner selektiert in einem Rutsch): NICHT pro Event ein
# _send_state feuern. Sonst rauscht das Dimensionen-Panel mit 30+
# Re-Renders durch und die Werte/Auswahl-Anzeige flickert wild.
# Der Idle-Tick holt die finale Selektion eh ~5/s nach.
if sc.sticky.get("_elemente_select_busy"): return
# Waehrend User-Transform (Gumball/Move/Rotate): kein Re-Send, sonst
# rauscht Replace-Storm durch und der Frontend-State zappelt.
if sc.sticky.get("_dossier_user_transform_active"): return
b = sc.sticky.get("dimensionen_bridge") b = sc.sticky.get("dimensionen_bridge")
if b is not None: if b is not None:
try: b._send_state(force=True) try: b._send_state(force=True)
+1014 -21
View File
File diff suppressed because it is too large Load Diff
+87 -81
View File
@@ -96,53 +96,44 @@ def _find_axis_for_obj(doc, obj):
return None return None
def _curve_endpoints(curve): def _axis_vertices(geom):
"""Liefert (start_pt, end_pt) fuer eine wand_axis. Funktioniert fuer """Liefert die Vertices der wand_axis-Curve als Liste.
LineCurve, PolylineCurve, NurbsCurve etc — alle Curve-Typen haben - PolylineCurve: alle Vertices
PointAtStart/PointAtEnd. Bei degenerierten Curves None.""" - LineCurve / sonstige Curve: [Start, End] (zwei-Vertex-Faelle)
if curve is None: return None, None Returnt [] bei degeneriertem Input."""
if geom is None: return []
try: try:
return curve.PointAtStart, curve.PointAtEnd if isinstance(geom, rg.PolylineCurve):
poly = geom.ToPolyline()
if poly is None or poly.Count < 2: return []
return list(poly)
p_start = geom.PointAtStart
p_end = geom.PointAtEnd
return [p_start, p_end]
except Exception: except Exception:
return None, None return []
def _replace_axis_endpoint(doc, axis_obj, kind, new_pt): def _replace_axis_vertex(doc, axis_obj, vertex_idx, new_pt):
"""Tauscht den Start- (kind='start') oder Endpunkt (kind='end') der """Tauscht den Vertex an Index `vertex_idx` der wand_axis-Curve gegen
wand_axis-Curve gegen new_pt. Geht intelligent um mit: new_pt. Funktioniert fuer Linien (idx 0/1) und Polylinien (alle idx).
- LineCurve: erzeuge neue Line vom fixen Punkt zum neuen Punkt Setzt die neue Geometrie via Objects.Replace — feuert
- PolylineCurve: ersetze ersten/letzten Vertex, Rest bleibt
- andere Curve-Typen: aktuell nur Line-Fallback (Erst/Letzt-Vertex
rekonstruieren)
Setzt die neue Geometrie via Objects.Replace — das feuert
ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft.""" ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft."""
if axis_obj is None or axis_obj.IsDeleted: return False if axis_obj is None or axis_obj.IsDeleted: return False
geom = axis_obj.Geometry geom = axis_obj.Geometry
if geom is None: return False if geom is None: return False
try: try:
# PolylineCurve mit > 2 Vertices: ersten/letzten Vertex ersetzen pts = _axis_vertices(geom)
if isinstance(geom, rg.PolylineCurve): if not pts: return False
poly = geom.ToPolyline() if vertex_idx < 0 or vertex_idx >= len(pts): return False
if poly is None or poly.Count < 2: return False pts[vertex_idx] = new_pt
pts = list(poly) if len(pts) == 2:
if kind == "start": new_curve = rg.LineCurve(pts[0], pts[1])
pts[0] = new_pt
else:
pts[-1] = new_pt
new_poly = rg.Polyline(pts)
new_curve = rg.PolylineCurve(new_poly)
else: else:
# LineCurve oder unbekannter Typ → reduziere auf Line zwischen new_curve = rg.PolylineCurve(rg.Polyline(pts))
# neuem + altem fixen Punkt.
p_start, p_end = _curve_endpoints(geom)
if p_start is None or p_end is None: return False
if kind == "start":
new_curve = rg.LineCurve(new_pt, p_end)
else:
new_curve = rg.LineCurve(p_start, new_pt)
return doc.Objects.Replace(axis_obj.Id, new_curve) return doc.Objects.Replace(axis_obj.Id, new_curve)
except Exception as ex: except Exception as ex:
print("[WAND_GRIPS] replace endpoint:", ex) print("[WAND_GRIPS] replace vertex:", ex)
return False return False
@@ -155,15 +146,17 @@ class _EndpointConduit(rd.DisplayConduit):
def __init__(self): def __init__(self):
rd.DisplayConduit.__init__(self) rd.DisplayConduit.__init__(self)
self.hot_key = None # (axis_id_str, kind) — fuer Hover self.hot_key = None # (axis_id_str, vidx) — fuer Hover
self.drag_key = None # (axis_id_str, kind) — waehrend aktivem Drag self.drag_key = None # (axis_id_str, vidx) — waehrend aktivem Drag
self.drag_preview = None # rg.Line — Live-Vorschau waehrend GetPoint self.drag_preview = None # Liste von rg.Line — Live-Vorschau (Linien
# zu Nachbar-Vertices waehrend GetPoint)
def _collect_endpoints(self, doc): def _collect_grip_points(self, doc):
"""Liefert Liste von (axis_obj, kind, world_pt) fuer alle selektier- """Liefert Liste von (axis_obj, vertex_idx, world_pt) fuer ALLE
ten Waende. Iteriert die Selektion + dedupliziert Achsen (jede Vertices aller selektierten Waende — fuer Polyline-Waende ist
Wand erscheint nur einmal, auch wenn mehrere Volumen mit-selek- jeder Knick ein eigener Grip. Iteriert die Selektion + dedupli-
tiert sind).""" ziert Achsen (jede Wand erscheint nur einmal, auch wenn mehrere
Volumen mit-selektiert sind)."""
out = [] out = []
seen_axis = set() seen_axis = set()
try: try:
@@ -175,24 +168,21 @@ class _EndpointConduit(rd.DisplayConduit):
aid = str(axis.Id) aid = str(axis.Id)
if aid in seen_axis: continue if aid in seen_axis: continue
seen_axis.add(aid) seen_axis.add(aid)
p_start, p_end = _curve_endpoints(axis.Geometry) for i, pt in enumerate(_axis_vertices(axis.Geometry)):
if p_start is not None: out.append((axis, i, pt))
out.append((axis, "start", p_start))
if p_end is not None:
out.append((axis, "end", p_end))
return out return out
def DrawForeground(self, e): def DrawForeground(self, e):
try: try:
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return if doc is None: return
for axis, kind, pt in self._collect_endpoints(doc): for axis, vidx, pt in self._collect_grip_points(doc):
aid = str(axis.Id) aid = str(axis.Id)
# Skip den gerade gezogenen Marker — der wird via # Skip den gerade gezogenen Marker — der wird via
# drag_preview separat dargestellt. # drag_preview separat dargestellt.
if self.drag_key and self.drag_key == (aid, kind): if self.drag_key and self.drag_key == (aid, vidx):
continue continue
is_hot = self.hot_key and self.hot_key == (aid, kind) is_hot = self.hot_key and self.hot_key == (aid, vidx)
r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX
fill = _MARKER_HOVER if is_hot else _MARKER_FILL fill = _MARKER_HOVER if is_hot else _MARKER_FILL
# DrawPoint mit RoundControlPoint = gefuellter Kreis + # DrawPoint mit RoundControlPoint = gefuellter Kreis +
@@ -204,11 +194,12 @@ class _EndpointConduit(rd.DisplayConduit):
# Fallback fuer aeltere Rhino-Versionen: einfacher # Fallback fuer aeltere Rhino-Versionen: einfacher
# DrawDot mit Label "●" # DrawDot mit Label "●"
e.Display.DrawDot(pt, "", fill, _MARKER_BORDER) e.Display.DrawDot(pt, "", fill, _MARKER_BORDER)
# Drag-Preview-Linie waehrend GetPoint aktiv ist # Drag-Preview-Linien waehrend GetPoint aktiv ist
if self.drag_preview is not None: if self.drag_preview:
try: for line in self.drag_preview:
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2) try:
except Exception: pass e.Display.DrawLine(line, _MARKER_HOVER, 2)
except Exception: pass
except Exception as ex: except Exception as ex:
print("[WAND_GRIPS] DrawForeground:", ex) print("[WAND_GRIPS] DrawForeground:", ex)
@@ -226,21 +217,21 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point
def _hit_test(self, view, screen_pt): def _hit_test(self, view, screen_pt):
"""Liefert (axis, kind, world_pt) wenn screen_pt nahe eines Endpoint- """Liefert (axis, vertex_idx, world_pt) wenn screen_pt nahe eines
Markers liegt, sonst None. Iteriert die aktuelle Conduit-Liste.""" Vertex-Markers liegt, sonst None."""
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return None if doc is None: return None
try: try:
vp = view.ActiveViewport vp = view.ActiveViewport
except Exception: return None except Exception: return None
thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX
for axis, kind, world_pt in self.conduit._collect_endpoints(doc): for axis, vidx, world_pt in self.conduit._collect_grip_points(doc):
try: try:
s = vp.WorldToClient(world_pt) s = vp.WorldToClient(world_pt)
dx = s.X - screen_pt.X dx = s.X - screen_pt.X
dy = s.Y - screen_pt.Y dy = s.Y - screen_pt.Y
if (dx * dx + dy * dy) <= thresh2: if (dx * dx + dy * dy) <= thresh2:
return axis, kind, world_pt return axis, vidx, world_pt
except Exception: continue except Exception: continue
return None return None
@@ -274,44 +265,59 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
# Default-Klick (Selection) abwuergen — wir uebernehmen # Default-Klick (Selection) abwuergen — wir uebernehmen
try: e.Cancel = True try: e.Cancel = True
except Exception: pass except Exception: pass
axis, kind, world_pt = hit axis, vidx, world_pt = hit
self._start_drag(view.Document, axis, kind, world_pt) self._start_drag(view.Document, axis, vidx, world_pt)
except Exception as ex: except Exception as ex:
print("[WAND_GRIPS] OnMouseDown:", ex) print("[WAND_GRIPS] OnMouseDown:", ex)
def _start_drag(self, doc, axis, kind, anchor_pt): def _start_drag(self, doc, axis, vertex_idx, anchor_pt):
"""Startet eine Rhino-GetPoint-Interaktion um den neuen Endpunkt """Startet eine Rhino-GetPoint-Interaktion um den Vertex zu
zu picken. Der ANDERE Endpunkt (Fix-Punkt) wird als BasePoint verschieben. BasePoint-Strategie:
gesetzt — damit kriegt der User Tracking-Linie, Ortho-Mode etc. - End-Vertex (idx 0 oder letzter): gegenueberliegender End-Vertex
wie bei _Move.""" → User bekommt Tracking-Linie + Wand-Laenge wie bei _Move
- Mittel-Vertex (Polyline-Knick): Vertex selbst, plus Live-Preview
zu beiden Nachbar-Vertices damit beide Segmente sichtbar mit-
schwingen."""
if doc is None: return if doc is None: return
geom = axis.Geometry geom = axis.Geometry
if geom is None: return if geom is None: return
p_start, p_end = _curve_endpoints(geom) pts = _axis_vertices(geom)
if p_start is None or p_end is None: return if not pts or vertex_idx < 0 or vertex_idx >= len(pts): return
fixed_pt = p_end if kind == "start" else p_start is_first = vertex_idx == 0
# Conduit-State: drag-Marker hervorheben + Preview-Linie is_last = vertex_idx == len(pts) - 1
self.conduit.drag_key = (str(axis.Id), kind) prev_pt = pts[vertex_idx - 1] if not is_first else None
self.conduit.drag_preview = rg.Line(fixed_pt, anchor_pt) next_pt = pts[vertex_idx + 1] if not is_last else None
if is_first: base_pt = next_pt
elif is_last: base_pt = prev_pt
else: base_pt = anchor_pt
# Conduit-State: drag-Marker hervorheben + Preview-Linien
self.conduit.drag_key = (str(axis.Id), vertex_idx)
self.conduit.drag_preview = []
if prev_pt is not None:
self.conduit.drag_preview.append(rg.Line(prev_pt, anchor_pt))
if next_pt is not None:
self.conduit.drag_preview.append(rg.Line(next_pt, anchor_pt))
self._busy = True self._busy = True
try: try:
gp = Rhino.Input.Custom.GetPoint() gp = Rhino.Input.Custom.GetPoint()
gp.SetCommandPrompt("Wand-Endpunkt: neuer Punkt (Esc=Abbruch)") gp.SetCommandPrompt("Wand-Vertex: neuer Punkt (Esc=Abbruch)")
gp.SetBasePoint(fixed_pt, True) gp.SetBasePoint(base_pt, True)
gp.DrawLineFromPoint(fixed_pt, True) gp.DrawLineFromPoint(base_pt, True)
# Live-Preview ueber Conduit (zusaetzlich zu Rhinos eigener
# Tracking-Linie) — sieht ueblich, hilft beim Verstehen welcher
# Endpunkt sich bewegt.
def _on_mouse_move(sender, args): def _on_mouse_move(sender, args):
try: try:
self.conduit.drag_preview = rg.Line(fixed_pt, args.Point) preview = []
if prev_pt is not None:
preview.append(rg.Line(prev_pt, args.Point))
if next_pt is not None:
preview.append(rg.Line(next_pt, args.Point))
self.conduit.drag_preview = preview
except Exception: pass except Exception: pass
try: gp.MouseMove += _on_mouse_move try: gp.MouseMove += _on_mouse_move
except Exception: pass except Exception: pass
res = gp.Get() res = gp.Get()
if res == Rhino.Input.GetResult.Point: if res == Rhino.Input.GetResult.Point:
new_pt = gp.Point() new_pt = gp.Point()
_replace_axis_endpoint(doc, axis, kind, new_pt) _replace_axis_vertex(doc, axis, vertex_idx, new_pt)
except Exception as ex: except Exception as ex:
print("[WAND_GRIPS] _start_drag:", ex) print("[WAND_GRIPS] _start_drag:", ex)
finally: finally:
+57 -5
View File
@@ -6,7 +6,7 @@ import { BarToggle, BarButton, BarCombo } from './components/BarControls'
import { import {
onMessage, notifyReady, onMessage, notifyReady,
createWall, createDecke, createDach, createWall, createDecke, createDach,
createFenster, createTuer, createAussparung, createTreppe, createFenster, createTuer, createAussparung, createTreppe, setTreppe2DShow,
createStuetze, createTraeger, createRaum, createStempel, createStuetze, createTraeger, createRaum, createStempel,
openSwisstopo, openSwisstopoDialog, openOsmDialog, openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, openElementeUebersicht, openElementeProperties, updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
@@ -136,9 +136,15 @@ function PopupMenu({ items, onClose }) {
zIndex: 100, zIndex: 100,
minWidth: 140, minWidth: 140,
}}> }}>
{items.map((it, i) => ( {items.map((it, i) => it._divider ? (
<div key={i} style={{
height: 1, margin: '4px 2px',
background: 'var(--border)', opacity: 0.6,
}} />
) : (
<button key={i} <button key={i}
onClick={(e) => { e.stopPropagation(); it.onClick(); onClose() }} onClick={(e) => { e.stopPropagation(); it.onClick();
if (!it.keepOpen) onClose() }}
disabled={it.disabled} disabled={it.disabled}
title={it.hint || ''} title={it.hint || ''}
style={{ style={{
@@ -157,7 +163,10 @@ function PopupMenu({ items, onClose }) {
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent' e.currentTarget.style.background = 'transparent'
}}> }}>
{it.icon && <Icon name={it.icon} size={12} {it.checked !== undefined ? (
<Icon name={it.checked ? 'check_box' : 'check_box_outline_blank'}
size={12} style={{ color: 'var(--accent)' }} />
) : it.icon && <Icon name={it.icon} size={12}
style={{ color: 'var(--accent)' }} />} style={{ color: 'var(--accent)' }} />}
<span style={{ flex: 1 }}>{it.label}</span> <span style={{ flex: 1 }}>{it.label}</span>
{it.badge && ( {it.badge && (
@@ -331,7 +340,7 @@ function ElementListRow({ el, meta }) {
} }
function NeuesElementSection({ noGeschoss, activeName, elementsCount }) { function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DShow }) {
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false) const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false) const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false) const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
@@ -348,6 +357,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
const openStuetzeMenu = (e) => { e.preventDefault(); setStuetzeMenuOpen(true) } const openStuetzeMenu = (e) => { e.preventDefault(); setStuetzeMenuOpen(true) }
const openTraegerMenu = (e) => { e.preventDefault(); setTraegerMenuOpen(true) } const openTraegerMenu = (e) => { e.preventDefault(); setTraegerMenuOpen(true) }
const treppe2DOn = treppe2DShow !== false
const treppeItems = [ const treppeItems = [
{ icon: 'stairs', label: 'Gerade Treppe', { icon: 'stairs', label: 'Gerade Treppe',
hint: 'Lauflinie mit 2 Punkten', hint: 'Lauflinie mit 2 Punkten',
@@ -358,6 +368,10 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
{ icon: 'rotate_right', label: 'Wendeltreppe', { icon: 'rotate_right', label: 'Wendeltreppe',
hint: '3 Punkte: Mittelpunkt, Start-Lauflinie, End-Lauflinie', hint: '3 Punkte: Mittelpunkt, Start-Lauflinie, End-Lauflinie',
onClick: () => createTreppe({ treppeArt: 'wendel' }) }, onClick: () => createTreppe({ treppeArt: 'wendel' }) },
{ _divider: true },
{ checked: treppe2DOn, label: '2D-Plansymbol',
hint: 'Trittlinien + Auf-Pfeil auf Schnittebene zeichnen',
onClick: () => setTreppe2DShow(!treppe2DOn) },
] ]
const profilItems = (factory) => [ const profilItems = (factory) => [
@@ -604,6 +618,7 @@ export default function ElementeApp() {
noGeschoss={noGeschoss} noGeschoss={noGeschoss}
activeName={activeName} activeName={activeName}
elementsCount={elements.length} elementsCount={elements.length}
treppe2DShow={state.treppe2DShow}
/> />
</div> </div>
</div> </div>
@@ -2197,6 +2212,43 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</div> </div>
)} )}
{/* 2D-Plansymbol Bestandteile */}
<div style={{ height: 1, background: 'var(--border-light)', margin: '2px 0' }} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<span style={{ fontSize: 9, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
2D-Plansymbol
</span>
{[
['showLauflinie', 'Lauflinie'],
['showAussen', 'Außenlinie'],
['showBruch', 'Bruchsymbol'],
].map(([key, label]) => {
const on = treppe[key] !== false
return (
<label key={key} style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer',
}}>
<input type="checkbox" checked={on}
onChange={(e) => onUpdate({ [key]: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>{label}</span>
</label>
)
})}
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 10, cursor: 'pointer',
marginLeft: 18, color: 'var(--text-secondary)',
}} title="Tritte und Außenlinie oberhalb der Schnittebene gestrichelt anzeigen">
<input type="checkbox" checked={treppe.obereDashed !== false}
onChange={(e) => onUpdate({ obereDashed: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>Obere Stufen gestrichelt</span>
</label>
</div>
{/* Schrittmass-Tabelle: H (editierbar), S, A, 2S+A mit on/off + range */} {/* Schrittmass-Tabelle: H (editierbar), S, A, 2S+A mit on/off + range */}
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', gap: 3, display: 'flex', flexDirection: 'column', gap: 3,
+1
View File
@@ -389,6 +389,7 @@ export function createFenster(p) { send('CREATE_FENSTER', p || {}) }
export function createTuer(p) { send('CREATE_TUER', p || {}) } export function createTuer(p) { send('CREATE_TUER', p || {}) }
export function createAussparung(p) { send('CREATE_AUSSPARUNG', p || {}) } export function createAussparung(p) { send('CREATE_AUSSPARUNG', p || {}) }
export function createTreppe(p) { send('CREATE_TREPPE', p || {}) } export function createTreppe(p) { send('CREATE_TREPPE', p || {}) }
export function setTreppe2DShow(on) { send('SET_TREPPE_2D_SHOW', { on: !!on }) }
export function createStuetze(p) { send('CREATE_STUETZE', p || {}) } export function createStuetze(p) { send('CREATE_STUETZE', p || {}) }
export function createTraeger(p) { send('CREATE_TRAEGER', p || {}) } export function createTraeger(p) { send('CREATE_TRAEGER', p || {}) }
export function createRaum(p) { send('CREATE_RAUM', p || {}) } export function createRaum(p) { send('CREATE_RAUM', p || {}) }