diff --git a/rhino/elemente.py b/rhino/elemente.py index 5a18f5b..a2b1137 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -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, 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) if segs is None: return [] - s1, _s2, N1, _N2 = segs - lo, _up = _lauflinie_gerade(s1, N1, z, -1, False, arrow_style, doc, - breite, referenz) - return lo + s1, s2, _N1, _N2 = segs + P0a = s1.PointAtStart; P1a = s1.PointAtEnd + txa, tya = P1a.X - P0a.X, P1a.Y - P0a.Y + 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, total_h=0.0, cut_h=0.0, n_stufen=15): - """Returnt (lower, upper). Bei Cut: betroffener Lauf wird gesplittet, - anderer Lauf komplett lower bzw. upper.""" + """Returnt (lower, upper). Ohne Cut: sauberes L-Polygon (eine Outline). + 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) if segs is None: return [], [] s1, s2, N1, N2 = segs H1 = total_h * (N1 / float(N1 + N2)) if total_h > 0 else 0.0 H2 = total_h - H1 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: l1, u1 = _aussen_gerade(s1, breite, referenz, z, cut1, N1) l2, _ = _aussen_gerade(s2, breite, referenz, z, -1, None) 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) - l2, u2 = _aussen_gerade(s2, breite, referenz, z, cut2, N2) - 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, [] + l2, u2 = _aussen_gerade(s2, breite, referenz, z, cut2, N2) + return l1 + l2, u2 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) tail = rg.Point3d(arc_end.X - tang_x * head_len * 0.3, 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: print("[ELEMENTE] _lauflinie_wendel:", ex) return out @@ -11907,12 +11992,30 @@ class ElementeBridge(panel_base.BaseBridge): def _update_wall(self, p): """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 wall_id = p.get("id") if not wall_id: return axis_obj, old_meta = _find_source(doc, wall_id) 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) if old_meta["type"] in ("stuetze_point", "traeger_axis"): kind = p.get("kind", old_meta.get("trag_kind", "stuetze")) @@ -12234,26 +12337,88 @@ class ElementeBridge(panel_base.BaseBridge): if H_now > 0.1: tn = max(2, int(round(H_now / ttarget_s))) # Axis-Laenge so anpassen dass A = target_A bleibt - # (nur fuer gerade Treppe = LineCurve) 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: _g = axis_obj.Geometry + # ---- GERADE: LineCurve P1 verschieben 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 P1 = _g.PointAtEnd dx = P1.X - P0.X; dy = P1.Y - P0.Y if cur_axis_L > 1e-6: ux = dx / cur_axis_L uy = dy / cur_axis_L - P1n = rg.Point3d(P0.X + ux * new_L, - P0.Y + uy * new_L, P0.Z) + P1n = rg.Point3d(P0.X + ux * new_total_L, + P0.Y + uy * new_total_L, P0.Z) new_line = rg.LineCurve(P0, P1n) 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) + attrs = axis_obj.Attributes except Exception as ex: print("[ELEMENTE] lock axis-len adjust:", ex) print("[ELEMENTE] Lock: H={:.3f}/S={:.3f}→n={}, A={:.3f}".format(