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
This commit is contained in:
2026-05-28 02:16:57 +02:00
parent 970281e10a
commit 6060c74b17
+184 -19
View File
@@ -6374,36 +6374,115 @@ def _tritte_l(axis_polyline, breite, referenz, n_stufen, z,
def _lauflinie_l(axis_polyline, n_stufen, z, arrow_style="klassisch", doc=None, def _lauflinie_l(axis_polyline, n_stufen, z, arrow_style="klassisch", doc=None,
breite=None, referenz=None): breite=None, referenz=None):
"""Lauflinie ueber BEIDE Laeufe einer L-Treppe. Schaft 1 vom Start zur
PROJEZIERTEN Eck-Mitte (Schnittpunkt der zwei mid-versetzten Linien),
Schaft 2 von Eck-Mitte zum Ende mit Pfeil. So sauberer Übergang ohne
Versatz an der Ecke auch bei Lage=links/rechts."""
segs = _l_segments(axis_polyline, n_stufen, z) segs = _l_segments(axis_polyline, n_stufen, z)
if segs is None: return [] if segs is None: return []
s1, _s2, N1, _N2 = segs s1, s2, _N1, _N2 = segs
lo, _up = _lauflinie_gerade(s1, N1, z, -1, False, arrow_style, doc, P0a = s1.PointAtStart; P1a = s1.PointAtEnd
breite, referenz) txa, tya = P1a.X - P0a.X, P1a.Y - P0a.Y
return lo La = (txa * txa + tya * tya) ** 0.5
P0b = s2.PointAtStart; P1b = s2.PointAtEnd
txb, tyb = P1b.X - P0b.X, P1b.Y - P0b.Y
Lb = (txb * txb + tyb * tyb) ** 0.5
if La < 1e-6 or Lb < 1e-6: return []
uxa, uya = txa / La, tya / La
uxb, uyb = txb / Lb, tyb / Lb
# Mid-offset relativ zur Axis
mid_off = 0.0
wide_l = wide_r = None
if breite is not None and referenz is not None:
off_l, off_r = _treppe_2d_side_offsets(breite, referenz)
mid_off = (off_l + off_r) * 0.5
wide_l = off_l - mid_off
wide_r = off_r - mid_off
# perp 90° CCW
pa_x, pa_y = -uya * mid_off, uxa * mid_off
pb_x, pb_y = -uyb * mid_off, uxb * mid_off
P0a_s = rg.Point3d(P0a.X + pa_x, P0a.Y + pa_y, z)
P1a_s = rg.Point3d(P1a.X + pa_x, P1a.Y + pa_y, z)
P0b_s = rg.Point3d(P0b.X + pb_x, P0b.Y + pb_y, z)
P1b_s = rg.Point3d(P1b.X + pb_x, P1b.Y + pb_y, z)
# Echte Eck-Mitte als Schnittpunkt der zwei versetzten Schaft-Linien
if abs(mid_off) > 1e-6:
meet = _line_intersect_xy(P0a_s, rg.Vector3d(uxa, uya, 0),
P1b_s, rg.Vector3d(uxb, uyb, 0))
if meet is None: meet = P1a_s # Fallback (parallel oder collinear)
else:
meet = P1a_s # = P0b_s wenn mid=0
out = []
out.append(rg.LineCurve(P0a_s, meet))
head_size = min(0.25, Lb * 0.05)
out.extend(_treppe_2d_arrow_curves(meet, P1b_s, head_size, arrow_style,
doc, wide_l, wide_r))
return out
def _aussen_l_polygon(axis_polyline, breite, referenz, z):
"""Sauberes L-Polygon: 6-Punkt-Outline, die beiden Laeufe schliessen
sich an der Ecke an (Outer/Inner via Linien-Schnitt). Returnt eine
Polyline ODER None bei Singularitaet (parallele Schenkel)."""
ok, poly = axis_polyline.TryGetPolyline()
if not ok or poly is None or poly.Count != 3: return None
p0 = rg.Point3d(poly[0].X, poly[0].Y, z)
pc = rg.Point3d(poly[1].X, poly[1].Y, z)
p1 = rg.Point3d(poly[2].X, poly[2].Y, z)
L1 = ((pc.X - p0.X) ** 2 + (pc.Y - p0.Y) ** 2) ** 0.5
L2 = ((p1.X - pc.X) ** 2 + (p1.Y - pc.Y) ** 2) ** 0.5
if L1 < 1e-6 or L2 < 1e-6: return None
u1 = rg.Vector3d((pc.X - p0.X) / L1, (pc.Y - p0.Y) / L1, 0)
u2 = rg.Vector3d((p1.X - pc.X) / L2, (p1.Y - pc.Y) / L2, 0)
# perp 90° CCW
pp1 = (-u1.Y, u1.X)
pp2 = (-u2.Y, u2.X)
off_l, off_r = _treppe_2d_side_offsets(breite, referenz)
def _off(pt, perp, d):
return rg.Point3d(pt.X + perp[0] * d, pt.Y + perp[1] * d, z)
# Lauf1 Seiten (start + corner)
p0_l = _off(p0, pp1, off_l); p0_r = _off(p0, pp1, off_r)
pc_l1 = _off(pc, pp1, off_l); pc_r1 = _off(pc, pp1, off_r)
# Lauf2 Seiten (corner + end)
pc_l2 = _off(pc, pp2, off_l); pc_r2 = _off(pc, pp2, off_r)
p1_l = _off(p1, pp2, off_l); p1_r = _off(p1, pp2, off_r)
# Ecken-Schnitte: linker Schenkel intersect, rechter Schenkel intersect
corner_l = _line_intersect_xy(p0_l, u1, p1_l, u2) or pc_l1
corner_r = _line_intersect_xy(p0_r, u1, p1_r, u2) or pc_r1
pts = [p0_l, p0_r, corner_r, p1_r, p1_l, corner_l, p0_l]
return rg.PolylineCurve(rg.Polyline(pts))
def _aussen_l(axis_polyline, breite, referenz, z, def _aussen_l(axis_polyline, breite, referenz, z,
total_h=0.0, cut_h=0.0, n_stufen=15): total_h=0.0, cut_h=0.0, n_stufen=15):
"""Returnt (lower, upper). Bei Cut: betroffener Lauf wird gesplittet, """Returnt (lower, upper). Ohne Cut: sauberes L-Polygon (eine Outline).
anderer Lauf komplett lower bzw. upper.""" Mit Cut: faellt zurueck auf Per-Lauf-Rechtecke mit Diagonal-Cut auf der
betroffenen Seite der jeweils andere Lauf wird komplett lower bzw.
upper (= gestrichelt wenn oben)."""
segs = _l_segments(axis_polyline, n_stufen, z) segs = _l_segments(axis_polyline, n_stufen, z)
if segs is None: return [], [] if segs is None: return [], []
s1, s2, N1, N2 = segs s1, s2, N1, N2 = segs
H1 = total_h * (N1 / float(N1 + N2)) if total_h > 0 else 0.0 H1 = total_h * (N1 / float(N1 + N2)) if total_h > 0 else 0.0
H2 = total_h - H1 H2 = total_h - H1
cut1 = _cut_idx_gerade(N1, H1, cut_h) if total_h > 0 else -1 cut1 = _cut_idx_gerade(N1, H1, cut_h) if total_h > 0 else -1
cut2 = _cut_idx_gerade(N2, H2, cut_h - H1) if total_h > 0 else -1
# Ohne Cut: zeichne sauberes L-Polygon
if cut1 < 0 and cut2 < 0:
poly = _aussen_l_polygon(axis_polyline, breite, referenz, z)
if poly is not None:
return [poly], []
# Fallback: zwei Rechtecke
l1, _ = _aussen_gerade(s1, breite, referenz, z, -1, None)
l2, _ = _aussen_gerade(s2, breite, referenz, z, -1, None)
return l1 + l2, []
# Mit Cut: per-Lauf, betroffener Lauf wird diagonal gesplittet
if cut1 >= 0: if cut1 >= 0:
l1, u1 = _aussen_gerade(s1, breite, referenz, z, cut1, N1) l1, u1 = _aussen_gerade(s1, breite, referenz, z, cut1, N1)
l2, _ = _aussen_gerade(s2, breite, referenz, z, -1, None) l2, _ = _aussen_gerade(s2, breite, referenz, z, -1, None)
return l1, u1 + l2 return l1, u1 + l2
cut2 = _cut_idx_gerade(N2, H2, cut_h - H1) if total_h > 0 else -1
if cut2 >= 0:
l1, _ = _aussen_gerade(s1, breite, referenz, z, -1, None) l1, _ = _aussen_gerade(s1, breite, referenz, z, -1, None)
l2, u2 = _aussen_gerade(s2, breite, referenz, z, cut2, N2) l2, u2 = _aussen_gerade(s2, breite, referenz, z, cut2, N2)
return l1 + l2, u2 return l1 + l2, u2
l1, _ = _aussen_gerade(s1, breite, referenz, z, -1, None)
l2, _ = _aussen_gerade(s2, breite, referenz, z, -1, None)
return l1 + l2, []
def _bruch_l(axis_polyline, breite, referenz, n_stufen, total_h, cut_h, z): def _bruch_l(axis_polyline, breite, referenz, n_stufen, total_h, cut_h, z):
@@ -6487,7 +6566,13 @@ def _lauflinie_wendel(axis_polyline, breite, referenz, n_stufen, z,
head_len = min(0.25, abs(r_mid * da) * 0.6) head_len = min(0.25, abs(r_mid * da) * 0.6)
tail = rg.Point3d(arc_end.X - tang_x * head_len * 0.3, tail = rg.Point3d(arc_end.X - tang_x * head_len * 0.3,
arc_end.Y - tang_y * head_len * 0.3, z) arc_end.Y - tang_y * head_len * 0.3, z)
out.extend(_treppe_2d_arrow_curves(tail, arc_end, head_len, arrow_style, doc)) # 'voll'-Style: Spitzen-Offsets relativ zum r_mid (Tangenten-perp =
# radial). Innen → negativ, Aussen → positiv (in perp-Richtung
# CCW vom Tangenten-Vektor — entspricht der Radial-Richtung).
wide_in = r_inner - r_mid # negativ
wide_out = r_outer - r_mid # positiv
out.extend(_treppe_2d_arrow_curves(tail, arc_end, head_len, arrow_style,
doc, wide_in, wide_out))
except Exception as ex: except Exception as ex:
print("[ELEMENTE] _lauflinie_wendel:", ex) print("[ELEMENTE] _lauflinie_wendel:", ex)
return out return out
@@ -11907,12 +11992,30 @@ class ElementeBridge(panel_base.BaseBridge):
def _update_wall(self, p): def _update_wall(self, p):
"""Properties eines Elements aendern (Wand/Decke/Dach/Oeffnung). """Properties eines Elements aendern (Wand/Decke/Dach/Oeffnung).
Volumen wird anschliessend regeneriert.""" Volumen wird anschliessend regeneriert.
Wird in EINEM Undo-Record zusammengefasst, sodass Cmd+Z die ganze
Patch-Operation inkl. Regen rueckgaengig macht (statt nur den
letzten AddBrep)."""
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
wall_id = p.get("id") wall_id = p.get("id")
if not wall_id: return if not wall_id: return
axis_obj, old_meta = _find_source(doc, wall_id) axis_obj, old_meta = _find_source(doc, wall_id)
if axis_obj is None or old_meta is None: return if axis_obj is None or old_meta is None: return
# Undo-Record umschliesst Source-Mods + Regen-Delete/Add
_ur_serial = None
try:
if doc is not None:
_ur_serial = doc.BeginUndoRecord("Dossier: Element-Update")
except Exception: _ur_serial = None
try:
return self._update_wall_body(doc, p, wall_id, axis_obj, old_meta)
finally:
if _ur_serial is not None:
try: doc.EndUndoRecord(_ur_serial)
except Exception: pass
def _update_wall_body(self, doc, p, wall_id, axis_obj, old_meta):
"""Eigentliche Patch-Logik. Sieh _update_wall fuer Undo-Wrapping."""
# Tragwerk: Stuetze (Punkt) oder Traeger/Unterzug (Achse) # Tragwerk: Stuetze (Punkt) oder Traeger/Unterzug (Achse)
if old_meta["type"] in ("stuetze_point", "traeger_axis"): if old_meta["type"] in ("stuetze_point", "traeger_axis"):
kind = p.get("kind", old_meta.get("trag_kind", "stuetze")) kind = p.get("kind", old_meta.get("trag_kind", "stuetze"))
@@ -12234,24 +12337,86 @@ class ElementeBridge(panel_base.BaseBridge):
if H_now > 0.1: if H_now > 0.1:
tn = max(2, int(round(H_now / ttarget_s))) tn = max(2, int(round(H_now / ttarget_s)))
# Axis-Laenge so anpassen dass A = target_A bleibt # Axis-Laenge so anpassen dass A = target_A bleibt
# (nur fuer gerade Treppe = LineCurve)
if ttarget_a > 0.05: if ttarget_a > 0.05:
new_L = tn * ttarget_a new_total_L = tn * ttarget_a
tart_cur = old_meta.get("treppe_art", "gerade")
try: try:
_g = axis_obj.Geometry _g = axis_obj.Geometry
# ---- GERADE: LineCurve P1 verschieben
if (isinstance(_g, rg.LineCurve) if (isinstance(_g, rg.LineCurve)
and abs(cur_axis_L - new_L) > 1e-4): and abs(cur_axis_L - new_total_L) > 1e-4):
P0 = _g.PointAtStart P0 = _g.PointAtStart
P1 = _g.PointAtEnd P1 = _g.PointAtEnd
dx = P1.X - P0.X; dy = P1.Y - P0.Y dx = P1.X - P0.X; dy = P1.Y - P0.Y
if cur_axis_L > 1e-6: if cur_axis_L > 1e-6:
ux = dx / cur_axis_L ux = dx / cur_axis_L
uy = dy / cur_axis_L uy = dy / cur_axis_L
P1n = rg.Point3d(P0.X + ux * new_L, P1n = rg.Point3d(P0.X + ux * new_total_L,
P0.Y + uy * new_L, P0.Z) P0.Y + uy * new_total_L, P0.Z)
new_line = rg.LineCurve(P0, P1n) new_line = rg.LineCurve(P0, P1n)
doc.Objects.Replace(axis_obj.Id, new_line) doc.Objects.Replace(axis_obj.Id, new_line)
# axis_obj-Reference neu holen axis_obj = doc.Objects.Find(axis_obj.Id)
attrs = axis_obj.Attributes
# ---- L: PolylineCurve mit 3 Punkten,
# beide Laeufe proportional skalieren
elif (isinstance(_g, rg.PolylineCurve) and tart_cur == "l"):
ok_pl, poly = _g.TryGetPolyline()
if ok_pl and poly is not None and poly.Count == 3:
p0 = poly[0]; pc = poly[1]; p1 = poly[2]
L1c = ((pc.X-p0.X)**2 + (pc.Y-p0.Y)**2) ** 0.5
L2c = ((p1.X-pc.X)**2 + (p1.Y-pc.Y)**2) ** 0.5
cur_total = L1c + L2c
if cur_total > 1e-6:
ratio = new_total_L / cur_total
if abs(ratio - 1.0) > 1e-4:
# Lauf 1 skalieren — pc neu setzen
u1x = (pc.X-p0.X)/L1c
u1y = (pc.Y-p0.Y)/L1c
new_L1 = L1c * ratio
pc_n = rg.Point3d(
p0.X + u1x*new_L1,
p0.Y + u1y*new_L1, p0.Z)
# Lauf 2 skalieren — p1 neu setzen
u2x = (p1.X-pc.X)/L2c
u2y = (p1.Y-pc.Y)/L2c
new_L2 = L2c * ratio
p1_n = rg.Point3d(
pc_n.X + u2x*new_L2,
pc_n.Y + u2y*new_L2, p1.Z)
new_poly = rg.PolylineCurve(
rg.Polyline([p0, pc_n, p1_n]))
doc.Objects.Replace(axis_obj.Id, new_poly)
axis_obj = doc.Objects.Find(axis_obj.Id)
attrs = axis_obj.Attributes
# ---- WENDEL: PolylineCurve [center, start, end]
# → Sweep-Winkel anpassen damit Bogenlaenge =
# new_total_L (Bogenlaenge = r_mid * |delta|)
elif (isinstance(_g, rg.PolylineCurve) and tart_cur == "wendel"):
import math as _m
ok_pl, poly = _g.TryGetPolyline()
if ok_pl and poly is not None and poly.Count == 3:
center = poly[0]; pstart = poly[1]; pend = poly[2]
r_click = _m.hypot(pstart.X-center.X,
pstart.Y-center.Y)
if r_click > 0.2:
r_inner_l, r_outer_l = _wendel_radii(
r_click, tb, tref)
r_mid = (r_inner_l + r_outer_l) * 0.5
a_start_cur, delta_cur = _wendel_sweep(
center, pstart, pend)
if abs(delta_cur) > 0.01 and r_mid > 1e-6:
cur_arc = abs(r_mid * delta_cur)
if abs(cur_arc - new_total_L) > 1e-4:
sign = 1.0 if delta_cur > 0 else -1.0
new_delta = sign * (new_total_L / r_mid)
a_end_new = a_start_cur + new_delta
pend_n = rg.Point3d(
center.X + r_click * _m.cos(a_end_new),
center.Y + r_click * _m.sin(a_end_new),
pend.Z)
new_poly = rg.PolylineCurve(
rg.Polyline([center, pstart, pend_n]))
doc.Objects.Replace(axis_obj.Id, new_poly)
axis_obj = doc.Objects.Find(axis_obj.Id) axis_obj = doc.Objects.Find(axis_obj.Id)
attrs = axis_obj.Attributes attrs = axis_obj.Attributes
except Exception as ex: except Exception as ex: