[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:
+7
-1
@@ -28,7 +28,12 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --- Alle exportierbaren INSERT-Entities sammeln ---
|
;; --- Alle exportierbaren INSERT-Entities sammeln ---
|
||||||
;; Gleiche Filterlogik wie csv:collect-export-blocks
|
;; Gleiche Filterlogik wie csv:collect-export-blocks (siehe dort auch
|
||||||
|
;; pattern_separator/pattern_scanner in cfg/export.cfg - Separator_SP/S-LP/
|
||||||
|
;; Scanner sind eigenstaendige Sensor-Bloecke, die inzwischen ein eigenes
|
||||||
|
;; ID-ATTDEF haben und daher hier ebenfalls erfasst werden muessen, sonst
|
||||||
|
;; bleibt ihr ID-Attribut trotz ATTDEF für immer leer - ssg-attrib-set-on
|
||||||
|
;; kann nur einen Wert SETZEN, wenn der Block ueberhaupt erst hier landet).
|
||||||
(defun ssg-id-collect-blocks ( / ss-all ss-out i ename ed bname)
|
(defun ssg-id-collect-blocks ( / ss-all ss-out i ename ed bname)
|
||||||
(setq ss-all (ssget "X" (list (cons 0 "INSERT"))))
|
(setq ss-all (ssget "X" (list (cons 0 "INSERT"))))
|
||||||
(if (null ss-all)
|
(if (null ss-all)
|
||||||
@@ -43,6 +48,7 @@
|
|||||||
(wcmatch bname "KR_*,KREISEL_*,ECKRAD_*")
|
(wcmatch bname "KR_*,KREISEL_*,ECKRAD_*")
|
||||||
(wcmatch (strcase bname) "AP110*,AP_110*,AP60*,AP_60*,APG110*")
|
(wcmatch (strcase bname) "AP110*,AP_110*,AP60*,AP_60*,APG110*")
|
||||||
(wcmatch bname "Vario*,Staustrecke*,AUS_Element*,EIN_Element*,VF_*,GF_*")
|
(wcmatch bname "Vario*,Staustrecke*,AUS_Element*,EIN_Element*,VF_*,GF_*")
|
||||||
|
(wcmatch bname "Separator_SP*,S-LP*,Scanner*")
|
||||||
(wcmatch bname "#*")
|
(wcmatch bname "#*")
|
||||||
)
|
)
|
||||||
(ssadd ename ss-out)
|
(ssadd ename ss-out)
|
||||||
|
|||||||
@@ -115,3 +115,18 @@ debug_log=1
|
|||||||
# nicht gesetzt.
|
# nicht gesetzt.
|
||||||
bb_minimum_height_mm=1
|
bb_minimum_height_mm=1
|
||||||
|
|
||||||
|
[Kreisel]
|
||||||
|
# Kreisel-Durchmesser (Antriebs-/Spannstation AN8/SP8) in mm. Quelle der
|
||||||
|
# Wahrheit ist data/json/component_defaults.json ("kreisel"."durchmesser",
|
||||||
|
# Default 800.0) - hier fuer Python gespiegelt (Aenderungen bitte an beiden
|
||||||
|
# Stellen nachziehen, siehe Hinweis oben bei [blockpattern]).
|
||||||
|
#
|
||||||
|
# Wird von lib/export_neighbors.py (kreisel_half_bboxes) benutzt, um die
|
||||||
|
# Bounding-Box jedes Kreisels bei der Kollisionspruefung in eine linke und
|
||||||
|
# eine rechte Haelfte zu teilen: AN8 (Antriebsstation) liegt lokal bei
|
||||||
|
# (radius, 0), SP8 (Spannstation) bei (radius+abstand, 0) - siehe
|
||||||
|
# KreiselInsert.lsp::draw-module - radius = durchmesser_mm/2 (Default 400mm).
|
||||||
|
# Die gesamte Geometrie ist symmetrisch um die lokale X-Achse (Y=0, die
|
||||||
|
# Linie durch beide Stationsmitten) - genau diese Linie trennt links/rechts.
|
||||||
|
durchmesser_mm=800
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+73
-18
@@ -38,7 +38,20 @@ Fehler = "unverbunden" (keine Partner, aber mindestens einer noetig),
|
|||||||
(Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/
|
(Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/
|
||||||
Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/
|
Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/
|
||||||
Foerderer/Strecke-Modul mindestens zwei (siehe export_neighbors.py
|
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
|
Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer
|
||||||
EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. Insertpoint/
|
EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. Insertpoint/
|
||||||
K1-K4 stehen zwar in export_raw.json fuer jeden Export zur Verfuegung (siehe
|
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 (
|
from export_neighbors import (
|
||||||
load_neighbor_tolerance_mm,
|
load_neighbor_tolerance_mm,
|
||||||
load_omniflo_cell_size_mm,
|
load_omniflo_cell_size_mm,
|
||||||
|
load_kreisel_durchmesser_mm,
|
||||||
load_collision_debug_target,
|
load_collision_debug_target,
|
||||||
compute_neighbor_ids,
|
compute_neighbor_ids,
|
||||||
compute_neighbor_errors,
|
compute_neighbor_errors,
|
||||||
|
compute_sensor_zuordnung,
|
||||||
|
compute_scanner_nearest_separator,
|
||||||
|
SENSOR_TEILEARTEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
BLOCKPATTERNS = load_patterns()
|
BLOCKPATTERNS = load_patterns()
|
||||||
PLANQUADRAT_CFG = load_planquadrat_config()
|
PLANQUADRAT_CFG = load_planquadrat_config()
|
||||||
NEIGHBOR_TOLERANCE_MM = load_neighbor_tolerance_mm()
|
NEIGHBOR_TOLERANCE_MM = load_neighbor_tolerance_mm()
|
||||||
NEIGHBOR_OMNIFLO_CELL_SIZE_MM = load_omniflo_cell_size_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()
|
NEIGHBOR_DEBUG_TARGET = load_collision_debug_target()
|
||||||
|
|
||||||
|
|
||||||
@@ -191,9 +209,9 @@ def build_gerade_merkmale(block):
|
|||||||
def build_sensor_merkmale(block):
|
def build_sensor_merkmale(block):
|
||||||
"""Merkmale-Dict fuer einen eigenstaendigen Separator_SP/S-LP- oder
|
"""Merkmale-Dict fuer einen eigenstaendigen Separator_SP/S-LP- oder
|
||||||
Scanner-Block (siehe pattern_separator/pattern_scanner in cfg/export.cfg).
|
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 (+ neuerdings ID) sind die ATTDEFs (siehe
|
||||||
ZUORDNUNG/ARTINR/LAYER sind die einzigen ATTDEFs (siehe ils-insert-sensor/
|
ils-insert-sensor/mubea:build-separator-one, ssg-id-collect-blocks in
|
||||||
mubea:build-separator-one)."""
|
ssg_id.lsp)."""
|
||||||
attribs = block.get("attribs", {})
|
attribs = block.get("attribs", {})
|
||||||
return {
|
return {
|
||||||
"Zuordnung": attribs.get("ZUORDNUNG", ""),
|
"Zuordnung": attribs.get("ZUORDNUNG", ""),
|
||||||
@@ -204,13 +222,17 @@ def build_sensor_merkmale(block):
|
|||||||
|
|
||||||
|
|
||||||
def format_sensor_id(prefix, n):
|
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
|
Separator_SP/Scanner haben seit kurzem ein eigenes ID-ATTDEF (siehe
|
||||||
(anders als Kreisel/GF/VF, deren "ID" von ssg-id-generate/ssg-id-check-all
|
ssg-id-collect-blocks in ssg_id.lsp) - normalerweise wird also das echte,
|
||||||
beim Insert bzw. vor jedem Export vergeben wird) - der Praefix macht das
|
ueber ssg-id-generate/ssg-id-check-all vergebene "ID"-Attribut als
|
||||||
sichtbar: eine rein fuer diesen Exportlauf erzeugte, NICHT ueber mehrere
|
TeileId verwendet, genau wie bei Kreisel/GF/VF. Nur wenn das (noch)
|
||||||
Exportlaeufe hinweg stabile laufende Nummer, keine echte Block-ID.
|
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}"
|
return f"{prefix}-{n:04d}"
|
||||||
|
|
||||||
@@ -489,6 +511,15 @@ def process_blocks(blocks, lookup, dbg=None):
|
|||||||
"bezeichnung": f"Kreisel :{kreisel_count}",
|
"bezeichnung": f"Kreisel :{kreisel_count}",
|
||||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||||
"merkmale": build_kreisel_merkmale(block),
|
"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),
|
**bbox_columns(block),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
@@ -508,18 +539,23 @@ def process_blocks(blocks, lookup, dbg=None):
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Separator (Separator_SP/S-LP) - eigene TeileArt-Zeile mit laufender
|
# Separator (Separator_SP/S-LP) - eigene TeileArt-Zeile. TeileId: das
|
||||||
# ID (siehe format_sensor_id; kein echtes ID-Attribut am Block, anders
|
# echte ID-Attribut, falls vorhanden (Separator_SP/Scanner haben seit
|
||||||
# als Kreisel/GF/VF). EXPORTSIVAS schliesst denselben Blockpattern
|
# kurzem ein eigenes ID-ATTDEF, siehe ssg-id-collect-blocks in
|
||||||
# bewusst aus (siehe export_sivas.py) - dort zaehlt bereits
|
# ssg_id.lsp - ssg-id-check-all fuellt es vor jedem Export). Fallback
|
||||||
# ANZAHL_SEPARATOR am zugehoerigen Kreisel/GF/VF.
|
# 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", [])):
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_separator", [])):
|
||||||
elem_nr += 1
|
elem_nr += 1
|
||||||
separator_count += 1
|
separator_count += 1
|
||||||
items.append({
|
items.append({
|
||||||
"nr": elem_nr,
|
"nr": elem_nr,
|
||||||
"teileart": "ILS 2.0 Separator",
|
"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}",
|
"bezeichnung": f"Separator :{separator_count}",
|
||||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||||
"merkmale": build_sensor_merkmale(block),
|
"merkmale": build_sensor_merkmale(block),
|
||||||
@@ -534,7 +570,7 @@ def process_blocks(blocks, lookup, dbg=None):
|
|||||||
items.append({
|
items.append({
|
||||||
"nr": elem_nr,
|
"nr": elem_nr,
|
||||||
"teileart": "ILS 2.0 Scanner",
|
"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}",
|
"bezeichnung": f"Scanner :{scanner_count}",
|
||||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||||
"merkmale": build_sensor_merkmale(block),
|
"merkmale": build_sensor_merkmale(block),
|
||||||
@@ -563,12 +599,31 @@ def process_blocks(blocks, lookup, dbg=None):
|
|||||||
})
|
})
|
||||||
|
|
||||||
neighbor_ids = compute_neighbor_ids(
|
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)
|
neighbor_errors = compute_neighbor_errors(items, neighbor_ids)
|
||||||
for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors):
|
for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors):
|
||||||
item["nachbarn"] = nachbarn
|
item["nachbarn"] = nachbarn
|
||||||
item["fehler"] = fehler
|
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
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+266
-7
@@ -33,13 +33,20 @@ MIN_PARTNER) - Ergebnis ist die CSV-Spalte "Fehler" in export_csv.py.
|
|||||||
Nur von export_csv.py genutzt (EXPORTCSV), nicht von export_sivas.py.
|
Nur von export_csv.py genutzt (EXPORTCSV), nicht von export_sivas.py.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from export_blockpatterns import cfg_path_from_env, load_export_cfg
|
from export_blockpatterns import cfg_path_from_env, load_export_cfg, safe_float
|
||||||
|
|
||||||
KREISEL_TEILEARTEN = {"ILS 2.0 Kreisel", "ILS 2.0 Eckrad"}
|
KREISEL_TEILEARTEN = {"ILS 2.0 Kreisel", "ILS 2.0 Eckrad"}
|
||||||
STRECKEN_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"}
|
STRECKEN_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"}
|
||||||
OMNIFLO_TEILEARTEN = {"Omniflo Kurve", "Omniflo Weiche", "Omniflo Gerade"}
|
OMNIFLO_TEILEARTEN = {"Omniflo Kurve", "Omniflo Weiche", "Omniflo Gerade"}
|
||||||
|
SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"}
|
||||||
|
|
||||||
|
# Nur "ILS 2.0 Kreisel" (echte Kreisel-Bloecke mit AN8/Antrieb + SP8/Spann-
|
||||||
|
# station + ABSTAND-Attribut) werden links/rechts gesplittet - "ILS 2.0
|
||||||
|
# Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox.
|
||||||
|
KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel"
|
||||||
|
|
||||||
# Mindestanzahl Partner (Nachbarn) je TeileArt fuer die Fehlerspalte "Fehler"
|
# Mindestanzahl Partner (Nachbarn) je TeileArt fuer die Fehlerspalte "Fehler"
|
||||||
# (siehe compute_neighbor_errors): Kreisel/Eckrad und Omniflo-Elemente
|
# (siehe compute_neighbor_errors): Kreisel/Eckrad und Omniflo-Elemente
|
||||||
@@ -74,6 +81,16 @@ def load_omniflo_cell_size_mm(cfg_path=None):
|
|||||||
return parser.getfloat("Nachbarschaft", "omniflo_zellgroesse_mm", fallback=3000.0)
|
return parser.getfloat("Nachbarschaft", "omniflo_zellgroesse_mm", fallback=3000.0)
|
||||||
|
|
||||||
|
|
||||||
|
def load_kreisel_durchmesser_mm(cfg_path=None):
|
||||||
|
"""Liest den Kreisel-Durchmesser (mm) aus export.cfg. Default: 800.0
|
||||||
|
(siehe [Kreisel] durchmesser_mm - gespiegelter Wert von
|
||||||
|
data/json/component_defaults.json "kreisel"."durchmesser")."""
|
||||||
|
if cfg_path is None:
|
||||||
|
cfg_path = cfg_path_from_env()
|
||||||
|
parser = load_export_cfg(cfg_path)
|
||||||
|
return parser.getfloat("Kreisel", "durchmesser_mm", fallback=800.0)
|
||||||
|
|
||||||
|
|
||||||
def load_collision_debug_target(cfg_path=None):
|
def load_collision_debug_target(cfg_path=None):
|
||||||
"""Zielpfad fuer das Kollisions-/Nachbarschafts-Debuglog aus export.cfg.
|
"""Zielpfad fuer das Kollisions-/Nachbarschafts-Debuglog aus export.cfg.
|
||||||
|
|
||||||
@@ -120,6 +137,73 @@ def _add_neighbor(result, idx_a, idx_b, id_a, id_b):
|
|||||||
result[idx_b].append(id_a)
|
result[idx_b].append(id_a)
|
||||||
|
|
||||||
|
|
||||||
|
def kreisel_half_bboxes(block, durchmesser_mm):
|
||||||
|
"""Teilt die Welt-Bounding-Box eines "ILS 2.0 Kreisel"-Blocks entlang der
|
||||||
|
Kreiselachse (Linie durch die Mitten von Antriebs- (AN8) und Spann-
|
||||||
|
station (SP8)) in eine linke und eine rechte Haelfte.
|
||||||
|
|
||||||
|
Geometrie (siehe KreiselInsert.lsp::draw-module): AN8 liegt in lokalen
|
||||||
|
(unrotierten) Blockkoordinaten bei (radius, 0), SP8 bei
|
||||||
|
(radius+abstand, 0) - radius = durchmesser_mm/2 (Default 400mm, siehe
|
||||||
|
[Kreisel] in cfg/export.cfg). Beide Stationen liegen exakt auf der
|
||||||
|
lokalen X-Achse (Y=0); die gesamte Geometrie (zwei Kreise + zwei
|
||||||
|
Tangenten bei Y=+-radius) ist symmetrisch dazu. Die Trennlinie durch die
|
||||||
|
Stationsmitten IST also die lokale X-Achse selbst, und teilt die
|
||||||
|
Bounding-Box in zwei GLEICH GROSSE Haelften mit je "radius" (Default
|
||||||
|
400mm) Breite - "links" = lokal Y in [0, +radius], "rechts" = lokal Y in
|
||||||
|
[-radius, 0] (Blickrichtung von Antriebs- zu Spannstation, +X; mathe-
|
||||||
|
matische Standardkonvention: mathematisch positive/CCW-Drehung von +X
|
||||||
|
fuehrt nach +Y, also links).
|
||||||
|
|
||||||
|
Block-Insertpunkt (world x,y) und -rotation (world "rotation", Grad,
|
||||||
|
CCW positiv) kommen direkt aus block (siehe csv:block-to-json). Die
|
||||||
|
ABSTAND-Attribut wird fuer die lokale Laenge (0..abstand+durchmesser)
|
||||||
|
benoetigt.
|
||||||
|
|
||||||
|
Rueckgabe: (bbox_links, bbox_rechts), je ein dict {"cx","cy","dx","dy"} -
|
||||||
|
die Welt-AABB der jeweils rotierten Haelfte (bei Rotationen, die kein
|
||||||
|
Vielfaches von 90 Grad sind, umschliesst die AABB die tatsaechliche
|
||||||
|
Haelfte etwas grosszuegiger, wie jede achsparallele Bounding-Box einer
|
||||||
|
gedrehten Flaeche - konsistent mit jeder anderen Bounding-Box in diesem
|
||||||
|
Modul).
|
||||||
|
"""
|
||||||
|
attribs = block.get("attribs", {})
|
||||||
|
abstand_mm = safe_float(attribs.get("ABSTAND", "0"), 0.0)
|
||||||
|
radius_mm = durchmesser_mm / 2.0
|
||||||
|
x0 = block.get("x", 0.0) or 0.0
|
||||||
|
y0 = block.get("y", 0.0) or 0.0
|
||||||
|
rot_rad = math.radians(block.get("rotation", 0.0) or 0.0)
|
||||||
|
cos_r, sin_r = math.cos(rot_rad), math.sin(rot_rad)
|
||||||
|
|
||||||
|
def world(local_x, local_y):
|
||||||
|
return (
|
||||||
|
x0 + local_x * cos_r - local_y * sin_r,
|
||||||
|
y0 + local_x * sin_r + local_y * cos_r,
|
||||||
|
)
|
||||||
|
|
||||||
|
# AN8-Kreis reicht lokal bis X=0 (Mitte radius, Radius radius); SP8-Kreis
|
||||||
|
# bis X=abstand+durchmesser (Mitte radius+abstand, Radius radius).
|
||||||
|
local_x_max = abstand_mm + durchmesser_mm
|
||||||
|
|
||||||
|
def half_bbox(y_lo, y_hi):
|
||||||
|
corners = [
|
||||||
|
world(0.0, y_lo), world(local_x_max, y_lo),
|
||||||
|
world(local_x_max, y_hi), world(0.0, y_hi),
|
||||||
|
]
|
||||||
|
xs = [c[0] for c in corners]
|
||||||
|
ys = [c[1] for c in corners]
|
||||||
|
minx, maxx = min(xs), max(xs)
|
||||||
|
miny, maxy = min(ys), max(ys)
|
||||||
|
return {
|
||||||
|
"cx": (minx + maxx) / 2.0, "cy": (miny + maxy) / 2.0,
|
||||||
|
"dx": maxx - minx, "dy": maxy - miny,
|
||||||
|
}
|
||||||
|
|
||||||
|
bbox_links = half_bbox(0.0, radius_mm)
|
||||||
|
bbox_rechts = half_bbox(-radius_mm, 0.0)
|
||||||
|
return bbox_links, bbox_rechts
|
||||||
|
|
||||||
|
|
||||||
def _fmt_bounds(b):
|
def _fmt_bounds(b):
|
||||||
"""(minx, maxx, miny, maxy) kompakt fuer das Debuglog."""
|
"""(minx, maxx, miny, maxy) kompakt fuer das Debuglog."""
|
||||||
return f"[x {b[0]:.0f}..{b[1]:.0f} | y {b[2]:.0f}..{b[3]:.0f}]"
|
return f"[x {b[0]:.0f}..{b[1]:.0f} | y {b[2]:.0f}..{b[3]:.0f}]"
|
||||||
@@ -132,12 +216,21 @@ def _dbg_pair(dbg, id_a, id_b, overlap):
|
|||||||
|
|
||||||
def _test_group_pairs(group, result, dbg=None):
|
def _test_group_pairs(group, result, dbg=None):
|
||||||
"""Alle Paare EINER Gruppe gegeneinander testen (O(n^2) - fuer Kreisel/
|
"""Alle Paare EINER Gruppe gegeneinander testen (O(n^2) - fuer Kreisel/
|
||||||
Eckrad-Stueckzahlen pro Zeichnung unkritisch)."""
|
Eckrad-Stueckzahlen pro Zeichnung unkritisch).
|
||||||
|
|
||||||
|
idx_a == idx_b wird uebersprungen: seit dem Kreisel-Links/Rechts-Split
|
||||||
|
(siehe kreisel_half_bboxes) kann dieselbe Original-idx zweimal in der
|
||||||
|
Gruppe auftauchen (die linke und die rechte Haelfte DESSELBEN Kreisels) -
|
||||||
|
die beiden beruehren sich zwangslaeufig an der Trennlinie und waeren
|
||||||
|
sonst faelschlich gegenseitige "Nachbarn".
|
||||||
|
"""
|
||||||
n = len(group)
|
n = len(group)
|
||||||
for a in range(n):
|
for a in range(n):
|
||||||
idx_a, bounds_a, id_a = group[a]
|
idx_a, bounds_a, id_a = group[a]
|
||||||
for b in range(a + 1, n):
|
for b in range(a + 1, n):
|
||||||
idx_b, bounds_b, id_b = group[b]
|
idx_b, bounds_b, id_b = group[b]
|
||||||
|
if idx_a == idx_b:
|
||||||
|
continue
|
||||||
overlap = _overlaps(bounds_a, bounds_b)
|
overlap = _overlaps(bounds_a, bounds_b)
|
||||||
_dbg_pair(dbg, id_a, id_b, overlap)
|
_dbg_pair(dbg, id_a, id_b, overlap)
|
||||||
if overlap:
|
if overlap:
|
||||||
@@ -195,7 +288,8 @@ def _test_omniflo_grid(group, cell_size_mm, result, dbg=None):
|
|||||||
_add_neighbor(result, idx_a, idx_b, id_a, id_b)
|
_add_neighbor(result, idx_a, idx_b, id_a, id_b)
|
||||||
|
|
||||||
|
|
||||||
def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=None):
|
def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0,
|
||||||
|
kreisel_durchmesser_mm=800.0, dbg=None):
|
||||||
"""Ermittelt je Item die IDs (item["teileid"]) benachbarter Elemente.
|
"""Ermittelt je Item die IDs (item["teileid"]) benachbarter Elemente.
|
||||||
|
|
||||||
items = Liste von dict mit "teileart", "teileid" und optional "_bbox"
|
items = Liste von dict mit "teileart", "teileid" und optional "_bbox"
|
||||||
@@ -203,12 +297,23 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=N
|
|||||||
ohne _bbox (z.B. die synthetische Omniflo-Sum-Zeile) bleiben ohne
|
ohne _bbox (z.B. die synthetische Omniflo-Sum-Zeile) bleiben ohne
|
||||||
Nachbarn.
|
Nachbarn.
|
||||||
|
|
||||||
|
"ILS 2.0 Kreisel"-Items werden dabei NICHT als eine BBox, sondern als
|
||||||
|
ZWEI gegeneinander getestet: eine linke und eine rechte Haelfte (siehe
|
||||||
|
kreisel_half_bboxes) - die Kreisel-Gruppe fuer die Kollisionspruefung
|
||||||
|
besteht also aus <teileid>-L/<teileid>-R statt <teileid> je Kreisel
|
||||||
|
("ILS 2.0 Eckrad" bleibt unveraendert eine einzelne BBox). Ein
|
||||||
|
angrenzendes Element (z.B. eine Gefaellestrecke) bekommt dadurch in
|
||||||
|
seiner eigenen Nachbarliste "<teileid>-L" oder "<teileid>-R" statt nur
|
||||||
|
"<teileid>" - je nachdem, welche Haelfte tatsaechlich ueberschneidet.
|
||||||
|
|
||||||
dbg = optionale Callable(str) fuer ein Debugprotokoll (Klassifikation,
|
dbg = optionale Callable(str) fuer ein Debugprotokoll (Klassifikation,
|
||||||
Gruppen, jeder Ueberschneidungstest, Ergebnis). None = kein Protokoll.
|
Gruppen, jeder Ueberschneidungstest, Ergebnis). None = kein Protokoll.
|
||||||
|
|
||||||
Rueckgabe: Liste von kommaseparierten Nachbar-ID-Strings, positionsgleich
|
Rueckgabe: Liste von kommaseparierten Nachbar-ID-Strings, positionsgleich
|
||||||
zu items (nicht ueber die ID dedupliziert, da TeileId nicht zwingend
|
zu items. Innerhalb eines Items dedupliziert (identische IDs entfernt -
|
||||||
eindeutig ist).
|
das passiert z.B., wenn ein Kreisel dieselbe Strecke ueber BEIDE
|
||||||
|
Haelften beruehrt), sonst nicht global ueber alle Items hinweg (TeileId
|
||||||
|
ist nicht zwingend eindeutig).
|
||||||
"""
|
"""
|
||||||
half_tol = tolerance_mm / 2.0
|
half_tol = tolerance_mm / 2.0
|
||||||
result = [[] for _ in items]
|
result = [[] for _ in items]
|
||||||
@@ -221,7 +326,8 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=N
|
|||||||
if dbg:
|
if dbg:
|
||||||
dbg("=== Nachbarschafts-/Kollisionserkennung ===")
|
dbg("=== Nachbarschafts-/Kollisionserkennung ===")
|
||||||
dbg(f"Toleranz={tolerance_mm}mm (Bounding-Box je Seite +{half_tol}mm erweitert), "
|
dbg(f"Toleranz={tolerance_mm}mm (Bounding-Box je Seite +{half_tol}mm erweitert), "
|
||||||
f"Omniflo-Zellgroesse={omniflo_cell_size_mm}mm")
|
f"Omniflo-Zellgroesse={omniflo_cell_size_mm}mm, "
|
||||||
|
f"Kreisel-Durchmesser={kreisel_durchmesser_mm}mm (Radius je Haelfte)")
|
||||||
dbg(f"Elemente gesamt: {len(items)}")
|
dbg(f"Elemente gesamt: {len(items)}")
|
||||||
dbg("")
|
dbg("")
|
||||||
dbg("Klassifikation je Element:")
|
dbg("Klassifikation je Element:")
|
||||||
@@ -237,6 +343,20 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=N
|
|||||||
continue
|
continue
|
||||||
teileart = item.get("teileart", "")
|
teileart = item.get("teileart", "")
|
||||||
teileid = item.get("teileid", "")
|
teileid = item.get("teileid", "")
|
||||||
|
if teileart == KREISEL_SPLIT_TEILEART:
|
||||||
|
gruppe = "Kreisel (links/rechts gesplittet)"
|
||||||
|
bbox_links, bbox_rechts = kreisel_half_bboxes(item, kreisel_durchmesser_mm)
|
||||||
|
bounds_links = _bounds(bbox_links, half_tol)
|
||||||
|
bounds_rechts = _bounds(bbox_rechts, half_tol)
|
||||||
|
kreisel_group.append((idx, bounds_links, f"{teileid}-L"))
|
||||||
|
kreisel_group.append((idx, bounds_rechts, f"{teileid}-R"))
|
||||||
|
if dbg:
|
||||||
|
dbg(f" idx={idx} teileid={teileid!r} teileart={teileart!r} "
|
||||||
|
f"-> {gruppe}; links bbox(cx={bbox_links['cx']:.0f},cy={bbox_links['cy']:.0f},"
|
||||||
|
f"dx={bbox_links['dx']:.0f},dy={bbox_links['dy']:.0f}) bounds={_fmt_bounds(bounds_links)}; "
|
||||||
|
f"rechts bbox(cx={bbox_rechts['cx']:.0f},cy={bbox_rechts['cy']:.0f},"
|
||||||
|
f"dx={bbox_rechts['dx']:.0f},dy={bbox_rechts['dy']:.0f}) bounds={_fmt_bounds(bounds_rechts)}")
|
||||||
|
continue
|
||||||
bounds = _bounds(bbox, half_tol)
|
bounds = _bounds(bbox, half_tol)
|
||||||
if teileart in KREISEL_TEILEARTEN:
|
if teileart in KREISEL_TEILEARTEN:
|
||||||
gruppe = "Kreisel/Eckrad"
|
gruppe = "Kreisel/Eckrad"
|
||||||
@@ -276,7 +396,13 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=N
|
|||||||
# 3. Omniflo gegen Omniflo, rasterbasiert vorgefiltert
|
# 3. Omniflo gegen Omniflo, rasterbasiert vorgefiltert
|
||||||
_test_omniflo_grid(omniflo_group, omniflo_cell_size_mm, result, dbg)
|
_test_omniflo_grid(omniflo_group, omniflo_cell_size_mm, result, dbg)
|
||||||
|
|
||||||
neighbor_id_lists = [", ".join(neighbor_ids) for neighbor_ids in result]
|
# dict.fromkeys() dedupliziert unter Erhalt der Reihenfolge - relevant
|
||||||
|
# seit dem Kreisel-Split: beruehrt eine Strecke BEIDE Haelften desselben
|
||||||
|
# Kreisels nicht (normalerweise ausgeschlossen), waere sie sonst nicht
|
||||||
|
# betroffen; umgekehrt kann derselbe Strecken-Nachbar im Kreisel-Eintrag
|
||||||
|
# theoretisch doppelt auftauchen, wenn (Rand-/Toleranzfall) beide
|
||||||
|
# Haelften ihn beruehren.
|
||||||
|
neighbor_id_lists = [", ".join(dict.fromkeys(neighbor_ids)) for neighbor_ids in result]
|
||||||
|
|
||||||
if dbg:
|
if dbg:
|
||||||
dbg("")
|
dbg("")
|
||||||
@@ -316,3 +442,136 @@ def compute_neighbor_errors(items, neighbor_ids):
|
|||||||
else:
|
else:
|
||||||
errors.append("")
|
errors.append("")
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def compute_sensor_zuordnung(items, tolerance_mm, kreisel_durchmesser_mm=800.0, dbg=None):
|
||||||
|
"""Ordnet jeden Separator/Scanner (SENSOR_TEILEARTEN) per Bounding-Box-
|
||||||
|
Ueberschneidung seiner Gefaellestrecke, seinem Foerderer oder einer
|
||||||
|
Kreiselhaelfte (links/rechts, siehe kreisel_half_bboxes) zu.
|
||||||
|
|
||||||
|
Eigener, unabhaengiger Durchlauf NACH compute_neighbor_ids (nicht in
|
||||||
|
dessen Gruppenaufbau integriert, da Separatoren/Scanner dort bewusst
|
||||||
|
keine eigene Gruppe sind - Nachbarschaft und Zuordnung sind zwei
|
||||||
|
unterschiedliche Fragen: Nachbarschaft = "beruehrt sich", Zuordnung =
|
||||||
|
"gehoert zu genau einem Trägerelement"). Prioritaet bei mehreren
|
||||||
|
Treffern: Gefaellestrecke vor Foerderer vor Kreiselhaelfte (ein
|
||||||
|
Separator/Scanner haengt fachlich an genau einem dieser drei).
|
||||||
|
|
||||||
|
items = dieselbe Liste wie bei compute_neighbor_ids (braucht "teileart",
|
||||||
|
"teileid" und "_bbox").
|
||||||
|
|
||||||
|
Rueckgabe: Liste von Zuordnungs-Strings, positionsgleich zu items -
|
||||||
|
"" fuer Items, die keine SENSOR_TEILEART sind oder keine BBox haben.
|
||||||
|
Bei Treffer: die TeileId der Gefaellestrecke/des Foerderers, oder
|
||||||
|
"<KreiselTeileId>-L"/"-R" bei einem Kreisel-Treffer. Kein Treffer bei
|
||||||
|
vorhandener BBox: "nicht zugeordnet".
|
||||||
|
"""
|
||||||
|
half_tol = tolerance_mm / 2.0
|
||||||
|
|
||||||
|
gf_entries = []
|
||||||
|
vf_entries = []
|
||||||
|
kreisel_entries = []
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
bbox = item.get("_bbox")
|
||||||
|
if not bbox:
|
||||||
|
continue
|
||||||
|
teileart = item.get("teileart", "")
|
||||||
|
teileid = item.get("teileid", "")
|
||||||
|
if teileart == "ILS 2.0 Gefaellestrecke":
|
||||||
|
gf_entries.append((teileid, _bounds(bbox, half_tol)))
|
||||||
|
elif teileart in ("ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"):
|
||||||
|
vf_entries.append((teileid, _bounds(bbox, half_tol)))
|
||||||
|
elif teileart == KREISEL_SPLIT_TEILEART:
|
||||||
|
bbox_links, bbox_rechts = kreisel_half_bboxes(item, kreisel_durchmesser_mm)
|
||||||
|
kreisel_entries.append((f"{teileid}-L", _bounds(bbox_links, half_tol)))
|
||||||
|
kreisel_entries.append((f"{teileid}-R", _bounds(bbox_rechts, half_tol)))
|
||||||
|
|
||||||
|
if dbg:
|
||||||
|
dbg("")
|
||||||
|
dbg("=== Separator/Scanner-Zuordnung (GF > Foerderer > Kreiselhaelfte) ===")
|
||||||
|
dbg(f"Kandidaten: {len(gf_entries)} Gefaellestrecke(n), "
|
||||||
|
f"{len(vf_entries)} Foerderer/Streckenmodul(e), "
|
||||||
|
f"{len(kreisel_entries)} Kreiselhaelfte(n)")
|
||||||
|
|
||||||
|
def _find(bounds, candidates):
|
||||||
|
for teileid, other_bounds in candidates:
|
||||||
|
if _overlaps(bounds, other_bounds):
|
||||||
|
return teileid
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for item in items:
|
||||||
|
teileart = item.get("teileart", "")
|
||||||
|
if teileart not in SENSOR_TEILEARTEN:
|
||||||
|
result.append("")
|
||||||
|
continue
|
||||||
|
bbox = item.get("_bbox")
|
||||||
|
if not bbox:
|
||||||
|
result.append("")
|
||||||
|
continue
|
||||||
|
bounds = _bounds(bbox, half_tol)
|
||||||
|
treffer = (_find(bounds, gf_entries)
|
||||||
|
or _find(bounds, vf_entries)
|
||||||
|
or _find(bounds, kreisel_entries)
|
||||||
|
or "nicht zugeordnet")
|
||||||
|
if dbg:
|
||||||
|
dbg(f" {item.get('teileart', '')} {item.get('teileid', '')!r} "
|
||||||
|
f"bounds={_fmt_bounds(bounds)} -> {treffer!r}")
|
||||||
|
result.append(treffer)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def compute_scanner_nearest_separator(items, dbg=None):
|
||||||
|
"""Sucht fuer jeden Scanner ("ILS 2.0 Scanner") den raeumlich naechst-
|
||||||
|
gelegenen Separator ("ILS 2.0 Separator") - euklidischer Abstand der
|
||||||
|
BBox-Mittelpunkte (cx/cy, Grundriss, Z ignoriert wie ueberall in diesem
|
||||||
|
Modul).
|
||||||
|
|
||||||
|
Eigener, unabhaengiger Durchlauf, der NACH compute_sensor_zuordnung
|
||||||
|
aufgerufen wird (die Zuordnung der Separatoren zu ihrer Gefaellestrecke/
|
||||||
|
ihrem Foerderer/ihrer Kreiselhaelfte steht damit bereits fest - diese
|
||||||
|
Funktion aendert daran nichts, sie sucht unabhaengig davon zusaetzlich
|
||||||
|
den naechsten Separator zu jedem Scanner). Es gibt KEINE Abstands-
|
||||||
|
obergrenze - "immer der naechstgelegene" bedeutet auch bei grosser
|
||||||
|
Entfernung einen Treffer, solange ueberhaupt ein Separator existiert.
|
||||||
|
|
||||||
|
Rueckgabe: Liste von TeileId-Strings (des jeweils naechsten Separators),
|
||||||
|
positionsgleich zu items - "" fuer Nicht-Scanner-Items, fuer Scanner ohne
|
||||||
|
BBox oder wenn ueberhaupt kein Separator mit BBox in der Zeichnung ist.
|
||||||
|
"""
|
||||||
|
separators = [
|
||||||
|
(item.get("teileid", ""), item["_bbox"])
|
||||||
|
for item in items
|
||||||
|
if item.get("teileart", "") == "ILS 2.0 Separator" and item.get("_bbox")
|
||||||
|
]
|
||||||
|
|
||||||
|
if dbg:
|
||||||
|
dbg("")
|
||||||
|
dbg("=== Scanner -> naechstgelegener Separator ===")
|
||||||
|
dbg(f"Separator-Kandidaten mit BBox: {len(separators)}")
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for item in items:
|
||||||
|
if item.get("teileart", "") != "ILS 2.0 Scanner":
|
||||||
|
result.append("")
|
||||||
|
continue
|
||||||
|
bbox = item.get("_bbox")
|
||||||
|
if not bbox or not separators:
|
||||||
|
result.append("")
|
||||||
|
if dbg:
|
||||||
|
dbg(f" Scanner {item.get('teileid', '')!r}: "
|
||||||
|
f"{'keine BBox' if not bbox else 'keine Separatoren vorhanden'} -> kein Treffer")
|
||||||
|
continue
|
||||||
|
cx, cy = bbox["cx"], bbox["cy"]
|
||||||
|
best_id, best_dist = "", None
|
||||||
|
for sep_id, sep_bbox in separators:
|
||||||
|
dist = math.hypot(sep_bbox["cx"] - cx, sep_bbox["cy"] - cy)
|
||||||
|
if best_dist is None or dist < best_dist:
|
||||||
|
best_dist = dist
|
||||||
|
best_id = sep_id
|
||||||
|
if dbg:
|
||||||
|
dbg(f" Scanner {item.get('teileid', '')!r} -> naechster Separator "
|
||||||
|
f"{best_id!r} (Abstand {best_dist:.0f}mm)")
|
||||||
|
result.append(best_id)
|
||||||
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user