Compare commits

...

7 Commits

Author SHA1 Message Date
karim 7930705d01 L-Treppe: Schrittmass-Clamp bei Setzen + 3D-Podest Lage-aware
Bisher griff der Schrittmass-Clamp nur bei treppe_art=='gerade'.
Fuer L-Treppen jetzt:
- gp2 (Eck-Klick): clampt erste Lauf-Laenge auf [1-Stufe-min, (n-1)-Stufen-max]
- gp3 (End-Klick): schaetzt N1 aus erster Lauf-Laenge, clampt zweiten Lauf
  auf den S/A-konformen Bereich (mit cut_back kompensiert)

Volume-Fix _make_treppe_l_volume:
- cut_back am Eckpunkt war hardcoded half_b (= passt nur fuer Lage=mid).
  Fuer Lage=links/rechts wird die FULL breite cut-back gebraucht — sonst
  ueberlappen die Lauf-Volumen am Eckpunkt mit dem Podest falsch.
  Fix: cut_back = half_b if mid else breite.
2026-05-28 12:57:37 +02:00
karim d8966cc035 L-Treppe: 3-Punkt-Axis behalten + Referenz IMMER aussen
User-Feedback: L-Treppe bleibt 3-Punkt-Polyline (Start/Eck/End), Podest
ergibt sich aus dem Eckpunkt + Breite. Aber Referenz darf nie 'mid'
sein — sonst kollidieren die Laeufe am Eck. Constraint:
- _update_wall: bei treppe_art=='l' und tref=='mid' → 'links' erzwungen
- Frontend: REF_OPTIONS filtert 'mittig' raus wenn treppeArt=='l'

Dead-Code: _l_segments + _aussen_l_polygon + _lauflinie_l + _make_treppe_l_volume
behalten 4-Punkt-Handling als optionalen Pfad (gerade nicht erreicht weil
Creation immer 3-Punkt produziert) — schaden nicht, koennen spaeter
wieder aktiviert werden wenn Podest-als-Segment doch gewuenscht.

Schlafsession-Status: Trittmass-Lock + Lauflinie zentriert + Pfeile +
Cmd+Z + Pure-Transform fuer hidden Layer = alles in main.
2026-05-28 12:49:32 +02:00
karim e406e8d9b2 L-Aussen-Polygon: Z-Konsistenz fix + try/except defensive
_line_intersect_xy lieferte Z=0 was nicht zum Polygon-Z (=OKFF) passte,
PolylineCurve mit gemischten Z konnte fehlschlagen → 2D-Generierung
broken fuer L-Treppen. Fix: _at_z(p, fallback) Helper setzt Z explicit.
Plus try/except um die ganze Polygon-Konstruktion — Fallback auf
2-Rechteck-Variante bei Fehler.
2026-05-28 02:40:09 +02:00
karim bb64e4d41e Lock: Targets clearen bei Disable + Wendel-Sweep clamp auf 2π 2026-05-28 02:19:22 +02:00
karim 6060c74b17 Treppe L+Wendel: Lauflinie, Aussen-Polygon, Pfeil-voll, Lock + Cmd+Z
Cmd+Z:
- _update_wall wrapped in BeginUndoRecord/EndUndoRecord — sodass
  Property-Patches + Regen-Delete/Add als ein Undo-Schritt rueckgaengig

L-Treppe Aussenlinie:
- _aussen_l_polygon: sauberes 6-Punkt L-Polygon mit korrekt projizierten
  Ecken (Outer + Inner via Linien-Schnitt der mid-versetzten Seiten)
- _aussen_l: nutzt Polygon wenn kein Cut, faellt sonst zurueck auf
  per-Lauf Rechtecke mit Diagonal-Cut (wie bisher)

L-Treppe Lauflinie:
- ueber BEIDE Laeufe, mid-perp versetzt, Pfeil am Treppen-Ende
- Eck-Mitte als Linien-Schnitt der zwei versetzten Schaft-Linien →
  sauberer Übergang auch bei Lage=links/rechts (kein Versatz an der Ecke)
- 'voll'-Pfeil-Style mit wide-Offsets relativ zur Lauflinie

Wendel-Treppe:
- _lauflinie_wendel: 'voll'-Pfeil-Style mit r_inner/r_outer-Spitzen
  relativ zu r_mid (Radial-Offsets)

Trittmass-Lock auf alle Treppen-Arten:
- L: Beide Laeufe proportional skalieren (ratio = N*target_A / (L1+L2))
- Wendel: Sweep-Winkel anpassen (new_delta = sign × N*target_A/r_mid)
- Axis-Geometrie wird in-place via Replace ausgetauscht — Source moves
  fliessen in regulären Regen-Pfad ein
