[ADD] Kreisel-Split (links/rechts) bei Kollisionspruefung + Separator/Scanner-Zuordnung

- export_neighbors.py: kreisel_half_bboxes() teilt die Kreisel-BBox entlang
  der Achse durch Antriebs-/Spannstation (AN8/SP8) in eine linke und eine
  rechte Haelfte (je ca. 400mm breit, siehe [Kreisel] durchmesser_mm in
  cfg/export.cfg, gespiegelt von component_defaults.json). compute_neighbor_ids
  ersetzt jeden Kreisel-Eintrag in der Kollisionsgruppe durch dieses Paar
  (TeileId-L/-R) statt einer einzelnen BBox - Nachbarn einer Gefaellestrecke/
  eines Foerderers zeigen jetzt, welche Haelfte tatsaechlich beruehrt.
- compute_sensor_zuordnung(): ordnet jeden Separator/Scanner per BBox-
  Ueberschneidung seiner Gefaellestrecke, seinem Foerderer oder einer
  Kreiselhaelfte zu (Prioritaet in dieser Reihenfolge), ersetzt die
  ZUORDNUNG-Attribut-Merkmale im CSV-Export bei Treffer.
- compute_scanner_nearest_separator(): sucht zusaetzlich, unabhaengig von der
  Zuordnung, zu jedem Scanner den raeumlich naechstgelegenen Separator.
- ssg_id.lsp: ssg-id-collect-blocks erfasst jetzt auch Separator_SP/S-LP/
  Scanner-Bloecke, damit ssg-id-check-all ihr (neues) ID-Attribut vor jedem
  Export befuellt; export_csv.py bevorzugt dieses echte ID-Attribut als
  TeileId und faellt nur bei leerem Attribut auf eine laufende SEP-/SCN-
  Ersatznummer zurueck.
- data/ils/Scanner_2D.dwg, Scanner_3D.dwg, Separator_SP_2D.dwg: ID-ATTDEF
  ergaenzt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 11:21:16 +02:00
