Polyline-Wand-Refactor (Chain-Anchor) + Cmd+Z-Fixes
Chain-Modell: kompatible benachbarte Waende (gleiche dicke/referenz/layers/ material) teilen ein gemeinsames wand_volume via Polyline-Extrusion. Anchor (= alphabetisch kleinste ID) baut + besitzt das Volume; Non-Anchors haben keins. wand_chain_members UserString trackt Mitglieder. - _find_wall_chain/_build_chain_polyline mit korrekter Erst-Seg-Orientierung - Anchor-Branch im Regen + Non-Anchor-covered-Check (Loop-Schutz) - Pre-Test des Chain-Breps vor Cleanup → Fallback per-wand bei Brep-Build-Fail (z.B. Offset self-intersect auf Innen-Ecke) - Pre-Check in _on_command_end liest existing chain_members statt Live-Detect → erkennt teil-bewegte Chains + aborted pure-transform - affected_chain_members in regular regen-Pfad → Survivor regenes mit - _update_wall regen't alte Chain-Members nach Property-Change - _on_object_replaced No-Op-Check fuer wand_axis (Grips-Toggle skippt regen) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+396
-18
@@ -36,6 +36,7 @@ _KEY_REFERENZ = "dossier_referenz" # "mid" | "left" | "right"
|
||||
_KEY_WAND_LAYERED = "dossier_wand_layered" # "1" = mehrschichtig, sonst solid
|
||||
_KEY_WAND_LAYERS = "dossier_wand_layers" # JSON-Liste [{name, dicke, color}]
|
||||
_KEY_WAND_LAYER_IDX = "dossier_wand_layer_idx" # Layer-Index am Volume-Brep
|
||||
_KEY_WAND_CHAIN_MEMBERS = "dossier_wand_chain_members" # JSON-Liste wand_ids einer Polyline-Chain (nur auf wand_volume)
|
||||
_KEY_DACH_NEIGUNG = "dossier_dach_neigung" # Grad als string ("30")
|
||||
_KEY_DACH_EAVE = "dossier_dach_eave" # Index der Traufkante (string)
|
||||
_KEY_DACH_TYP = "dossier_dach_typ" # "pult"|"sattel"|"walm"|"mansarde"
|
||||
@@ -1808,16 +1809,20 @@ def _miter_dir(out_a, out_b):
|
||||
|
||||
|
||||
def _detect_t_junction(doc, geschoss_id, wall_id, endpoint,
|
||||
pos_tol=0.01, end_tol=0.05):
|
||||
pos_tol=0.01, end_tol=0.05, exclude_ids=None):
|
||||
"""Sucht ob `endpoint` auf der INNEREN Achse einer anderen Wand liegt
|
||||
(T-Stoss). Endpunkte der anderen Wand (Eckverbindung) werden bewusst
|
||||
ausgeschlossen — die werden bereits durch die Corner-Logik abgedeckt.
|
||||
`exclude_ids` (optional): zusaetzliche wall_ids die ignoriert werden
|
||||
sollen (Chain-Members), sonst nur wall_id.
|
||||
Liefert (other_wall_id, b_tangent_vec3, b_dicke) oder None."""
|
||||
skip = set(exclude_ids or ())
|
||||
skip.add(wall_id)
|
||||
for obj in doc.Objects:
|
||||
meta = _read_meta(obj)
|
||||
if not meta or meta["type"] != "wand_axis": continue
|
||||
if meta["geschoss"] != geschoss_id: continue
|
||||
if meta["id"] == wall_id: continue
|
||||
if meta["id"] in skip: continue
|
||||
geom = obj.Geometry
|
||||
if not isinstance(geom, rg.Curve): continue
|
||||
try:
|
||||
@@ -1863,6 +1868,169 @@ def _t_junction_miter(endpoint, out_dir, b_tan, b_dicke):
|
||||
return (mpt, mdir)
|
||||
|
||||
|
||||
# --- Wand-Chain: Polyline-vereinte Volumes -----------------------------------
|
||||
# Wenn N kompatible Waende sequentiell zusammenliegen (gleiche Geometrie/
|
||||
# Material/Hoehe, jeweils 2-Wall-Joints) verbinden wir ihre Achsen zu einer
|
||||
# Polyline und extrudieren ein einziges Brep statt N. Damit verschwinden die
|
||||
# internen Stoss-Linien in der Section-View (gleichfarbige Hatch ueber die
|
||||
# ganze Polyline).
|
||||
#
|
||||
# Anchor-Modell: das gemeinsame wand_volume gehoert zur "Anchor"-Wand
|
||||
# (alphabetisch kleinste wall_id der Chain). Die anderen Chain-Members haben
|
||||
# kein eigenes wand_volume — `_find_volume` schaut deswegen zusaetzlich nach
|
||||
# wand_chain_members-UserStrings.
|
||||
|
||||
def _wand_chain_compat(meta_a, meta_b):
|
||||
"""Sind zwei Waende kompatibel fuer einen gemeinsamen Polyline-Chain?
|
||||
Wenn irgendein geometrie-relevanter Parameter abweicht: nein. Sonst
|
||||
waere das gemeinsame Volume nicht sauber baubar."""
|
||||
if not meta_a or not meta_b: return False
|
||||
if meta_a.get("geschoss") != meta_b.get("geschoss"): return False
|
||||
if abs(float(meta_a.get("dicke", 0)) - float(meta_b.get("dicke", 0))) > 1e-6:
|
||||
return False
|
||||
if meta_a.get("referenz", "mid") != meta_b.get("referenz", "mid"):
|
||||
return False
|
||||
if (meta_a.get("uk_override") or "") != (meta_b.get("uk_override") or ""):
|
||||
return False
|
||||
if (meta_a.get("ok_override") or "") != (meta_b.get("ok_override") or ""):
|
||||
return False
|
||||
if bool(meta_a.get("wand_layered")) != bool(meta_b.get("wand_layered")):
|
||||
return False
|
||||
if meta_a.get("wand_layered"):
|
||||
la = meta_a.get("wand_layers") or []
|
||||
lb = meta_b.get("wand_layers") or []
|
||||
if len(la) != len(lb): return False
|
||||
for x, y in zip(la, lb):
|
||||
if abs(float(x.get("dicke", 0)) - float(y.get("dicke", 0))) > 1e-6:
|
||||
return False
|
||||
# Material muss identisch sein — die Layer-Reihenfolge + Materialien
|
||||
# bestimmen die Sub-Layer-Zuordnung der Schicht-Breps.
|
||||
if (x.get("material") or "") != (y.get("material") or ""):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _find_wall_chain(doc, wall_id):
|
||||
"""Liefert ORDERED Liste der wall_ids im Polyline-Chain von wall_id.
|
||||
Reihenfolge: vom Chain-Start bis zum Chain-Ende (so dass die Achsen
|
||||
aneinander anschliessen). wall_id selbst ist immer dabei.
|
||||
Bei nicht-Wand oder Wand nicht gefunden: leere Liste.
|
||||
Stop-Bedingungen: Verzweigung (>=2 Nachbarn am Joint), T-Stoss,
|
||||
inkompatibler Nachbar, oder kein Nachbar."""
|
||||
src, meta = _find_source(doc, wall_id)
|
||||
if src is None or meta is None or meta.get("type") != "wand_axis":
|
||||
return []
|
||||
geschoss = meta["geschoss"]
|
||||
joints = _collect_wall_joints(doc, geschoss)
|
||||
meta_by_id = {wall_id: meta}
|
||||
geom_by_id = {wall_id: src.Geometry}
|
||||
for obj in doc.Objects:
|
||||
m = _read_meta(obj)
|
||||
if not m or m["type"] != "wand_axis": continue
|
||||
if m["geschoss"] != geschoss: continue
|
||||
if m["id"] in meta_by_id: continue
|
||||
meta_by_id[m["id"]] = m
|
||||
geom_by_id[m["id"]] = obj.Geometry
|
||||
|
||||
def _chain_neighbor(cur_id, cur_pt):
|
||||
"""Am gemeinsamen Punkt: liefert (neighbor_id, neighbor_end) wenn
|
||||
es genau einen kompatiblen Nachbarn gibt, sonst None. neighbor_end
|
||||
ist "start" oder "end" — welcher Endpunkt von neighbor an cur_pt
|
||||
sitzt."""
|
||||
key = _pt_key(cur_pt)
|
||||
partners = [(p_wid, p_end)
|
||||
for (p_wid, p_end, _od) in joints.get(key, [])
|
||||
if p_wid != cur_id]
|
||||
if len(partners) != 1: return None
|
||||
p_wid, p_end = partners[0]
|
||||
if not _wand_chain_compat(meta_by_id.get(cur_id),
|
||||
meta_by_id.get(p_wid)):
|
||||
return None
|
||||
return (p_wid, p_end)
|
||||
|
||||
chain = [wall_id]
|
||||
visited = {wall_id}
|
||||
# Vorwaerts: ans "end" der aktuellen Wand entlang
|
||||
cur_id = wall_id
|
||||
cur_pt = geom_by_id[cur_id].PointAtEnd
|
||||
while True:
|
||||
nb = _chain_neighbor(cur_id, cur_pt)
|
||||
if nb is None: break
|
||||
p_wid, p_end = nb
|
||||
if p_wid in visited: break
|
||||
chain.append(p_wid); visited.add(p_wid)
|
||||
cur_id = p_wid
|
||||
# Anderer Endpunkt des Nachbarn ist der naechste Walk-Point
|
||||
cur_pt = (geom_by_id[p_wid].PointAtEnd if p_end == "start"
|
||||
else geom_by_id[p_wid].PointAtStart)
|
||||
# Rueckwaerts: ans "start" der aktuellen Wand entlang
|
||||
cur_id = wall_id
|
||||
cur_pt = geom_by_id[cur_id].PointAtStart
|
||||
while True:
|
||||
nb = _chain_neighbor(cur_id, cur_pt)
|
||||
if nb is None: break
|
||||
p_wid, p_end = nb
|
||||
if p_wid in visited: break
|
||||
chain.insert(0, p_wid); visited.add(p_wid)
|
||||
cur_id = p_wid
|
||||
cur_pt = (geom_by_id[p_wid].PointAtEnd if p_end == "start"
|
||||
else geom_by_id[p_wid].PointAtStart)
|
||||
return chain
|
||||
|
||||
|
||||
def _chain_anchor(chain_ids):
|
||||
"""Eindeutiger Anchor (= alphabetisch kleinste ID). Anchor besitzt das
|
||||
gemeinsame wand_volume; die anderen Chain-Members haben keins."""
|
||||
if not chain_ids: return None
|
||||
return sorted(chain_ids)[0]
|
||||
|
||||
|
||||
def _build_chain_polyline(doc, ordered_chain_ids):
|
||||
"""Joint die wand_axis-Curves zu einer PolylineCurve. Orientiert die
|
||||
erste Curve so dass ihr End-Punkt am Joint zur zweiten Curve sitzt,
|
||||
danach Auto-Flip pro Folge-Curve. Returns Curve oder None.
|
||||
Bei single-element chain: Duplicate der Original-Curve."""
|
||||
if not ordered_chain_ids: return None
|
||||
if len(ordered_chain_ids) == 1:
|
||||
obj = _find_axis(doc, ordered_chain_ids[0])
|
||||
return obj.Geometry.DuplicateCurve() if obj else None
|
||||
# Alle Segs (Point3d-Listen) sammeln
|
||||
segs = []
|
||||
for wid in ordered_chain_ids:
|
||||
obj = _find_axis(doc, wid)
|
||||
if obj is None: return None
|
||||
geom = obj.Geometry
|
||||
if not isinstance(geom, rg.Curve): return None
|
||||
rc, pl = geom.TryGetPolyline()
|
||||
if not rc:
|
||||
if isinstance(geom, rg.LineCurve):
|
||||
pl = rg.Polyline([geom.PointAtStart, geom.PointAtEnd])
|
||||
else:
|
||||
return None
|
||||
segs.append([pl[i] for i in range(pl.Count)])
|
||||
# Erste Seg orientieren: ihr letzter Punkt MUSS am Joint mit Seg[1]
|
||||
# sitzen. Wenn nicht (Joint ist an seg0[0]) → seg0 flippen. Sonst
|
||||
# springt die Polyline spaeter quer durch den Raum.
|
||||
s0, s1 = segs[0], segs[1]
|
||||
tol = 1e-3
|
||||
if (s0[-1].DistanceTo(s1[0]) > tol
|
||||
and s0[-1].DistanceTo(s1[-1]) > tol):
|
||||
segs[0] = s0[::-1]
|
||||
# Folge-Segs flippen wenn ihr Start nicht am prev_end sitzt.
|
||||
pts = list(segs[0])
|
||||
for seg in segs[1:]:
|
||||
prev_end = pts[-1]
|
||||
d_start = prev_end.DistanceTo(seg[0])
|
||||
d_end = prev_end.DistanceTo(seg[-1])
|
||||
if d_end < d_start:
|
||||
seg = seg[::-1]
|
||||
# erstes pt deckt sich mit prev_end → ueberspringen (sonst Duplikat
|
||||
# im Knick → Offset bricht oder erzeugt komische Mitten-Diskontinuitaet)
|
||||
pts.extend(seg[1:])
|
||||
if len(pts) < 2: return None
|
||||
return rg.PolylineCurve(rg.Polyline(pts))
|
||||
|
||||
|
||||
def _find_dependent_walls(doc, geschoss_id, moving_wall_id, old_curve, new_curve,
|
||||
pos_tol=0.01):
|
||||
"""Findet alle Waende deren Geometrie sich aendert wenn moving_wall sich
|
||||
@@ -2212,6 +2380,7 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
|
||||
raum_rundung=None, raum_txt_h=None,
|
||||
raum_align=None, raum_sia=None, raum_fuellung=None,
|
||||
wand_layered=None, wand_layers=None, wand_layer_idx=None,
|
||||
wand_chain_members=None,
|
||||
aussp_parent=None):
|
||||
"""User-Strings auf die Object-Attributes setzen."""
|
||||
obj_attrs.SetUserString(_KEY_ID, wall_id)
|
||||
@@ -2393,6 +2562,17 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
|
||||
try: obj_attrs.SetUserString(_KEY_WAND_LAYER_IDX,
|
||||
"{}".format(int(wand_layer_idx)))
|
||||
except Exception: pass
|
||||
if wand_chain_members is not None:
|
||||
try:
|
||||
import json as _json
|
||||
if isinstance(wand_chain_members, str):
|
||||
obj_attrs.SetUserString(_KEY_WAND_CHAIN_MEMBERS,
|
||||
wand_chain_members)
|
||||
else:
|
||||
obj_attrs.SetUserString(_KEY_WAND_CHAIN_MEMBERS,
|
||||
_json.dumps(list(wand_chain_members),
|
||||
ensure_ascii=False))
|
||||
except Exception: pass
|
||||
# Decken-Aussparung
|
||||
if aussp_parent is not None:
|
||||
obj_attrs.SetUserString(_KEY_AUSSP_PARENT, str(aussp_parent))
|
||||
@@ -2562,6 +2742,15 @@ def _read_meta(obj):
|
||||
except Exception: pass
|
||||
try: w_layer_idx = int(a.GetUserString(_KEY_WAND_LAYER_IDX) or "-1")
|
||||
except Exception: w_layer_idx = -1
|
||||
w_chain_raw = a.GetUserString(_KEY_WAND_CHAIN_MEMBERS) or ""
|
||||
w_chain_members = []
|
||||
if w_chain_raw:
|
||||
try:
|
||||
import json as _json
|
||||
parsed = _json.loads(w_chain_raw)
|
||||
if isinstance(parsed, list):
|
||||
w_chain_members = [str(x) for x in parsed if x]
|
||||
except Exception: pass
|
||||
aussp_parent_raw = a.GetUserString(_KEY_AUSSP_PARENT) or ""
|
||||
return {
|
||||
"id": a.GetUserString(_KEY_ID) or "",
|
||||
@@ -2628,6 +2817,7 @@ def _read_meta(obj):
|
||||
"wand_layered": w_layered,
|
||||
"wand_layers": w_layers,
|
||||
"wand_layer_idx": w_layer_idx,
|
||||
"wand_chain_members": w_chain_members,
|
||||
"aussp_parent": aussp_parent_raw,
|
||||
}
|
||||
except Exception:
|
||||
@@ -2652,8 +2842,17 @@ def _find_axis(doc, wall_id):
|
||||
|
||||
|
||||
def _find_volume(doc, wall_id):
|
||||
# Direkter Hit: Volume gehoert genau dieser Wand
|
||||
for obj, meta in _find_objects_by_wall_id(doc, wall_id, "wand_volume"):
|
||||
return obj
|
||||
# Indirekter Hit: Wand ist Non-Anchor in einem Chain — Volume haengt am
|
||||
# Anchor, fuehrt aber wand_chain_members das diese Wand enthaelt
|
||||
for obj in doc.Objects:
|
||||
meta = _read_meta(obj)
|
||||
if meta and meta.get("type") == "wand_volume":
|
||||
members = meta.get("wand_chain_members") or []
|
||||
if wall_id in members:
|
||||
return obj
|
||||
return None
|
||||
|
||||
|
||||
@@ -4885,13 +5084,94 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
"""Eigentliche Implementierung des Regen — der aeussere Wrapper
|
||||
`_regenerate_element` setzt _REGEN_BUSY und dispatcht oeffnung_point."""
|
||||
if meta["type"] == "wand_axis":
|
||||
# Chain-Detection: wenn diese Wand mit Nachbarn (gleiche Geometrie/
|
||||
# Material/Hoehe, 2-Wall-Joints) zu einem Polyline-Chain gehoert,
|
||||
# baut nur die Anchor-Wand (alphabetisch kleinste ID) das gemeinsame
|
||||
# Volume. Andere Chain-Members loeschen ggf. ihr eigenes Volume und
|
||||
# triggern Anchor-Regen.
|
||||
chain_ids = []
|
||||
try:
|
||||
chain_ids = _find_wall_chain(doc, element_id)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] chain detect:", ex)
|
||||
if len(chain_ids) > 1:
|
||||
anchor = _chain_anchor(chain_ids)
|
||||
if anchor != element_id:
|
||||
# Non-Anchor: eigenes altes Volume entfernen, Anchor regenen
|
||||
for o, _m in _find_objects_by_wall_id(doc, element_id,
|
||||
"wand_volume"):
|
||||
try: doc.Objects.Delete(o.Id, True)
|
||||
except Exception: pass
|
||||
try: _regenerate_element(doc, anchor)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] anchor regen:", ex)
|
||||
# Pruefen: hat Anchor ein Chain-Volume gebaut das uns deckt?
|
||||
# Wenn ja → fertig. Wenn nein (z.B. Polyline-Build failed) →
|
||||
# weiterlaufen + uns als Solo-Wand bauen.
|
||||
covered = False
|
||||
for obj in doc.Objects:
|
||||
m = _read_meta(obj)
|
||||
if m and m.get("type") == "wand_volume":
|
||||
members = m.get("wand_chain_members") or []
|
||||
if element_id in members:
|
||||
covered = True; break
|
||||
if covered:
|
||||
return True
|
||||
# Anchor hat keine Chain — wir bauen Solo. chain_ids leer
|
||||
# damit chain_set + Miter-Logik wie Solo wirkt.
|
||||
chain_ids = []
|
||||
# Anchor: Polyline aus den Chain-Achsen bauen + als geom verwenden.
|
||||
# Vor-Verifikation: Brep-Build mit Polyline probieren. Wenn das
|
||||
# fehlschlaegt (z.B. Innenseite-Offset self-intersect) → Chain-
|
||||
# Modus dropen + per-Wand regenen. Sonst gibt's invisible walls.
|
||||
chain_curve = None
|
||||
try:
|
||||
chain_curve = _build_chain_polyline(doc, chain_ids)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] chain polyline:", ex)
|
||||
if chain_curve is not None:
|
||||
_test_uk, _test_ok = _resolve_uk_ok(doc, meta["geschoss"],
|
||||
meta["uk_override"],
|
||||
meta["ok_override"])
|
||||
_test_brep = _make_volume_geometry(
|
||||
chain_curve, meta["dicke"], _test_uk, _test_ok,
|
||||
meta.get("referenz", "mid"))
|
||||
if _test_brep is None:
|
||||
print("[ELEMENTE] chain {} brep build FAILED — fallback "
|
||||
"per-wand (Offset evtl. mehrteilig)".format(chain_ids))
|
||||
chain_curve = None
|
||||
if chain_curve is None:
|
||||
# Chain-Modus dropen: Anchor verhaelt sich wie Solo-Wand.
|
||||
# chain_ids leerziehen damit chain_set spaeter auch nur den
|
||||
# element_id enthaelt (Miter-Logik wirkt wie Solo).
|
||||
# Andere Chain-Members werden NICHT requeued — sonst Loop
|
||||
# (sie wuerden chain detecten + Anchor anrufen + wieder
|
||||
# failure). Stattdessen: ihre eigenen Regen-Calls
|
||||
# (Move-Listener → deps) fallen via covered-Check durch
|
||||
# auf Solo. Bei Erstellung wird _make_wall_from_axis ohnehin
|
||||
# _find_dependent_walls auf alle benachbarten Waende anwenden.
|
||||
chain_ids = []
|
||||
else:
|
||||
# Stale wand_volumes der anderen Chain-Members raeumen
|
||||
for wid in chain_ids:
|
||||
if wid == element_id: continue
|
||||
for o, _m in _find_objects_by_wall_id(doc, wid,
|
||||
"wand_volume"):
|
||||
try: doc.Objects.Delete(o.Id, True)
|
||||
except Exception: pass
|
||||
geom = chain_curve
|
||||
uk, ok = _resolve_uk_ok(doc, meta["geschoss"],
|
||||
meta["uk_override"], meta["ok_override"])
|
||||
print("[ELEMENTE] regen wand {}: uk={:.3f} ok={:.3f} (uk_over='{}' ok_over='{}')".format(
|
||||
element_id, uk, ok, meta.get("uk_override", ""), meta.get("ok_override", "")))
|
||||
print("[ELEMENTE] regen wand {}: uk={:.3f} ok={:.3f} chain={} (uk_over='{}' ok_over='{}')".format(
|
||||
element_id, uk, ok, len(chain_ids) if chain_ids else 1,
|
||||
meta.get("uk_override", ""), meta.get("ok_override", "")))
|
||||
# Wand-Verbindungen: Miter-Linien aus Nachbarwand-Joints (Corner + T).
|
||||
# Bei Chain: alle Chain-Members ausschliessen, weil sie an Polyline-
|
||||
# Endpunkten dort nicht als "externe Nachbarn" zaehlen sollen — die
|
||||
# liegen ja schon in der gejointen Polyline drin.
|
||||
miter_start = None
|
||||
miter_end = None
|
||||
chain_set = set(chain_ids) if (chain_ids and len(chain_ids) > 1) else {element_id}
|
||||
try:
|
||||
joints = _collect_wall_joints(doc, meta["geschoss"])
|
||||
out_s, out_e = _wall_out_dirs(geom)
|
||||
@@ -4901,7 +5181,7 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
key_s = _pt_key(p_s)
|
||||
partners_s = [(wid, end, od)
|
||||
for (wid, end, od) in joints.get(key_s, [])
|
||||
if wid != element_id]
|
||||
if wid not in chain_set]
|
||||
if len(partners_s) == 1:
|
||||
_wid, _end, other_out = partners_s[0]
|
||||
mdir = _miter_dir(out_s, other_out)
|
||||
@@ -4909,7 +5189,8 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
miter_start = (p_s, mdir)
|
||||
elif len(partners_s) == 0:
|
||||
tj = _detect_t_junction(doc, meta["geschoss"],
|
||||
element_id, p_s)
|
||||
element_id, p_s,
|
||||
exclude_ids=chain_set)
|
||||
if tj is not None:
|
||||
_oid, b_tan, b_dicke = tj
|
||||
tm = _t_junction_miter(p_s, out_s, b_tan, b_dicke)
|
||||
@@ -4918,7 +5199,7 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
key_e = _pt_key(p_e)
|
||||
partners_e = [(wid, end, od)
|
||||
for (wid, end, od) in joints.get(key_e, [])
|
||||
if wid != element_id]
|
||||
if wid not in chain_set]
|
||||
if len(partners_e) == 1:
|
||||
_wid, _end, other_out = partners_e[0]
|
||||
mdir = _miter_dir(out_e, other_out)
|
||||
@@ -4926,7 +5207,8 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
miter_end = (p_e, mdir)
|
||||
elif len(partners_e) == 0:
|
||||
tj = _detect_t_junction(doc, meta["geschoss"],
|
||||
element_id, p_e)
|
||||
element_id, p_e,
|
||||
exclude_ids=chain_set)
|
||||
if tj is not None:
|
||||
_oid, b_tan, b_dicke = tj
|
||||
tm = _t_junction_miter(p_e, out_e, b_tan, b_dicke)
|
||||
@@ -4948,12 +5230,29 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
geom, meta["dicke"], uk, ok,
|
||||
meta.get("referenz", "mid"),
|
||||
miter_start=miter_start, miter_end=miter_end)
|
||||
if single_brep is None:
|
||||
# Bei Chain: Polyline ist Curve, Single-Wall-Logik koennte
|
||||
# an mehrteiliger Curve scheitern. Pro-Segment-Fallback.
|
||||
if (chain_ids and len(chain_ids) > 1
|
||||
and isinstance(geom, rg.PolylineCurve)):
|
||||
print("[ELEMENTE] chain volume geom FAILED, fallback "
|
||||
"per-segment (chain={})".format(chain_ids))
|
||||
else:
|
||||
print("[ELEMENTE] wand volume geom FAILED for {}".format(
|
||||
element_id))
|
||||
layer_breps = [(single_brep, "", "")] if single_brep else []
|
||||
|
||||
# Oeffnungen einsammeln + Cutouts pro Schicht anwenden.
|
||||
# Chain: alle Member-Walls liefern Oeffnungen — der Anchor baut ein
|
||||
# gemeinsames Brep, alle Cutouts werden dort eingearbeitet.
|
||||
opening_jobs = []
|
||||
cutouts = []
|
||||
for op_obj, op_meta in _find_openings_for_wall(doc, element_id):
|
||||
opening_walls = (chain_ids if (chain_ids and len(chain_ids) > 1)
|
||||
else [element_id])
|
||||
all_openings = []
|
||||
for _wid in opening_walls:
|
||||
all_openings.extend(_find_openings_for_wall(doc, _wid))
|
||||
for op_obj, op_meta in all_openings:
|
||||
pt_geom = op_obj.Geometry
|
||||
pt_loc = None
|
||||
if hasattr(pt_geom, 'Location'):
|
||||
@@ -5181,7 +5480,9 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
meta.get("referenz", "mid"),
|
||||
wand_layered=is_layered,
|
||||
wand_layers=layers_json,
|
||||
wand_layer_idx=idx)
|
||||
wand_layer_idx=idx,
|
||||
wand_chain_members=(chain_ids
|
||||
if (chain_ids and len(chain_ids) > 1) else None))
|
||||
try: doc.Objects.AddBrep(lbrep, attrs)
|
||||
except Exception as ex: print("[ELEMENTE] AddBrep wand layer:", ex)
|
||||
return True
|
||||
@@ -9431,6 +9732,14 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
try:
|
||||
dicke = sum(float(l.get("dicke", 0)) for l in wand_layers)
|
||||
except Exception: pass
|
||||
# ALTE Chain merken: Property-Aenderung kann Chain brechen
|
||||
# (z.B. dicke geaendert -> nicht mehr kompatibel). Nach dem Regen
|
||||
# muessen ehemalige Chain-Members eigenes Volume erhalten.
|
||||
old_chain_members = []
|
||||
if old_meta["type"] == "wand_axis":
|
||||
try:
|
||||
old_chain_members = _find_wall_chain(doc, wall_id)
|
||||
except Exception: pass
|
||||
_attach_meta(attrs, wall_id, old_meta["type"], geschoss, dicke,
|
||||
uk_over, ok_over, referenz,
|
||||
neigung=neigung, eave_idx=eave_idx, dach_typ=dach_typ,
|
||||
@@ -9442,6 +9751,13 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
axis_obj.CommitChanges()
|
||||
# Volumen regenerieren (Layer ggf. anpassen)
|
||||
_regenerate_volume(doc, wall_id)
|
||||
# Ehemalige Chain-Members re-regen damit sie ihr eigenes Volume
|
||||
# bekommen (oder einen neuen Anchor bilden)
|
||||
for wid in old_chain_members:
|
||||
if wid == wall_id: continue
|
||||
try: _regenerate_element(doc, wid)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] old-chain regen:", ex)
|
||||
doc.Views.Redraw()
|
||||
self._send_state()
|
||||
|
||||
@@ -9973,19 +10289,41 @@ def _on_object_replaced_body(sender, e):
|
||||
|
||||
# Oeffnungen entlang der neuen Achse migrieren + Regen einreihen.
|
||||
if meta.get("type") == "wand_axis":
|
||||
old_geom = e.OldRhinoObject.Geometry if e.OldRhinoObject else None
|
||||
new_geom = e.NewRhinoObject.Geometry if e.NewRhinoObject else None
|
||||
# No-Op-Check: GripsOn/CommitChanges feuert Replace ohne Geometrie-
|
||||
# Aenderung. Triggert sonst eine ganze Regen-Kette die in eigenem
|
||||
# Undo-Record laeuft (Cmd+Z muesste mehrfach gedrueckt werden).
|
||||
# Endpunkt-Vergleich reicht fuer LineCurves; Polylines/Splines
|
||||
# checken zusaetzlich Mid-Punkt + Laenge.
|
||||
unchanged = False
|
||||
try:
|
||||
if (isinstance(old_geom, rg.Curve)
|
||||
and isinstance(new_geom, rg.Curve)):
|
||||
p_o_s, p_o_e = old_geom.PointAtStart, old_geom.PointAtEnd
|
||||
p_n_s, p_n_e = new_geom.PointAtStart, new_geom.PointAtEnd
|
||||
tol = 1e-6
|
||||
if (p_o_s.DistanceTo(p_n_s) < tol
|
||||
and p_o_e.DistanceTo(p_n_e) < tol):
|
||||
# Endpunkte gleich — Polyline/Spline-Mitten checken
|
||||
try:
|
||||
l_o = old_geom.GetLength()
|
||||
l_n = new_geom.GetLength()
|
||||
if abs(l_o - l_n) < tol * 100:
|
||||
unchanged = True
|
||||
except Exception:
|
||||
unchanged = True
|
||||
except Exception: unchanged = False
|
||||
if unchanged:
|
||||
# Nur Grips-Toggle / Attribut-Aenderung — kein Regen noetig.
|
||||
return
|
||||
# Joint-Cache invalidieren — Wand hat sich geaendert
|
||||
_invalidate_joints_cache(meta.get("geschoss"))
|
||||
try:
|
||||
old_geom = e.OldRhinoObject.Geometry if e.OldRhinoObject else None
|
||||
new_geom = e.NewRhinoObject.Geometry if e.NewRhinoObject else None
|
||||
_migrate_openings_to_new_axis(meta["id"], old_geom, new_geom)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] migrate openings:", ex)
|
||||
# Wand-Verbindungen: alle ABHAENGIGEN Waende mit re-regenerieren.
|
||||
# Das umfasst sowohl Corner-Partner (Endpunkte teilen) als auch
|
||||
# T-Stoss-Wande (Endpunkt liegt auf der bewegten Achse). Wir
|
||||
# checken gegen ALTE und NEUE Geometrie damit auch sich-loesende
|
||||
# Verbindungen erkannt werden.
|
||||
try:
|
||||
doc2 = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc2 is not None:
|
||||
@@ -11420,6 +11758,37 @@ def _on_command_end(sender, e):
|
||||
elif canonical is not None:
|
||||
pure_transform = canonical
|
||||
|
||||
# Chain-Volumes-Pre-Check (basierend auf EXISTIERENDEN wand_chain_members,
|
||||
# nicht auf Live-Detection — sonst sieht Pre-Check schon die NEUE
|
||||
# Topologie und merkt zerbrochene Chains nicht):
|
||||
# Wenn ein chain-volume einen unserer moved walls referenziert UND nicht
|
||||
# ALLE chain_members mit-bewegt wurden → pure-transform ist unsicher
|
||||
# (Chain-Volume kann nicht einfach mit-translatiert werden, Chain muss
|
||||
# neu vermessen + rebuilded werden). Wenn alle members im Move → Volume
|
||||
# darf mit pure-transform folgen.
|
||||
chain_volume_ids_to_follow = set()
|
||||
affected_chain_members = set() # alle wall_ids deren Chain teil-bewegt wurde
|
||||
if pure_transform is not None:
|
||||
moved_axis_ids = set()
|
||||
for moved_id in moved_ids:
|
||||
old = sources_snap.get(moved_id) or {}
|
||||
if old.get("type") == "wand_axis":
|
||||
moved_axis_ids.add(moved_id)
|
||||
if moved_axis_ids:
|
||||
for obj in doc.Objects:
|
||||
m = _read_meta(obj)
|
||||
if not m or m.get("type") != "wand_volume": continue
|
||||
members = m.get("wand_chain_members") or []
|
||||
if not members: continue
|
||||
if not any(wid in moved_axis_ids for wid in members):
|
||||
continue
|
||||
if not all(wid in moved_axis_ids for wid in members):
|
||||
print("[ELEMENTE] chain partial-move → abort pure-transform")
|
||||
affected_chain_members.update(members)
|
||||
pure_transform = None
|
||||
else:
|
||||
chain_volume_ids_to_follow.add(str(obj.Id))
|
||||
|
||||
if pure_transform is not None:
|
||||
# PURE-TRANSFORM PFAD: Transform auf alle Geometries anwenden die
|
||||
# nicht schon vom User-Move transformed wurden. Funktioniert fuer
|
||||
@@ -11433,11 +11802,14 @@ def _on_command_end(sender, e):
|
||||
tx, ty, tz, rot_deg))
|
||||
|
||||
# Eltern→Kind-Cascade: nur bewegte Sources + deren Children folgen.
|
||||
def _should_follow(m):
|
||||
# Chain-Volumes folgen wenn alle Chain-Members im Move waren.
|
||||
def _should_follow(m, obj_id_str=""):
|
||||
eid = m.get("id")
|
||||
if eid in moved_ids: return True
|
||||
parent = m.get("oeff_parent")
|
||||
if parent and parent in moved_ids: return True
|
||||
if obj_id_str and obj_id_str in chain_volume_ids_to_follow:
|
||||
return True
|
||||
return False
|
||||
|
||||
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
|
||||
@@ -11448,7 +11820,7 @@ def _on_command_end(sender, e):
|
||||
m = _read_meta(obj)
|
||||
if not m: continue
|
||||
t = m.get("type")
|
||||
if not _should_follow(m): continue
|
||||
if not _should_follow(m, str(obj.Id)): continue
|
||||
# Sources die nicht bewegt wurden (= identity transform)
|
||||
# transformen — nur via _should_follow erlaubt (Cascade).
|
||||
if t in SOURCE_TYPES:
|
||||
@@ -11618,6 +11990,12 @@ def _on_command_end(sender, e):
|
||||
sc.sticky[_REGEN_BUSY] = _was_busy
|
||||
sc.sticky["_dossier_skip_sync_regen"] = None
|
||||
|
||||
# Chain-Survivor: alle Members einer teil-bewegten Chain regenen, auch
|
||||
# wenn der einzelne Member nicht in affected_walls steckt. Sonst bleibt
|
||||
# ein non-anchor-survivor ohne Volume zurueck (sein altes Volume gehoerte
|
||||
# der inzwischen geloeschten/verschobenen Anchor).
|
||||
for wid in affected_chain_members:
|
||||
affected_walls.add(wid)
|
||||
# Sync-Regen aller betroffenen Wände — Move ist sauber abgeschlossen,
|
||||
# kein Konflikt mehr moeglich. EIN Regen pro Wand (nicht pro Oeffnung).
|
||||
# Display bleibt suppressed bis ALLE Wände durch sind → kein „Aufbauen".
|
||||
|
||||
Reference in New Issue
Block a user