2026-05-28 02:16:57 +02:00
karim 970281e10a Treppen UX-Polish: Start-Z, Trittmass-Lock, Pfeil-Stile, Grips
Properties-Panel:
- Konsistentes 50px/1fr/14px Grid fuer alle Treppen-Rows
- Lage + Unten als Dropdown (lowercase Labels)
- Versatz: Dropdown (Geschoss-OKFF) oder eigenes Z mit Input + x-Button
- Ziel: gleich (Geschoss-Liste oder eigene Hoehe), Geschosse-Filter
  excludes das Start-Geschoss
- Start-Dropdown filtert auf okff < Ziel-Z (kein hoeheres Geschoss als
  Start waehlbar, beachtet auch eigene-Hoehe-Ziel)
- Stufen: Dropdown 2-40 (statt freie Eingabe), mit Lock nur S-konforme
  Werte
- Dropdowns nutzen System-Font (statt mono)
- Ausgrenzung 'Aussenlinie'-Toggle (Aussenlinie immer an)
- Pfeil-Style-Dropdown unter Lauflinie-Checkbox: klassisch / gefuellt
  (Solid-Hatch) / breit / voll (Spitzen bis Treppen-Aussenkanten)

Backend Treppe:
- Start-Z-Override via treppe_uk_over (m Offset relativ zu Geschoss-OKFF)
- 2D-Symbol bleibt auf OKFF (egal ob Versatz) — Symbol klebt am Boden
- Lauflinie-Schaft auf visuellen Treppen-Mittelpunkt versetzt
  (bei Lage=links/rechts), nicht mehr auf der Referenz-Achse
- Trittmass-Lock: treppe_lock_s + target_S/A. Beim Aktivieren werden
  S+A als Ziel gespeichert. Bei H-Change wird N=round(H/target_S)
  recomputed + Axis-Laenge auf N*target_A angepasst (gerade Treppen)
- Bruchsymbol-Toggle aus: ganze Treppe ungesplittet zeichnen
  (eff_cut_h=0 → kein Lower/Upper-Split)
- Treppen-Endpunkt-Marker (treppe_grips.py) — gruene Punkte an Start/
  Ende der Lauflinie, beachtet treppe_art (Wendel: poly[1]/poly[2])

Verdoppelungs-Fix:
- _find_target_volume skipt treppe_2d_symbol explicit (sind 2D-Curves,
  kein Volume). Vorher konnte Replace(curve, brep) fehlschlagen → das
  echte Treppen-Brep blieb stehen + neues kam dazu → Duplikat
- _find_objects_by_wall_id mit HiddenObjects+LockedObjects-Iterator,
  findet auch Objs auf hidden 3D-Layer
- Anti-Dup-Cleanup in _regenerate_element: bei mehreren treppe_volume
  mit gleicher element_id → alle ausser dem ersten loeschen

State-Pipeline:
- geschosse-Liste enthaelt jetzt okff+hoehe (fuer Frontend-Constraints)
- Treppe-State neu: ukOver, arrowStyle, lockS, targetS, targetA
- Hidden-Source-Fallback in _send_state findet auch Treppen wenn der
  3D-Layer aus ist (sodass Properties-Panel angezeigt wird)

Dimensionen-Panel:
- on_select + on_idle skippen waehrend Partnership-Cascade oder
  User-Transform — kein Flicker mehr beim Drag

Andere:
- Wand-Polyline-Vertex-Grips (alle Vertices, nicht nur Enden)
- PopupMenu unterstuetzt _divider + checked-Items
- TREPPEN/RAEUME Layer-Migration auf Capital-Case
- selection-partnership tolerant: hidden Source wird trotzdem in die
  Selection genommen (sonst kann Drag nicht durch Pure-Transform)
