Sepliste-XDATA fuer Separator/AS/ES-Reihenfolge + Unit-Tests, makunbound-Fix
Sammelt beim Bau von VF_n/GF_n-Ketten die Baureihenfolge der Separator-/ AS-/ES-Sub-Bloecke vor dem Block-Sweep und schreibt sie als XDATA auf den fertigen Wrapper (ssg_ks_insert.lsp); export.lsp liest sie fuer das neue "sepliste"-Feld im CSV-Export, export_neighbors.py nutzt es als Fallback zur BBox-Kollisionspruefung. Dazu Unit-Tests fuer die reinen Serialisierungs- funktionen (Chunking, Roundtrip) in test_unit.lsp. Ausserdem: makunbound (existiert nicht in BricsCAD-AutoLISP) aus test_run_all.lsp entfernt - verursachte Laufzeitfehler bei SSG_RUN_ALL_TESTS_EXPORT/_OFFEN. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1266,3 +1266,156 @@ def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0,
|
||||
f"Strecken mit Warnung (>2 Kreisel): {len(warnings)}")
|
||||
|
||||
return result, warnings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sepliste (LISP-XDATA, siehe ssg-sepliste-xdata-schreiben) -> Nachbarschaft
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# "sepliste" (item["sepliste"], von csv:block-to-json aus SSG_VF_SEP/
|
||||
# SSG_GF_SEP-XDATA gelesen) ist die beim Bau in Baureihenfolge gesammelte
|
||||
# Liste der Separator-/AS-/ES-Sub-Bloecke EINER VF_n/GF_n-Kette:
|
||||
# [{"typ": "AS"|"SEP"|"ES", "lfdnr": int, "x": float, "y": float}, ...].
|
||||
# Ersetzt das bisherige Raten der Ketten-internen Reihenfolge (die Linie
|
||||
# selbst wird nicht exportiert) durch die tatsaechliche Bau-Reihenfolge.
|
||||
|
||||
def compute_sep_kette(items, dbg=None):
|
||||
"""Baut je VF_n/GF_n-Item MIT "sepliste" die ketteninterne Vorgaenger-/
|
||||
Nachfolger-Beziehung AS -> Sep1 -> ... -> SepN -> ES.
|
||||
|
||||
Jeder Sepliste-Eintrag bekommt eine (innerhalb dieser Kette eindeutige)
|
||||
Schluessel-ID "<strecke_teileid>#<lfdnr>" - das ist die ID, mit der
|
||||
build_strecke_schleus_items/build_separator_kette_items die spaeteren
|
||||
globalen TeileIds verknuepft (siehe export_csv.py). Vorgaenger/Nachfolger
|
||||
sind schlicht der vorherige/naechste Eintrag in der Sepliste (die bereits
|
||||
in Baureihenfolge vorliegt) - kein Rateverfahren noetig.
|
||||
|
||||
Rueckgabe: dict {key: {"typ", "x", "y", "strecke_id", "vorgaenger",
|
||||
"nachfolger"}} - vorgaenger/nachfolger sind wieder solche keys, oder None
|
||||
am jeweiligen Kettenende (Verkettung mit dem Kreisel-Umlauf uebernimmt
|
||||
compute_kreisel_umlauf).
|
||||
"""
|
||||
result = {}
|
||||
for item in items:
|
||||
if item.get("teileart", "") not in GF_VF_TEILEARTEN:
|
||||
continue
|
||||
sepliste = item.get("sepliste")
|
||||
if not sepliste:
|
||||
continue
|
||||
sid = item.get("teileid", "")
|
||||
keys = [f"{sid}#{e.get('lfdnr')}" for e in sepliste]
|
||||
for i, entry in enumerate(sepliste):
|
||||
key = keys[i]
|
||||
result[key] = {
|
||||
"typ": entry.get("typ", ""),
|
||||
"x": entry.get("x", 0.0),
|
||||
"y": entry.get("y", 0.0),
|
||||
"strecke_id": sid,
|
||||
"vorgaenger": keys[i - 1] if i > 0 else None,
|
||||
"nachfolger": keys[i + 1] if i < len(keys) - 1 else None,
|
||||
}
|
||||
if dbg:
|
||||
kette_txt = " -> ".join(f"{e.get('typ')}:{e.get('lfdnr')}" for e in sepliste)
|
||||
dbg(f" Strecke {sid}: Sepliste-Kette {kette_txt}")
|
||||
return result
|
||||
|
||||
|
||||
def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, dbg=None):
|
||||
"""Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort
|
||||
liegenden Punkte - frei platzierte Separatoren (per compute_sensor_
|
||||
zuordnung bereits einer Kreiselhaelfte zugeordnet) UND die AS/ES-Enden
|
||||
aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe wie
|
||||
compute_strecke_kreisel_schleus zugeordnet) - in eine geschlossene
|
||||
Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut).
|
||||
|
||||
Winkel wird um den Kapsel-Mittelpunkt (Mitte AN8-/SP8-Achse, siehe
|
||||
_kreisel_capsule) gemessen, 0 Grad = Kapsel-Achsrichtung. UZS
|
||||
(Uhrzeigersinn, Default) sortiert nach fallendem Winkel, GUZ nach
|
||||
steigendem - beides ergibt einen konsistenten Umlauf, die Achsrichtung
|
||||
selbst ist nur der Nullpunkt.
|
||||
|
||||
items = dieselbe Liste wie bei compute_neighbor_ids/compute_sensor_
|
||||
zuordnung. sep_kette = Rueckgabe von compute_sep_kette (fuer die AS/ES-
|
||||
Punkte: x/y + strecke_id).
|
||||
|
||||
Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key,
|
||||
nachfolger_key), ...]} in Umlaufreihenfolge - punkt_key ist entweder eine
|
||||
Separator-TeileId (freie Separatoren) oder ein sep_kette-Schluessel
|
||||
(AS/ES-Enden). Kreisel ohne zugeordnete Punkte liefern eine leere Liste.
|
||||
"""
|
||||
half_tol = tolerance_mm / 2.0
|
||||
kreisel = {}
|
||||
for item in items:
|
||||
if item.get("teileart", "") == KREISEL_SPLIT_TEILEART:
|
||||
kreisel[item.get("teileid", "")] = (
|
||||
_kreisel_capsule(item, durchmesser_mm),
|
||||
(item.get("attribs", {}) or {}).get("DREHRICHTUNG", "UZS"))
|
||||
|
||||
# Freie Separatoren -> ihre Kreiselhaelfte (bereits per BBox-Ueberschneidung
|
||||
# bestimmbar, wie compute_sensor_zuordnung es fuer die "Zuordnung"-Spalte
|
||||
# tut - hier direkt gegen die volle Kreisel-BBox statt der Haelften, weil
|
||||
# nur "gehoert zu diesem Kreisel" gebraucht wird, nicht links/rechts).
|
||||
kreisel_bounds = {}
|
||||
for tid, (cap, _dr) in kreisel.items():
|
||||
acx = (cap["a"][0] + cap["b"][0]) / 2.0
|
||||
acy = (cap["a"][1] + cap["b"][1]) / 2.0
|
||||
achs_len = math.hypot(cap["b"][0] - cap["a"][0], cap["b"][1] - cap["a"][1])
|
||||
kreisel_bounds[tid] = _bounds(
|
||||
{"cx": acx, "cy": acy, "dx": durchmesser_mm + achs_len, "dy": durchmesser_mm},
|
||||
half_tol)
|
||||
|
||||
punkte_je_kreisel = {tid: [] for tid in kreisel}
|
||||
|
||||
for item in items:
|
||||
if item.get("teileart", "") != "ILS 2.0 Separator":
|
||||
continue
|
||||
bbox = item.get("_bbox")
|
||||
if not bbox:
|
||||
continue
|
||||
bounds = _bounds(bbox, half_tol)
|
||||
for tid, kb in kreisel_bounds.items():
|
||||
if _overlaps(bounds, kb):
|
||||
punkte_je_kreisel[tid].append(
|
||||
(item.get("teileid", ""), bbox.get("cx", 0.0), bbox.get("cy", 0.0)))
|
||||
break
|
||||
|
||||
# AS/ES-Enden jeder Kette mit sepliste: naechstgelegener Kreisel (wie
|
||||
# compute_strecke_kreisel_schleus._nearest), aber direkt aus sep_kette
|
||||
# statt ueber die grobe BBox-Nachbarschaft der Strecke.
|
||||
for key, eintrag in sep_kette.items():
|
||||
if eintrag["typ"] not in ("AS", "ES"):
|
||||
continue
|
||||
p = (eintrag["x"], eintrag["y"])
|
||||
best_tid, best_d = None, None
|
||||
for tid, (cap, _dr) in kreisel.items():
|
||||
q = _closest_pt_on_segment(cap["a"], cap["b"], p)
|
||||
d = math.hypot(q[0] - p[0], q[1] - p[1])
|
||||
if best_d is None or d < best_d:
|
||||
best_tid, best_d = tid, d
|
||||
if best_tid is not None:
|
||||
punkte_je_kreisel[best_tid].append((key, eintrag["x"], eintrag["y"]))
|
||||
|
||||
result = {}
|
||||
for tid, punkte in punkte_je_kreisel.items():
|
||||
cap, drehrichtung = kreisel[tid]
|
||||
acx = (cap["a"][0] + cap["b"][0]) / 2.0
|
||||
acy = (cap["a"][1] + cap["b"][1]) / 2.0
|
||||
achs_winkel = math.atan2(cap["b"][1] - cap["a"][1], cap["b"][0] - cap["a"][0])
|
||||
|
||||
def _winkel(pxy):
|
||||
return math.atan2(pxy[2] - acy, pxy[1] - acx) - achs_winkel
|
||||
|
||||
reverse = (drehrichtung or "UZS").strip().upper() != "GUZ"
|
||||
sortiert = sorted(punkte, key=lambda p: _winkel(p), reverse=reverse)
|
||||
n = len(sortiert)
|
||||
umlauf = []
|
||||
for i, p in enumerate(sortiert):
|
||||
vorgaenger = sortiert[(i - 1) % n][0] if n > 1 else None
|
||||
nachfolger = sortiert[(i + 1) % n][0] if n > 1 else None
|
||||
umlauf.append((p[0], vorgaenger, nachfolger))
|
||||
result[tid] = umlauf
|
||||
if dbg:
|
||||
dbg(f" Kreisel {tid} ({drehrichtung}): Umlauf "
|
||||
f"{' -> '.join(p[0] for p in sortiert)}")
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user