parent 88e0aace42
commit f6461aab0f
7 changed files with 361 additions and 26 deletions
+73 -18
View File
@@ -38,7 +38,20 @@ Fehler = "unverbunden" (keine Partner, aber mindestens einer noetig),
(Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/
Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/
Foerderer/Strecke-Modul mindestens zwei (siehe export_neighbors.py
MIN_PARTNER).
MIN_PARTNER). Ein Kreisel wird dabei NICHT als eine, sondern als zwei
BBoxen (links/rechts, geteilt entlang der Achse durch Antriebs-/
Spannstation, siehe export_neighbors.kreisel_half_bboxes) gegen
Gefaellestrecke/Foerderer getestet - deren "Nachbarn"-Spalte zeigt darum
"<KreiselTeileId>-L" bzw. "-R" statt nur der Kreisel-TeileId.
Separator/Scanner-Zeilen bekommen zusaetzlich ein berechnetes "Zuordnung"-
Merkmal (Gefaellestrecke > Foerderer > Kreiselhaelfte, per BBox-Ueber-
schneidung - siehe export_neighbors.compute_sensor_zuordnung), das die
ATTRIB-Zuordnung (falls das Attribut ZUORDNUNG in der Zeichnung von Hand
gesetzt wurde) ersetzt, sofern ein Treffer gefunden wird. Scanner-Zeilen
bekommen zusaetzlich ein "Naechster Separator"-Merkmal (raeumlich naechst-
gelegener Separator, euklidischer Abstand der BBox-Mittelpunkte - eigene,
von der Zuordnung unabhaengige Suche, siehe export_neighbors.compute_
scanner_nearest_separator).
Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer
EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. Insertpoint/
K1-K4 stehen zwar in export_raw.json fuer jeden Export zur Verfuegung (siehe
@@ -55,15 +68,20 @@ from export_planquadrat import load_planquadrat_config, resolve_origins, compute
from export_neighbors import (
load_neighbor_tolerance_mm,
load_omniflo_cell_size_mm,
load_kreisel_durchmesser_mm,
load_collision_debug_target,
compute_neighbor_ids,
compute_neighbor_errors,
compute_sensor_zuordnung,
compute_scanner_nearest_separator,
SENSOR_TEILEARTEN,
)
BLOCKPATTERNS = load_patterns()
PLANQUADRAT_CFG = load_planquadrat_config()
NEIGHBOR_TOLERANCE_MM = load_neighbor_tolerance_mm()
NEIGHBOR_OMNIFLO_CELL_SIZE_MM = load_omniflo_cell_size_mm()
KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm()
NEIGHBOR_DEBUG_TARGET = load_collision_debug_target()
@@ -191,9 +209,9 @@ def build_gerade_merkmale(block):
def build_sensor_merkmale(block):
"""Merkmale-Dict fuer einen eigenstaendigen Separator_SP/S-LP- oder
Scanner-Block (siehe pattern_separator/pattern_scanner in cfg/export.cfg).
Diese Bloecke haben (anders als Kreisel/GF/VF) kein ID-Attribut -
ZUORDNUNG/ARTINR/LAYER sind die einzigen ATTDEFs (siehe ils-insert-sensor/
mubea:build-separator-one)."""
ZUORDNUNG/ARTINR/LAYER (+ neuerdings ID) sind die ATTDEFs (siehe
ils-insert-sensor/mubea:build-separator-one, ssg-id-collect-blocks in
ssg_id.lsp)."""
attribs = block.get("attribs", {})
return {
"Zuordnung": attribs.get("ZUORDNUNG", ""),
@@ -204,13 +222,17 @@ def build_sensor_merkmale(block):
def format_sensor_id(prefix, n):
"""Erzeugt eine laufende TeileId fuer Separator/Scanner-Zeilen im CSV-Export.
"""Fallback-TeileId fuer Separator/Scanner-Zeilen im CSV-Export, falls das
Block-Attribut "ID" (noch) leer ist.
Diese Bloecke haben KEIN persistiertes ID-Attribut in der Zeichnung
(anders als Kreisel/GF/VF, deren "ID" von ssg-id-generate/ssg-id-check-all
beim Insert bzw. vor jedem Export vergeben wird) - der Praefix macht das
sichtbar: eine rein fuer diesen Exportlauf erzeugte, NICHT ueber mehrere
Exportlaeufe hinweg stabile laufende Nummer, keine echte Block-ID.
Separator_SP/Scanner haben seit kurzem ein eigenes ID-ATTDEF (siehe
ssg-id-collect-blocks in ssg_id.lsp) - normalerweise wird also das echte,
ueber ssg-id-generate/ssg-id-check-all vergebene "ID"-Attribut als
TeileId verwendet, genau wie bei Kreisel/GF/VF. Nur wenn das (noch)
fehlt (z.B. bei aelteren, noch nicht neu exportierten Bloecken), erzeugt
diese Funktion eine rein fuer DIESEN Exportlauf gueltige, NICHT ueber
mehrere Exportlaeufe hinweg stabile Ersatznummer - der Praefix macht
sichtbar, dass es keine echte, persistierte Block-ID ist.
"""
return f"{prefix}-{n:04d}"
@@ -489,6 +511,15 @@ def process_blocks(blocks, lookup, dbg=None):
"bezeichnung": f"Kreisel :{kreisel_count}",
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_kreisel_merkmale(block),
# x/y/rotation/attribs (roh, nicht nur die Merkmale) werden
# von export_neighbors.kreisel_half_bboxes gebraucht, um die
# Links/Rechts-Trennung ueber AN8/SP8/ABSTAND nachzurechnen -
# bbox_columns() liefert nur die fertige Gesamt-BBox, keine
# Rohdaten dafuer.
"x": block.get("x", 0.0),
"y": block.get("y", 0.0),
"rotation": block.get("rotation", 0.0),
"attribs": block.get("attribs", {}),
**bbox_columns(block),
})
continue
@@ -508,18 +539,23 @@ def process_blocks(blocks, lookup, dbg=None):
})
continue
# Separator (Separator_SP/S-LP) - eigene TeileArt-Zeile mit laufender
# ID (siehe format_sensor_id; kein echtes ID-Attribut am Block, anders
# als Kreisel/GF/VF). EXPORTSIVAS schliesst denselben Blockpattern
# bewusst aus (siehe export_sivas.py) - dort zaehlt bereits
# ANZAHL_SEPARATOR am zugehoerigen Kreisel/GF/VF.
# Separator (Separator_SP/S-LP) - eigene TeileArt-Zeile. TeileId: das
# echte ID-Attribut, falls vorhanden (Separator_SP/Scanner haben seit
# kurzem ein eigenes ID-ATTDEF, siehe ssg-id-collect-blocks in
# ssg_id.lsp - ssg-id-check-all fuellt es vor jedem Export). Fallback
# format_sensor_id (laufende SEP-/SCN-Nummer NUR fuer diesen Export-
# lauf) fuer aeltere Bloecke, deren ID-Attribut noch leer ist (z.B.
# vor dem naechsten Insert/ssg-id-check-all-Durchlauf).
# EXPORTSIVAS schliesst denselben Blockpattern bewusst aus (siehe
# export_sivas.py) - dort zaehlt bereits ANZAHL_SEPARATOR am
# zugehoerigen Kreisel/GF/VF.
if matches_any(bname, BLOCKPATTERNS.get("pattern_separator", [])):
elem_nr += 1
separator_count += 1
items.append({
"nr": elem_nr,
"teileart": "ILS 2.0 Separator",
"teileid": format_sensor_id("SEP", separator_count),
"teileid": block.get("attribs", {}).get("ID") or format_sensor_id("SEP", separator_count),
"bezeichnung": f"Separator :{separator_count}",
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_sensor_merkmale(block),
@@ -534,7 +570,7 @@ def process_blocks(blocks, lookup, dbg=None):
items.append({
"nr": elem_nr,
"teileart": "ILS 2.0 Scanner",
"teileid": format_sensor_id("SCN", scanner_count),
"teileid": block.get("attribs", {}).get("ID") or format_sensor_id("SCN", scanner_count),
"bezeichnung": f"Scanner :{scanner_count}",
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_sensor_merkmale(block),
@@ -563,12 +599,31 @@ def process_blocks(blocks, lookup, dbg=None):
})
neighbor_ids = compute_neighbor_ids(
items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM, dbg=dbg)
items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM,
kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg)
neighbor_errors = compute_neighbor_errors(items, neighbor_ids)
for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors):
item["nachbarn"] = nachbarn
item["fehler"] = fehler
# Separator/Scanner: Zuordnung zu Gefaellestrecke/Foerderer/Kreiselhaelfte
# per BBox-Ueberschneidung berechnen (Prioritaet siehe compute_sensor_
# zuordnung) - ersetzt die von Hand gesetzte ZUORDNUNG-Attribut-Merkmale
# nur, wenn tatsaechlich ein Treffer gefunden wurde.
sensor_zuordnung = compute_sensor_zuordnung(
items, NEIGHBOR_TOLERANCE_MM, kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg)
for item, zuordnung in zip(items, sensor_zuordnung):
if item.get("teileart", "") in SENSOR_TEILEARTEN and zuordnung:
item["merkmale"]["Zuordnung"] = zuordnung
# NACH der Separator-Zuordnung: zu jedem Scanner den raeumlich naechst-
# gelegenen Separator suchen (eigene, unabhaengige Suche - siehe
# compute_scanner_nearest_separator).
nearest_separator = compute_scanner_nearest_separator(items, dbg=dbg)
for item, separator_id in zip(items, nearest_separator):
if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id:
item["merkmale"]["Naechster Separator"] = separator_id
return items