2026-05-28 02:09:38 +02:00
karim bcf7d557b1 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
2026-05-28 00:41:05 +02:00
6 changed files with 2136 additions and 215 deletions
+13
View File
@@ -586,6 +586,10 @@ def _install_listeners(bridge):
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
# Tick zwischen den einzelnen Deletes. CommandEnd refresht.
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")
if b is not None:
try: b.tick_idle()
@@ -595,6 +599,15 @@ def _install_listeners(bridge):
# Swisstopo-Import feuert tausende Selection-Events → bail.
if sc.sticky.get("dossier_swisstopo_busy"): 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")
if b is not None:
try: b._send_state(force=True)
+1657 -53
View File
File diff suppressed because it is too large Load Diff
+104
View File
@@ -0,0 +1,104 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
treppe_grips.py
Display-Conduit fuer gruene Endpunkt-Marker an Treppen-Achsen. Visuelle
Indikation wie bei Waenden, aber keine eigene Drag-Logik — der normale
Partnership-Cascade (elemente._on_select_objects) + Pure-Transform-Pfad
verschieben die Treppe bereits sauber.
Endpunkt-Logik pro Treppen-Art:
- gerade : PointAtStart, PointAtEnd der Linie
- L : poly[0] (Start), poly[2] (Ende) — poly[1] ist der Eck-Punkt
- Wendel : poly[1] (Start), poly[2] (Ende) — poly[0] ist Rotations-
zentrum, nicht der Treppen-Anfang
"""
import Rhino
import Rhino.Display as rd
import Rhino.Geometry as rg
import scriptcontext as sc
import System.Drawing as SD
_MARKER_RADIUS_PX = 7
_MARKER_FILL = SD.Color.FromArgb(220, 95, 168, 150) # petrol-gruen, gleich wie wand_grips
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
def _treppe_endpoints(axis_obj):
"""Liefert Liste von Point3d fuer Treppen-Start + -Ende. Beachtet
treppe_art (Wendel hat anderes Polyline-Schema)."""
if axis_obj is None or axis_obj.IsDeleted: return []
a = axis_obj.Attributes
if a.GetUserString("dossier_element_type") != "treppe_axis": return []
geom = axis_obj.Geometry
if not isinstance(geom, rg.Curve): return []
art = a.GetUserString("dossier_treppe_art") or "gerade"
try:
if art == "wendel":
ok, poly = geom.TryGetPolyline()
if not ok or poly is None or poly.Count != 3: return []
return [poly[1], poly[2]]
# gerade + L → Start- und End-Punkt der Curve sind die Treppen-Enden
return [geom.PointAtStart, geom.PointAtEnd]
except Exception:
return []
class _TreppeEndpointConduit(rd.DisplayConduit):
"""Zeichnet gruene Endpunkt-Marker an allen selektierten Treppen-Achsen."""
def DrawForeground(self, e):
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
sel = list(doc.Objects.GetSelectedObjects(False, False))
seen = set()
for obj in sel:
a = obj.Attributes
eid = a.GetUserString("dossier_element_id") or ""
if not eid or eid in seen: continue
# Source-Axis via element_id finden (kann anderer Obj sein
# wenn User nur Volume oder 2D-Symbol selektiert hat)
axis = None
for o in doc.Objects:
if o is None or o.IsDeleted: continue
try:
a2 = o.Attributes
if a2.GetUserString("dossier_element_id") == eid and \
a2.GetUserString("dossier_element_type") == "treppe_axis":
axis = o; break
except Exception: continue
if axis is None: continue
seen.add(eid)
for pt in _treppe_endpoints(axis):
try:
e.Display.DrawPoint(pt,
rd.PointStyle.RoundControlPoint,
_MARKER_RADIUS_PX, _MARKER_FILL)
except Exception:
try: e.Display.DrawDot(pt, "", _MARKER_FILL, _MARKER_BORDER)
except Exception: pass
except Exception as ex:
print("[TREPPE_GRIPS] DrawForeground:", ex)
_STICKY_CONDUIT = "_dossier_treppe_grips_conduit"
def install_handlers():
"""Idempotente Registrierung. Bei Modul-Reload alten Conduit zuerst
disablen, dann neuen anhaengen."""
try:
old = sc.sticky.get(_STICKY_CONDUIT)
if old is not None:
try: old.Enabled = False
except Exception: pass
conduit = _TreppeEndpointConduit()
conduit.Enabled = True
sc.sticky[_STICKY_CONDUIT] = conduit
print("[TREPPE_GRIPS] Endpoint-Conduit aktiv")
except Exception as ex:
print("[TREPPE_GRIPS] install:", ex)
+87 -81
View File
@@ -96,53 +96,44 @@ def _find_axis_for_obj(doc, obj):
return None
def _curve_endpoints(curve):
"""Liefert (start_pt, end_pt) fuer eine wand_axis. Funktioniert fuer
LineCurve, PolylineCurve, NurbsCurve etc — alle Curve-Typen haben
PointAtStart/PointAtEnd. Bei degenerierten Curves None."""
if curve is None: return None, None
def _axis_vertices(geom):
"""Liefert die Vertices der wand_axis-Curve als Liste.
- PolylineCurve: alle Vertices
- LineCurve / sonstige Curve: [Start, End] (zwei-Vertex-Faelle)
Returnt [] bei degeneriertem Input."""
if geom is None: return []
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:
return None, None
return []
def _replace_axis_endpoint(doc, axis_obj, kind, new_pt):
"""Tauscht den Start- (kind='start') oder Endpunkt (kind='end') der
wand_axis-Curve gegen new_pt. Geht intelligent um mit:
- LineCurve: erzeuge neue Line vom fixen Punkt zum neuen Punkt
- 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
def _replace_axis_vertex(doc, axis_obj, vertex_idx, new_pt):
"""Tauscht den Vertex an Index `vertex_idx` der wand_axis-Curve gegen
new_pt. Funktioniert fuer Linien (idx 0/1) und Polylinien (alle idx).
Setzt die neue Geometrie via Objects.Replace — feuert
ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft."""
if axis_obj is None or axis_obj.IsDeleted: return False
geom = axis_obj.Geometry
if geom is None: return False
try:
# PolylineCurve mit > 2 Vertices: ersten/letzten Vertex ersetzen
if isinstance(geom, rg.PolylineCurve):
poly = geom.ToPolyline()
if poly is None or poly.Count < 2: return False
pts = list(poly)
if kind == "start":
pts[0] = new_pt
else:
pts[-1] = new_pt
new_poly = rg.Polyline(pts)
new_curve = rg.PolylineCurve(new_poly)
pts = _axis_vertices(geom)
if not pts: return False
if vertex_idx < 0 or vertex_idx >= len(pts): return False
pts[vertex_idx] = new_pt
if len(pts) == 2:
new_curve = rg.LineCurve(pts[0], pts[1])
else:
# LineCurve oder unbekannter Typ → reduziere auf Line zwischen
# 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)
new_curve = rg.PolylineCurve(rg.Polyline(pts))
return doc.Objects.Replace(axis_obj.Id, new_curve)
except Exception as ex:
print("[WAND_GRIPS] replace endpoint:", ex)
print("[WAND_GRIPS] replace vertex:", ex)
return False
@@ -155,15 +146,17 @@ class _EndpointConduit(rd.DisplayConduit):
def __init__(self):
rd.DisplayConduit.__init__(self)
self.hot_key = None # (axis_id_str, kind) — fuer Hover
self.drag_key = None # (axis_id_str, kind) — waehrend aktivem Drag
self.drag_preview = None # rg.Line — Live-Vorschau waehrend GetPoint
self.hot_key = None # (axis_id_str, vidx) — fuer Hover
self.drag_key = None # (axis_id_str, vidx) — waehrend aktivem Drag
self.drag_preview = None # Liste von rg.Line — Live-Vorschau (Linien
# zu Nachbar-Vertices waehrend GetPoint)
def _collect_endpoints(self, doc):
"""Liefert Liste von (axis_obj, kind, world_pt) fuer alle selektier-
ten Waende. Iteriert die Selektion + dedupliziert Achsen (jede
Wand erscheint nur einmal, auch wenn mehrere Volumen mit-selek-
tiert sind)."""
def _collect_grip_points(self, doc):
"""Liefert Liste von (axis_obj, vertex_idx, world_pt) fuer ALLE
Vertices aller selektierten Waende — fuer Polyline-Waende ist
jeder Knick ein eigener Grip. Iteriert die Selektion + dedupli-
ziert Achsen (jede Wand erscheint nur einmal, auch wenn mehrere
Volumen mit-selektiert sind)."""
out = []
seen_axis = set()
try:
@@ -175,24 +168,21 @@ class _EndpointConduit(rd.DisplayConduit):
aid = str(axis.Id)
if aid in seen_axis: continue
seen_axis.add(aid)
p_start, p_end = _curve_endpoints(axis.Geometry)
if p_start is not None:
out.append((axis, "start", p_start))
if p_end is not None:
out.append((axis, "end", p_end))
for i, pt in enumerate(_axis_vertices(axis.Geometry)):
out.append((axis, i, pt))
return out
def DrawForeground(self, e):
try:
doc = Rhino.RhinoDoc.ActiveDoc
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)
# Skip den gerade gezogenen Marker — der wird via
# 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
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
fill = _MARKER_HOVER if is_hot else _MARKER_FILL
# DrawPoint mit RoundControlPoint = gefuellter Kreis +
@@ -204,11 +194,12 @@ class _EndpointConduit(rd.DisplayConduit):
# Fallback fuer aeltere Rhino-Versionen: einfacher
# DrawDot mit Label "●"
e.Display.DrawDot(pt, "", fill, _MARKER_BORDER)
# Drag-Preview-Linie waehrend GetPoint aktiv ist
if self.drag_preview is not None:
try:
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
except Exception: pass
# Drag-Preview-Linien waehrend GetPoint aktiv ist
if self.drag_preview:
for line in self.drag_preview:
try:
e.Display.DrawLine(line, _MARKER_HOVER, 2)
except Exception: pass
except Exception as 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
def _hit_test(self, view, screen_pt):
"""Liefert (axis, kind, world_pt) wenn screen_pt nahe eines Endpoint-
Markers liegt, sonst None. Iteriert die aktuelle Conduit-Liste."""
"""Liefert (axis, vertex_idx, world_pt) wenn screen_pt nahe eines
Vertex-Markers liegt, sonst None."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return None
try:
vp = view.ActiveViewport
except Exception: return None
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:
s = vp.WorldToClient(world_pt)
dx = s.X - screen_pt.X
dy = s.Y - screen_pt.Y
if (dx * dx + dy * dy) <= thresh2:
return axis, kind, world_pt
return axis, vidx, world_pt
except Exception: continue
return None
@@ -274,44 +265,59 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
# Default-Klick (Selection) abwuergen — wir uebernehmen
try: e.Cancel = True
except Exception: pass
axis, kind, world_pt = hit
self._start_drag(view.Document, axis, kind, world_pt)
axis, vidx, world_pt = hit
self._start_drag(view.Document, axis, vidx, world_pt)
except Exception as ex:
print("[WAND_GRIPS] OnMouseDown:", ex)
def _start_drag(self, doc, axis, kind, anchor_pt):
"""Startet eine Rhino-GetPoint-Interaktion um den neuen Endpunkt
zu picken. Der ANDERE Endpunkt (Fix-Punkt) wird als BasePoint
gesetzt — damit kriegt der User Tracking-Linie, Ortho-Mode etc.
wie bei _Move."""
def _start_drag(self, doc, axis, vertex_idx, anchor_pt):
"""Startet eine Rhino-GetPoint-Interaktion um den Vertex zu
verschieben. BasePoint-Strategie:
- End-Vertex (idx 0 oder letzter): gegenueberliegender End-Vertex
→ 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
geom = axis.Geometry
if geom is None: return
p_start, p_end = _curve_endpoints(geom)
if p_start is None or p_end is None: return
fixed_pt = p_end if kind == "start" else p_start
# Conduit-State: drag-Marker hervorheben + Preview-Linie
self.conduit.drag_key = (str(axis.Id), kind)
self.conduit.drag_preview = rg.Line(fixed_pt, anchor_pt)
pts = _axis_vertices(geom)
if not pts or vertex_idx < 0 or vertex_idx >= len(pts): return
is_first = vertex_idx == 0
is_last = vertex_idx == len(pts) - 1
prev_pt = pts[vertex_idx - 1] if not is_first else None
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
try:
gp = Rhino.Input.Custom.GetPoint()
gp.SetCommandPrompt("Wand-Endpunkt: neuer Punkt (Esc=Abbruch)")
gp.SetBasePoint(fixed_pt, True)
gp.DrawLineFromPoint(fixed_pt, True)
# Live-Preview ueber Conduit (zusaetzlich zu Rhinos eigener
# Tracking-Linie) — sieht ueblich, hilft beim Verstehen welcher
# Endpunkt sich bewegt.
gp.SetCommandPrompt("Wand-Vertex: neuer Punkt (Esc=Abbruch)")
gp.SetBasePoint(base_pt, True)
gp.DrawLineFromPoint(base_pt, True)
def _on_mouse_move(sender, args):
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
try: gp.MouseMove += _on_mouse_move
except Exception: pass
res = gp.Get()
if res == Rhino.Input.GetResult.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:
print("[WAND_GRIPS] _start_drag:", ex)
finally:
+274 -81
View File
@@ -6,7 +6,7 @@ import { BarToggle, BarButton, BarCombo } from './components/BarControls'
import {
onMessage, notifyReady,
createWall, createDecke, createDach,
createFenster, createTuer, createAussparung, createTreppe,
createFenster, createTuer, createAussparung, createTreppe, setTreppe2DShow,
createStuetze, createTraeger, createRaum, createStempel,
openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
@@ -136,9 +136,15 @@ function PopupMenu({ items, onClose }) {
zIndex: 100,
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}
onClick={(e) => { e.stopPropagation(); it.onClick(); onClose() }}
onClick={(e) => { e.stopPropagation(); it.onClick();
if (!it.keepOpen) onClose() }}
disabled={it.disabled}
title={it.hint || ''}
style={{
@@ -157,7 +163,10 @@ function PopupMenu({ items, onClose }) {
onMouseLeave={(e) => {
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)' }} />}
<span style={{ flex: 1 }}>{it.label}</span>
{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 [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
@@ -348,6 +357,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
const openStuetzeMenu = (e) => { e.preventDefault(); setStuetzeMenuOpen(true) }
const openTraegerMenu = (e) => { e.preventDefault(); setTraegerMenuOpen(true) }
const treppe2DOn = treppe2DShow !== false
const treppeItems = [
{ icon: 'stairs', label: 'Gerade Treppe',
hint: 'Lauflinie mit 2 Punkten',
@@ -358,6 +368,10 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
{ icon: 'rotate_right', label: 'Wendeltreppe',
hint: '3 Punkte: Mittelpunkt, Start-Lauflinie, End-Lauflinie',
onClick: () => createTreppe({ treppeArt: 'wendel' }) },
{ _divider: true },
{ checked: treppe2DOn, label: '2D-Plansymbol',
hint: 'Trittlinien + Auf-Pfeil auf Schnittebene zeichnen',
onClick: () => setTreppe2DShow(!treppe2DOn) },
]
const profilItems = (factory) => [
@@ -604,6 +618,7 @@ export default function ElementeApp() {
noGeschoss={noGeschoss}
activeName={activeName}
elementsCount={elements.length}
treppe2DShow={state.treppe2DShow}
/>
</div>
</div>
@@ -2020,6 +2035,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
const [nStufen, setNStufen] = useState(String(treppe.nStufen ?? 15))
const [laufD, setLaufD] = useState(String(treppe.laufD ?? 0.18))
const [hStr, setHStr] = useState('')
const [ukStr, setUkStr] = useState('')
useEffect(() => {
setBreite(String(treppe.breite ?? 1.0))
setNStufen(String(treppe.nStufen ?? 15))
@@ -2034,9 +2050,27 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
const sa = 2 * S + A
const soll = treppe.soll || DEFAULT_TREPPE_SOLL
const hasHOver = treppe.hOver != null && treppe.hOver !== ''
const hasUkOver = treppe.ukOver != null && treppe.ukOver !== ''
useEffect(() => {
setHStr(hasHOver ? String(treppe.hOver) : fmtNum(H))
}, [treppe.id, treppe.hOver, H, hasHOver])
useEffect(() => {
setUkStr(hasUkOver ? String(treppe.ukOver) : '')
}, [treppe.id, treppe.ukOver, hasUkOver])
const onCommitUk = () => {
const trimmed = (ukStr || '').trim()
if (trimmed === '') {
if (hasUkOver) onUpdate({ ukOver: '' })
return
}
const v = parseFloat(trimmed)
if (Number.isNaN(v)) { setUkStr(hasUkOver ? String(treppe.ukOver) : ''); return }
if (Math.abs(v) < 1e-6) {
if (hasUkOver) onUpdate({ ukOver: '' })
} else if (Math.abs(v - (parseFloat(treppe.ukOver) || 0)) > 1e-5) {
onUpdate({ ukOver: v })
}
}
const allOK = (
(!soll.s[2] || (S >= soll.s[0] && S <= soll.s[1])) &&
@@ -2059,18 +2093,41 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
}
const ref = treppe.treppeReferenz ?? 'mid'
const REF_OPTIONS = [
{ code: 'links', label: 'Links' },
{ code: 'mid', label: 'Mittig' },
{ code: 'rechts', label: 'Rechts' },
// L-Treppen: nur Aussen-Lage erlaubt (sonst kollidieren die Laeufe am Eck)
const REF_OPTIONS_ALL = [
{ code: 'links', label: 'links' },
{ code: 'mid', label: 'mittig' },
{ code: 'rechts', label: 'rechts' },
]
const REF_OPTIONS = treppe.treppeArt === 'l'
? REF_OPTIONS_ALL.filter(o => o.code !== 'mid')
: REF_OPTIONS_ALL
const modus = treppe.treppeModus ?? 'flach'
const MODUS_OPTIONS = [
{ code: 'massiv', label: 'massiv', hint: 'Block bis zum Boden — wie eine Mauer unter der Treppe' },
{ code: 'flach', label: 'flach', hint: 'Schräge Plattenunterseite parallel zum Treppenlauf (realistisch)' },
{ code: 'plattenrand', label: 'gestuft', hint: 'Plattenunterseite folgt den Stufen, vertikal versetzt' },
{ code: 'massiv', label: 'massiv', hint: 'Block bis zum Boden — wie eine Mauer unter der Treppe' },
{ code: 'flach', label: 'flach', hint: 'Schräge Plattenunterseite parallel zum Treppenlauf' },
{ code: 'plattenrand', label: 'gestuft', hint: 'Plattenunterseite folgt den Stufen, vertikal versetzt' },
]
// Konsistentes Grid: label(50) | control(1fr) | unit(14)
const rowStyle = {
display: 'grid',
gridTemplateColumns: '50px 1fr 14px',
alignItems: 'center', gap: 6,
}
const labelStyle = {
fontSize: 10, color: 'var(--text-secondary)',
}
const unitStyle = {
fontSize: 10, color: 'var(--text-muted)', textAlign: 'left',
}
const inputStyle = {
fontSize: 11, fontFamily: 'DM Mono, monospace', width: '100%',
}
const selectStyle = {
fontSize: 11, width: '100%', // Dropdowns nutzen System-Font (lesbar bei Worten)
}
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
@@ -2088,38 +2145,105 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Start</span>
<div style={rowStyle}>
<span style={labelStyle}>Start</span>
<select value={treppe.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
{geschosse.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Ziel</span>
<select
value={hasHOver ? '__custom__' : (treppe.geschossEnd || '')}
onChange={(e) => {
const v = e.target.value
if (v === '__custom__') {
// Eigene Hoehe — falls noch nicht gesetzt, mit aktuellem H starten
onUpdate({ hOver: H, geschossEnd: '' })
} else {
onUpdate({ geschossEnd: v, hOver: '' })
style={selectStyle}
title="Start-Geschoss — kann nicht hoeher als das Ziel-Geschoss sein">
{(() => {
// Ziel-Z bestimmen: aus Ziel-Geschoss oder aus hOver+startOkff+ukOver
let zielZ = null
if (treppe.geschossEnd) {
const g = geschosse.find(x => x.id === treppe.geschossEnd)
if (g) zielZ = Number(g.okff || 0)
} else if (treppe.hOver) {
const startG = geschosse.find(x => x.id === treppe.geschoss)
const startOkff = startG ? Number(startG.okff || 0) : 0
const ukO = Number(treppe.ukOver || 0)
zielZ = startOkff + ukO + Number(treppe.hOver)
}
}}
style={{ flex: 1, fontSize: 11 }}>
<option value="">(auto: Start + Höhe)</option>
{geschosse.filter(g => g.id !== treppe.geschoss)
.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
<option value="__custom__">eigene Höhe</option>
return geschosse
.filter(g => zielZ === null || Number(g.okff || 0) < zielZ)
.map(g => <option key={g.id} value={g.id}>{g.name}</option>)
})()}
</select>
<span style={unitStyle}></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
<div style={rowStyle}
title="Vertikaler Versatz des Treppen-Anfangs (relativ zum Geschoss-OKFF)">
<span style={labelStyle}>Versatz</span>
{hasUkOver ? (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={ukStr}
placeholder="0.00"
onChange={(e) => setUkStr(e.target.value)}
onBlur={onCommitUk}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
title="Versatz relativ zum Geschoss-OKFF"
style={{ ...inputStyle, flex: 1,
border: '1px solid var(--accent)' }} />
<button onClick={() => onUpdate({ ukOver: '' })}
title="Zurueck zu Geschoss-OKFF"
style={{ fontSize: 11, padding: '0 6px',
background: 'transparent', border: 'none',
color: 'var(--text-muted)', cursor: 'pointer' }}>×</button>
</div>
) : (
<select value=""
onChange={(e) => {
if (e.target.value === '__custom__') onUpdate({ ukOver: 0 })
}}
style={selectStyle}>
<option value="">(Geschoss-OKFF)</option>
<option value="__custom__">eigenes Z</option>
</select>
)}
<span style={unitStyle}>{hasUkOver ? 'm' : ''}</span>
</div>
<div style={rowStyle}>
<span style={labelStyle}>Ziel</span>
{hasHOver ? (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={hStr}
placeholder="1.50"
onChange={(e) => setHStr(e.target.value)}
onBlur={onCommitH}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
title="Treppen-Höhe (Delta Start → Ende)"
style={{ ...inputStyle, flex: 1,
border: '1px solid var(--accent)' }} />
<button onClick={() => onUpdate({ hOver: '' })}
title="Zurueck zu Geschoss-Verknuepfung"
style={{ fontSize: 11, padding: '0 6px',
background: 'transparent', border: 'none',
color: 'var(--text-muted)', cursor: 'pointer' }}>×</button>
</div>
) : (
<select
value={treppe.geschossEnd || ''}
onChange={(e) => {
const v = e.target.value
if (v === '__custom__') {
onUpdate({ hOver: H, geschossEnd: '' })
} else {
onUpdate({ geschossEnd: v, hOver: '' })
}
}}
style={selectStyle}>
<option value="">(auto: Start + Höhe)</option>
{geschosse.filter(g => g.id !== treppe.geschoss)
.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
<option value="__custom__">eigene Höhe</option>
</select>
)}
<span style={unitStyle}>{hasHOver ? 'm' : ''}</span>
</div>
<div style={rowStyle}>
<span style={labelStyle}>Breite</span>
<input type="text" value={breite}
onChange={(e) => setBreite(e.target.value)}
onBlur={() => {
@@ -2128,62 +2252,68 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
else setBreite(String(treppe.breite))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
style={inputStyle} />
<span style={unitStyle}>m</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Stufen</span>
<input type="text" value={nStufen}
onChange={(e) => setNStufen(e.target.value)}
onBlur={() => {
const v = parseInt(nStufen, 10)
if (Number.isFinite(v) && v >= 2 && v <= 40) onUpdate({ nStufen: v })
else setNStufen(String(treppe.nStufen))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>×</span>
<div style={rowStyle} title={treppe.lockS
? 'Mit Trittmaß-Lock: nur Anzahlen die ein S-Werte nahe der Sollhöhe ergeben'
: 'Anzahl Tritte (2-40)'}>
<span style={labelStyle}>Stufen</span>
<select value={treppe.nStufen}
onChange={(e) => onUpdate({ nStufen: parseInt(e.target.value, 10) })}
style={selectStyle}>
{(() => {
// Mit Lock: filtere die N-Werte deren resultierendes S nahe an
// target_S liegt (±10%). Sonst 2-40.
const range = []
for (let i = 2; i <= 40; i++) range.push(i)
if (treppe.lockS && treppe.targetS > 0.05 && H > 0.1) {
const tgt = Number(treppe.targetS)
const tol = tgt * 0.10
return range
.filter(n => Math.abs(H / n - tgt) <= tol)
.map(n => (
<option key={n} value={n}>
{n} (S={(H / n).toFixed(3)} m)
</option>
))
}
return range.map(n => <option key={n} value={n}>{n}</option>)
})()}
</select>
<span style={unitStyle}>×</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Lage</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<div style={rowStyle}>
<span style={labelStyle}>Lage</span>
<select value={ref}
onChange={(e) => onUpdate({ treppeReferenz: e.target.value })}
style={selectStyle}>
{REF_OPTIONS.map(o => (
<BarToggle key={o.code}
label={o.label}
active={ref === o.code}
onClick={() => onUpdate({ treppeReferenz: o.code })} />
<option key={o.code} value={o.code}>{o.label}</option>
))}
</div>
</select>
<span style={unitStyle}></span>
</div>
<div style={{ height: 1, background: 'var(--border-light)', margin: '2px 0' }} />
{/* Unterseite-Modus */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Form der Treppen-Unterseite">
Unten
</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<div style={rowStyle} title="Form der Treppen-Unterseite">
<span style={labelStyle}>Unten</span>
<select value={modus}
onChange={(e) => onUpdate({ treppeModus: e.target.value })}
style={selectStyle}>
{MODUS_OPTIONS.map(o => (
<BarToggle key={o.code}
label={o.label}
active={modus === o.code}
onClick={() => onUpdate({ treppeModus: o.code })}
title={o.hint} />
<option key={o.code} value={o.code} title={o.hint}>{o.label}</option>
))}
</div>
</select>
<span style={unitStyle}></span>
</div>
{/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */}
{modus !== 'massiv' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
Platte
</span>
<div style={rowStyle} title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
<span style={labelStyle}>Platte</span>
<input type="text" value={laufD}
onChange={(e) => setLaufD(e.target.value)}
onBlur={() => {
@@ -2192,11 +2322,65 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
else setLaufD(String(treppe.laufD))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
style={inputStyle} />
<span style={unitStyle}>m</span>
</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>
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer',
}}>
<input type="checkbox" checked={treppe.showLauflinie !== false}
onChange={(e) => onUpdate({ showLauflinie: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>Lauflinie</span>
</label>
{treppe.showLauflinie !== false && (
<div style={{
display: 'grid', gridTemplateColumns: '50px 1fr 14px',
alignItems: 'center', gap: 6, marginLeft: 18,
}}>
<span style={labelStyle}>Pfeil</span>
<select value={treppe.arrowStyle || 'klassisch'}
onChange={(e) => onUpdate({ arrowStyle: e.target.value })}
style={selectStyle}>
<option value="klassisch">klassisch</option>
<option value="filled">gefüllt</option>
<option value="breit">breit</option>
<option value="voll">voll (bis zu den Seiten)</option>
</select>
<span style={unitStyle}></span>
</div>
)}
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer',
}}>
<input type="checkbox" checked={treppe.showBruch !== false}
onChange={(e) => onUpdate({ showBruch: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>Bruchsymbol</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 */}
<div style={{
display: 'flex', flexDirection: 'column', gap: 3,
@@ -2231,6 +2415,15 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
}}>auto</button>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}
title="Wenn an: Beim Aendern der Hoehe (oder Versatz/Ziel) wird die Anzahl Stufen automatisch nachgerechnet, damit S konstant bleibt.">
<input type="checkbox" checked={!!treppe.lockS}
onChange={(e) => onUpdate({ lockS: e.target.checked })}
style={{ accentColor: 'var(--accent)', width: 12, height: 12 }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
Trittmaß fixiert (S{treppe.targetS ? ' = ' + Number(treppe.targetS).toFixed(3) + ' m' : ''})
</span>
</div>
<SollRow label="S" value={S} unit="m" soll={soll} sollKey="s"
onUpdateSoll={onUpdateSoll} />
<SollRow label="A" value={A} unit="m" soll={soll} sollKey="a"
+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 createAussparung(p) { send('CREATE_AUSSPARUNG', 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 createTraeger(p) { send('CREATE_TRAEGER', p || {}) }
export function createRaum(p) { send('CREATE_RAUM', p || {}) }