Compare commits
7 Commits
d9589e99f5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7930705d01 | |||
| d8966cc035 | |||
| e406e8d9b2 | |||
| bb64e4d41e | |||
| 6060c74b17 | |||
| 970281e10a | |||
| bcf7d557b1 |
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 || {}) }
|
||||
|
||||
Reference in New Issue
Block a user