[CHANGE] Omniflo-Nachbarschaft: STRtree-Broad-Phase + KOS-Verfeinerung (10mm); Gerade-K1/K2 im CSV-Export

Die Omniflo-Nachbarschaftserkennung (export_neighbors.py, nur EXPORTCSV) wird
zweistufig:
- Broad-Phase ueber shapely-STRtree auf den Bounding-Boxen (Fallback: das
  bisherige Raster, falls shapely fehlt).
- Narrow-Phase (KOS-Verfeinerung): zwei moegliche Nachbarn gelten nur dann als
  benachbart, wenn ein Anschluss-Koordinatensystem K1-K4 des einen naeher als
  omniflo_ks_toleranz_mm (neu, Default 10mm) an einem K-Punkt des anderen
  liegt. Das verwirft bloss ueberlappende BBoxen ohne echten Anschluss.
  Elemente ohne K1-K4 fallen auf reine BBox-Ueberschneidung zurueck.

Geraden fuehren keine echten K-Bloecke in der Zeichnung; ihre K1 (Anfang) /
K2 (Ende) werden beim Export aus Einfuegepunkt + Laenge + Rotation
synthetisiert (csv:gerade-k-kos-strings in export.lsp) und stehen damit in den
CSV-Spalten K1/K2 - wie echte KOS - und nehmen an der Kollisionspruefung teil.

Verifiziert an results/test-omnicollision: gerichtete Nachbarschaften 20 -> 14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:56:10 +02:00
parent bd79e3d1b6
commit 9c8f84799c
5 changed files with 282 additions and 47 deletions
+40
View File
@@ -350,6 +350,37 @@
(if found (cdr found) "") (if found (cdr found) "")
) )
;; --- K1 (Anfang) / K2 (Ende) einer Omniflo-Gerade SYNTHETISIEREN ---
;; ename = Entity-Name des platzierten Gerade-INSERT.
;; pt/rot-rad = Welt-Einfuegepunkt (= Anfang) und -Rotation (radiant) des
;; Blocks (siehe csv:block-to-json).
;; Geraden fuehren - anders als Boegen/Weichen - KEINE echten K-Bloecke in der
;; Zeichnung. Fuer den Export werden ihre Anschluss-Koordinatensysteme aus der
;; Geometrie berechnet, als laegen dort echte KOS: K1 am Einfuegepunkt (Anfang
;; der lokalen Linie 0,0), K2 am Ende (laenge,0), ueber die Block-Rotation in
;; Weltkoordinaten gedreht. Laenge aus dem LAENGE- bzw. A-Attribut. Damit
;; koennen Geraden in der Python-Nachbarschaftserkennung (export_neighbors.py)
;; genauso ueber ihre KOS gegen andere Geraden/Boegen/Weichen geprueft werden.
;; Rueckgabe wie csv:get-k-kos-strings: (("K1" . str) ("K2" . str)) oder nil.
(defun csv:gerade-k-kos-strings (ename pt rot-rad / laenge-str laenge z end-pt)
(setq laenge-str (cond ((csv:get-attrib ename "LAENGE"))
((csv:get-attrib ename "A"))
(t nil)))
(if (and laenge-str (> (strlen laenge-str) 0))
(progn
(setq laenge (atof laenge-str))
(setq z (if (caddr pt) (caddr pt) 0.0))
(setq end-pt (list (+ (car pt) (* laenge (cos rot-rad)))
(+ (cadr pt) (* laenge (sin rot-rad)))
z))
(list
(cons "K1" (csv:trans-encode (car pt) (cadr pt) z))
(cons "K2" (csv:trans-encode (car end-pt) (cadr end-pt) (caddr end-pt))))
)
nil
)
)
;; --- Mindesthoehe der Bounding-Box (Z-Ausdehnung), aus cfg/export.cfg --- ;; --- Mindesthoehe der Bounding-Box (Z-Ausdehnung), aus cfg/export.cfg ---
;; [Boundingbox] bb_minimum_height_mm, Default 1 (mm) falls nicht gesetzt. ;; [Boundingbox] bb_minimum_height_mm, Default 1 (mm) falls nicht gesetzt.
@@ -446,6 +477,15 @@
(car pt) (cadr pt) (if (caddr pt) (caddr pt) 0.0) (car pt) (cadr pt) (if (caddr pt) (caddr pt) 0.0)
(nth 0 insert-quat) (nth 1 insert-quat) (nth 2 insert-quat) (nth 3 insert-quat))) (nth 0 insert-quat) (nth 1 insert-quat) (nth 2 insert-quat) (nth 3 insert-quat)))
(setq k-list (csv:get-k-kos-strings ename pt rotation)) (setq k-list (csv:get-k-kos-strings ename pt rotation))
;; Omniflo-Geraden fuehren keine echten K-Bloecke - K1/K2 aus der Geometrie
;; (Anfang/Ende ueber Laenge + Rotation) synthetisieren, damit sie im Export
;; wie echte Anschluss-Koordinatensysteme erscheinen und in der Nachbar-
;; schaftserkennung (export_neighbors.py) mitverwendet werden koennen.
(if (and (null k-list)
(wcmatch (strcase blk-name)
(export:pattern "pattern_gerade" "AP110*,AP_110*")))
(setq k-list (csv:gerade-k-kos-strings ename pt rotation))
)
(setq bbox-json "") (setq bbox-json "")
(if include-bbox (if include-bbox
(progn (progn
+21 -4
View File
@@ -104,13 +104,30 @@ format= {x}/{y}
toleranz_mm=200 toleranz_mm=200
# Rastergroesse (mm) fuer die Omniflo-Nachbarschaftspruefung (Bogen/Weiche/ # Rastergroesse (mm) fuer die Omniflo-Nachbarschaftspruefung (Bogen/Weiche/
# Gerade gegen Bogen/Weiche/Gerade). Omniflo-Elemente werden vor dem # Gerade gegen Bogen/Weiche/Gerade). Wird nur als FALLBACK verwendet, wenn
# Ueberschneidungstest nach x/y-Koordinate in Quadranten dieser Groesse # shapely (STRtree) nicht verfuegbar ist - dann werden Omniflo-Elemente vor
# eingeteilt, geprueft wird dann nur innerhalb derselben und der 8 # dem Ueberschneidungstest nach x/y-Koordinate in Quadranten dieser Groesse
# angrenzenden Zellen (statt jedes Element gegen jedes). Sollte groesser # eingeteilt und nur innerhalb derselben und der 8 angrenzenden Zellen
# geprueft (statt jedes Element gegen jedes). Im Normalfall uebernimmt der
# STRtree die Broad-Phase, dieser Wert bleibt dann ungenutzt. Sollte groesser
# als die ueblichen Omniflo-Bauteile sein (AP110-Geraden ca. 2000mm). # als die ueblichen Omniflo-Bauteile sein (AP110-Geraden ca. 2000mm).
omniflo_zellgroesse_mm=3000 omniflo_zellgroesse_mm=3000
# Toleranz (mm) fuer die KOS-basierte VERFEINERUNG der Omniflo-Nachbarschaft.
# Nachdem der STRtree zwei Omniflo-Elemente (Bogen/Weiche/Gerade) anhand ihrer
# Bounding-Boxen als moegliche Nachbarn erkannt hat, wird zusaetzlich geprueft,
# ob sich ihre Anschluss-Koordinatensysteme (K1-K4) tatsaechlich beruehren:
# nur wenn ein K-Punkt des einen Elements naeher als dieser Wert an einem
# K-Punkt des anderen liegt, gelten sie als echte Nachbarn. Das trennt
# Elemente, deren Bounding-Boxen sich zwar ueberlappen, die aber nicht an
# einem gemeinsamen Anschlusspunkt zusammenstossen. Bei Elementen ohne K1-K4
# (z.B. Altbestand) faellt die Pruefung auf die reine BBox-Ueberschneidung
# zurueck. 0 = Verfeinerung aus (nur BBox). Wert in mm. Siehe
# lib/export_neighbors.py. Geraden fuehren KEINE echten K-Bloecke in der
# Zeichnung; ihre K1 (Anfang) / K2 (Ende) werden beim Export aus der Geometrie
# synthetisiert (siehe csv:gerade-k-kos-strings in Lisp/export.lsp).
omniflo_ks_toleranz_mm=10
# Kollisions-/Nachbarschafts-Debugprotokoll: 0/leer = aus, 1 = schreibt eine # Kollisions-/Nachbarschafts-Debugprotokoll: 0/leer = aus, 1 = schreibt eine
# .dbg-Datei nach DXFM_LOG/export_collision.dbg (bzw. <repo>/logs/, falls # .dbg-Datei nach DXFM_LOG/export_collision.dbg (bzw. <repo>/logs/, falls
# DXFM_LOG nicht gesetzt ist). Alternativ ein expliziter Dateipfad. Das Log # DXFM_LOG nicht gesetzt ist). Alternativ ein expliziter Dateipfad. Das Log
+13 -4
View File
@@ -30,9 +30,15 @@ Box-Ueberschneidung in der x/y-Ebene, reines Python ohne externe Abhaengigkeit,
siehe export_neighbors.py). Geprueft wird NICHT global ueber alle Elemente, siehe export_neighbors.py). Geprueft wird NICHT global ueber alle Elemente,
sondern in drei getrennten Gruppen: 1) Kreisel/Eckrad gegen Kreisel/Eckrad, sondern in drei getrennten Gruppen: 1) Kreisel/Eckrad gegen Kreisel/Eckrad,
2) Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul (diese drei 2) Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul (diese drei
nicht gegeneinander), 3) Omniflo-Elemente gegen Omniflo-Elemente (rasterbasiert nicht gegeneinander), 3) Omniflo-Elemente gegen Omniflo-Elemente. Fuer die
vorgefiltert nach x/y-Koordinate). Toleranz und Omniflo-Rastergroesse Omniflo-Gruppe liefert eine shapely-STRtree-Broad-Phase zunaechst alle Paare
konfigurierbar ueber [Nachbarschaft] in cfg/export.cfg. mit ueberschneidenden Bounding-Boxen; anschliessend gelten sie nur dann als
benachbart, wenn sich ihre Anschluss-Koordinatensysteme K1-K4 tatsaechlich
beruehren (ein K-Punkt naeher als omniflo_ks_toleranz_mm, Default 10mm) -
das trennt bloss ueberlappende BBoxen von echten Anschluessen (Geraden fuehren
dafuer beim Export synthetisierte K1/K2, siehe csv:gerade-k-kos-strings).
Toleranzen und (Fallback-)Rastergroesse konfigurierbar ueber [Nachbarschaft]
in cfg/export.cfg.
Fehler = "unverbunden" (keine Partner, aber mindestens einer noetig), Fehler = "unverbunden" (keine Partner, aber mindestens einer noetig),
"nur ein Partner" (ein Partner, aber mindestens zwei noetig) oder leer "nur ein Partner" (ein Partner, aber mindestens zwei noetig) oder leer
(Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/ (Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/
@@ -68,6 +74,7 @@ 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_omniflo_ks_tolerance_mm,
load_kreisel_durchmesser_mm, load_kreisel_durchmesser_mm,
load_collision_debug_target, load_collision_debug_target,
compute_neighbor_ids, compute_neighbor_ids,
@@ -81,6 +88,7 @@ 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()
NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM = load_omniflo_ks_tolerance_mm()
KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm() KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm()
NEIGHBOR_DEBUG_TARGET = load_collision_debug_target() NEIGHBOR_DEBUG_TARGET = load_collision_debug_target()
@@ -622,7 +630,8 @@ 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, items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM,
kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM,
omniflo_ks_tolerance_mm=NEIGHBOR_OMNIFLO_KS_TOLERANCE_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
+204 -39
View File
@@ -4,9 +4,9 @@
export_neighbors.py - Ermittelt benachbarte Elemente ueber Bounding-Box- export_neighbors.py - Ermittelt benachbarte Elemente ueber Bounding-Box-
Ueberschneidung in der x/y-Ebene (Grundriss, Z/Hoehe wird ignoriert). Ueberschneidung in der x/y-Ebene (Grundriss, Z/Hoehe wird ignoriert).
Reines Python (keine externe Abhaengigkeit wie shapely) - die Bloecke werden Die Bloecke werden dazu in drei Gruppen eingeteilt und NUR innerhalb dieser
dazu in drei Gruppen eingeteilt und NUR innerhalb dieser Kombinationen Kombinationen gegeneinander geprueft (nicht mehr alle Elemente einer Zeichnung
gegeneinander geprueft (nicht mehr alle Elemente einer Zeichnung gemeinsam): gemeinsam):
1. Kreisel/Eckrad gegen Kreisel/Eckrad 1. Kreisel/Eckrad gegen Kreisel/Eckrad
2. Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul - diese 2. Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul - diese
@@ -15,12 +15,26 @@ gegeneinander geprueft (nicht mehr alle Elemente einer Zeichnung gemeinsam):
3. Omniflo-Elemente (Bogen/Weiche/Gerade) gegen Omniflo-Elemente 3. Omniflo-Elemente (Bogen/Weiche/Gerade) gegen Omniflo-Elemente
Omniflo-Anlagen koennen mehrere hundert Elemente enthalten - ein Test jedes Omniflo-Anlagen koennen mehrere hundert Elemente enthalten - ein Test jedes
gegen jedes (O(n^2)) waere dort zu teuer. Die Omniflo-Gruppe wird daher vorab gegen jedes (O(n^2)) waere dort zu teuer. Die Omniflo-Gruppe wird daher in
per Raster (Zellgroesse siehe [Nachbarschaft] -> omniflo_zellgroesse_mm in zwei Phasen geprueft:
cfg/export.cfg) nach x/y-Koordinate in Quadranten/Zellen eingeteilt; jedes
Element wird nur gegen Elemente in derselben und den 8 angrenzenden Zellen * Broad-Phase: Ein shapely-STRtree ueber die (tolerierten) Bounding-Boxen
geprueft (3x3-Nachbarschaft, damit auch Elemente knapp jenseits einer liefert alle Paare, deren BBoxen sich in x/y ueberschneiden - die
Zellgrenze noch erkannt werden). "moeglichen Nachbarn". Ist shapely nicht verfuegbar, faellt die
Broad-Phase auf ein Raster (Zellgroesse siehe [Nachbarschaft] ->
omniflo_zellgroesse_mm in cfg/export.cfg) mit 3x3-Zellennachbarschaft
zurueck.
* Narrow-Phase (KOS-Verfeinerung): Fuer jedes moegliche Nachbarpaar wird
zusaetzlich geprueft, ob sich ihre Anschluss-Koordinatensysteme K1-K4
tatsaechlich beruehren - nur wenn ein K-Punkt des einen Elements naeher
als omniflo_ks_toleranz_mm (Default 10mm, siehe [Nachbarschaft]) an einem
K-Punkt des anderen liegt, gelten sie als echte Nachbarn. Das trennt
Elemente, deren BBoxen sich zwar ueberlappen, die aber nicht an einem
gemeinsamen Anschlusspunkt zusammenstossen. Elemente ohne K1-K4 (z.B.
Altbestand) fallen auf die reine BBox-Ueberschneidung zurueck. Die
K-Punkte kommen aus den csv:trans-encode-Strings der CSV-Spalten K1-K4
(siehe _omniflo_kpoints); Geraden fuehren keine echten K-Bloecke, ihre
K1/K2 werden beim Export synthetisiert (Lisp/export.lsp).
Zwei Bounding-Boxes gelten als benachbart, wenn sie sich (nach Erweiterung Zwei Bounding-Boxes gelten als benachbart, wenn sie sich (nach Erweiterung
um die halbe Toleranz je Seite) in x/y ueberschneiden. Toleranz aus um die halbe Toleranz je Seite) in x/y ueberschneiden. Toleranz aus
@@ -38,6 +52,23 @@ import os
from export_blockpatterns import cfg_path_from_env, load_export_cfg, safe_float from export_blockpatterns import cfg_path_from_env, load_export_cfg, safe_float
# shapely-STRtree fuer die Broad-Phase der Omniflo-Nachbarschaft (optional -
# fehlt shapely, wird auf das Raster _candidate_pairs_grid zurueckgefallen).
try:
from shapely import STRtree
from shapely.geometry import box as _shapely_box
_HAVE_STRTREE = True
except ImportError: # pragma: no cover - shapely ist optional
STRtree = None
_shapely_box = None
_HAVE_STRTREE = False
# Base64-Alphabet + Fixed-Point-Faktor von csv:trans-encode (Lisp/export.lsp) -
# zum Zurueckrechnen der K1-K4-Positionsstrings in x/y-Weltkoordinaten (mm).
_B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
_B64_INDEX = {c: i for i, c in enumerate(_B64_CHARS)}
_TRANS_FAKTOR = 10.0
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"}
@@ -81,6 +112,22 @@ 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_omniflo_ks_tolerance_mm(cfg_path=None):
"""Liest die Toleranz (mm) fuer die KOS-basierte Verfeinerung der Omniflo-
Nachbarschaft (K1-K4-Anschlusspunkte) aus export.cfg. Default 10mm.
Nachdem der STRtree zwei Omniflo-Elemente anhand ihrer Bounding-Boxen als
moegliche Nachbarn erkannt hat, gelten sie nur dann als echte Nachbarn,
wenn ein K-Punkt des einen naeher als dieser Wert an einem K-Punkt des
anderen liegt. 0 = Verfeinerung aus (nur BBox). Siehe [Nachbarschaft] ->
omniflo_ks_toleranz_mm.
"""
if cfg_path is None:
cfg_path = cfg_path_from_env()
parser = load_export_cfg(cfg_path)
return parser.getfloat("Nachbarschaft", "omniflo_ks_toleranz_mm", fallback=10.0)
def load_kreisel_durchmesser_mm(cfg_path=None): def load_kreisel_durchmesser_mm(cfg_path=None):
"""Liest den Kreisel-Durchmesser (mm) aus export.cfg. Default: 800.0 """Liest den Kreisel-Durchmesser (mm) aus export.cfg. Default: 800.0
(siehe [Kreisel] durchmesser_mm - gespiegelter Wert von (siehe [Kreisel] durchmesser_mm - gespiegelter Wert von
@@ -209,9 +256,62 @@ def _fmt_bounds(b):
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}]"
def _dbg_pair(dbg, id_a, id_b, overlap): def _dbg_pair(dbg, id_a, id_b, overlap, detail=""):
if dbg: if dbg:
dbg(f" {id_a} <-> {id_b}: {'NACHBARN' if overlap else 'kein Kontakt'}") dbg(f" {id_a} <-> {id_b}: "
f"{'NACHBARN' if overlap else 'kein Kontakt'}{detail}")
def _decode_trans_xy(text):
"""csv:trans-encode-String (K1-K4, 12 Zeichen, 3 Werte) -> (x, y) in mm
(Grundriss, Z wird nicht gebraucht), oder None bei leerem/ungueltigem
String.
Kodierung siehe Lisp/export.lsp (csv:trans-encode/csv:b64-encode-ints):
vorzeichenbehaftete 24-Bit-Fixed-Point-Werte (Faktor 10), je 4 Base64-
Zeichen, hoechstwertige 6 Bit zuerst. Es genuegen die ersten beiden Werte
(x, y) = die ersten 8 Zeichen.
"""
text = (text or "").strip()
if len(text) < 8:
return None
werte = []
for start in (0, 4):
wert = 0
for zeichen in text[start:start + 4]:
idx = _B64_INDEX.get(zeichen)
if idx is None:
return None
wert = (wert << 6) | idx
if wert >= 8388608: # 2^23 -> negativer 24-Bit-Wert (Zweierkompl.)
wert -= 16777216
werte.append(wert / _TRANS_FAKTOR)
return (werte[0], werte[1])
def _omniflo_kpoints(item):
"""Welt-(x,y)-Punkte der vorhandenen K1-K4-Koordinatensysteme eines
Omniflo-Elements, dekodiert aus den csv:trans-encode-Strings item["k1"]..
["k4"] (siehe export_csv.bbox_columns). Leere/fehlende Strings werden
uebersprungen - die Liste kann also 0 bis 4 Punkte enthalten. Fuer Geraden
stehen darin die beim Export synthetisierten Anfang/Ende-Punkte (K1/K2)."""
pts = []
for key in ("k1", "k2", "k3", "k4"):
p = _decode_trans_xy(item.get(key, ""))
if p is not None:
pts.append(p)
return pts
def _ks_within(kpts_a, kpts_b, tol_mm):
"""True, wenn irgendein K-Punkt aus a einem K-Punkt aus b naeher als tol_mm
kommt (euklidischer Abstand in x/y). Beide Listen muessen nicht leer sein -
das prueft der Aufrufer."""
for ax, ay in kpts_a:
for bx, by in kpts_b:
if math.hypot(ax - bx, ay - by) <= tol_mm:
return True
return False
def _test_group_pairs(group, result, dbg=None): def _test_group_pairs(group, result, dbg=None):
@@ -253,49 +353,111 @@ def _grid_cell(bbox, cell_size_mm):
return (int(bbox["cx"] // cell_size_mm), int(bbox["cy"] // cell_size_mm)) return (int(bbox["cx"] // cell_size_mm), int(bbox["cy"] // cell_size_mm))
def _test_omniflo_grid(group, cell_size_mm, result, dbg=None): def _candidate_pairs_strtree(group):
"""Omniflo-Elemente nur gegen Elemente in derselben oder einer der 8 """Broad-Phase per shapely-STRtree: liefert alle Gruppen-Indexpaare (a<b),
angrenzenden Rasterzellen testen (3x3-Nachbarschaft) statt gegen alle - deren (bereits um die halbe Toleranz erweiterte) Bounding-Boxen sich in
haelt den Aufwand bei mehreren hundert Omniflo-Elementen niedrig, ohne x/y ueberschneiden. Gruppen-Eintrag = (idx, bounds, teileid, bbox, kpts)
Ueberschneidungen an Zellgrenzen zu verpassen.""" mit bounds = (minx, maxx, miny, maxy)."""
boxes = [_shapely_box(bounds[0], bounds[2], bounds[1], bounds[3])
for (_idx, bounds, _tid, _bbox, _kpts) in group]
tree = STRtree(boxes)
pairs = set()
for a, geom in enumerate(boxes):
for b in tree.query(geom): # Envelope-Ueberschneidung (shapely 2.x: Indizes)
b = int(b)
if a < b:
pairs.add((a, b))
elif b < a:
pairs.add((b, a))
return pairs
def _candidate_pairs_grid(group, cell_size_mm, dbg=None):
"""Fallback-Broad-Phase ohne shapely: Omniflo-Elemente nach x/y-Koordinate
in Rasterzellen einteilen und nur Paare aus derselben oder einer der 8
angrenzenden Zellen (3x3-Nachbarschaft) als Kandidaten liefern - ohne
Ueberschneidungen an Zellgrenzen zu verpassen. Rueckgabe: Menge von
Gruppen-Indexpaaren (a<b)."""
buckets = {} buckets = {}
for entry in group: for gi, entry in enumerate(group):
idx, bounds, teileid, bbox = entry bbox = entry[3]
buckets.setdefault(_grid_cell(bbox, cell_size_mm), []).append(entry) buckets.setdefault(_grid_cell(bbox, cell_size_mm), []).append(gi)
if dbg: if dbg:
dbg(f" {len(buckets)} belegte Rasterzelle(n): " dbg(f" {len(buckets)} belegte Rasterzelle(n): "
+ ", ".join(f"{cell}={len(v)}" for cell, v in sorted(buckets.items()))) + ", ".join(f"{cell}={len(v)}" for cell, v in sorted(buckets.items())))
checked = set() pairs = set()
for (cx, cy), cell_items in buckets.items(): for (cx, cy), cell_items in buckets.items():
candidates = [] candidates = []
for dx in (-1, 0, 1): for dx in (-1, 0, 1):
for dy in (-1, 0, 1): for dy in (-1, 0, 1):
candidates.extend(buckets.get((cx + dx, cy + dy), [])) candidates.extend(buckets.get((cx + dx, cy + dy), []))
for a in cell_items:
for b in candidates:
if a < b:
pairs.add((a, b))
elif b < a:
pairs.add((b, a))
return pairs
for idx_a, bounds_a, id_a, _ in cell_items:
for idx_b, bounds_b, id_b, _ in candidates: def _test_omniflo(group, cell_size_mm, ks_tol_mm, result, dbg=None):
if idx_a >= idx_b: """Omniflo-Nachbarschaft in zwei Phasen (siehe Modul-Docstring):
continue
pair = (idx_a, idx_b) 1. Broad-Phase (STRtree bzw. Raster-Fallback): alle Paare mit sich
if pair in checked: ueberschneidenden Bounding-Boxen ("moegliche Nachbarn").
continue 2. Narrow-Phase (KOS-Verfeinerung): ein Paar gilt nur dann als benachbart,
checked.add(pair) wenn beide Elemente K-Punkte fuehren UND ein K-Punkt des einen naeher
overlap = _overlaps(bounds_a, bounds_b) als ks_tol_mm an einem K-Punkt des anderen liegt. Hat mindestens eines
_dbg_pair(dbg, id_a, id_b, overlap) der beiden keine K-Punkte (Altbestand) oder ist ks_tol_mm <= 0, bleibt
if overlap: es bei der reinen BBox-Ueberschneidung.
_add_neighbor(result, idx_a, idx_b, id_a, id_b) """
if not group:
return
if _HAVE_STRTREE:
pairs = _candidate_pairs_strtree(group)
broad = "shapely-STRtree"
else:
pairs = _candidate_pairs_grid(group, cell_size_mm, dbg)
broad = "Raster-Fallback (shapely nicht verfuegbar)"
if dbg:
dbg(f" Broad-Phase: {broad}, {len(group)} Omniflo-Element(e), "
f"{len(pairs)} BBox-Kandidatenpaar(e); "
f"KOS-Verfeinerung Toleranz={ks_tol_mm}mm")
for a, b in sorted(pairs):
idx_a, bounds_a, id_a, _bbox_a, kpts_a = group[a]
idx_b, bounds_b, id_b, _bbox_b, kpts_b = group[b]
overlap = _overlaps(bounds_a, bounds_b)
detail = ""
if overlap and ks_tol_mm > 0 and kpts_a and kpts_b:
if _ks_within(kpts_a, kpts_b, ks_tol_mm):
detail = f" (KOS-Kontakt <= {ks_tol_mm:.0f}mm)"
else:
overlap = False
detail = f" (BBox ueberschneidet, aber kein KOS-Paar <= {ks_tol_mm:.0f}mm)"
_dbg_pair(dbg, id_a, id_b, overlap, detail)
if overlap:
_add_neighbor(result, idx_a, idx_b, id_a, id_b)
def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0,
kreisel_durchmesser_mm=800.0, dbg=None): kreisel_durchmesser_mm=800.0,
omniflo_ks_tolerance_mm=10.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"
({"cx","cy","dx","dy",...}, siehe export_csv.py bbox_columns). Items ({"cx","cy","dx","dy",...}, siehe export_csv.py bbox_columns). Items
ohne _bbox (z.B. die synthetische Omniflo-Sum-Zeile) bleiben ohne ohne _bbox (z.B. die synthetische Omniflo-Sum-Zeile) bleiben ohne
Nachbarn. Nachbarn. Omniflo-Items koennen zusaetzlich "k1".."k4" (csv:trans-encode-
Strings) tragen - daraus wird die KOS-Verfeinerung gespeist (siehe
_test_omniflo / omniflo_ks_tolerance_mm).
omniflo_ks_tolerance_mm = Toleranz (mm) fuer die KOS-Verfeinerung der
Omniflo-Nachbarschaft (K1-K4). 0 = aus (nur BBox-Ueberschneidung).
"ILS 2.0 Kreisel"-Items werden dabei NICHT als eine BBox, sondern als "ILS 2.0 Kreisel"-Items werden dabei NICHT als eine BBox, sondern als
ZWEI gegeneinander getestet: eine linke und eine rechte Haelfte (siehe ZWEI gegeneinander getestet: eine linke und eine rechte Haelfte (siehe
@@ -326,7 +488,8 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0,
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 (nur Raster-Fallback), "
f"Omniflo-KOS-Toleranz={omniflo_ks_tolerance_mm}mm, "
f"Kreisel-Durchmesser={kreisel_durchmesser_mm}mm (Radius je Haelfte)") f"Kreisel-Durchmesser={kreisel_durchmesser_mm}mm (Radius je Haelfte)")
dbg(f"Elemente gesamt: {len(items)}") dbg(f"Elemente gesamt: {len(items)}")
dbg("") dbg("")
@@ -366,7 +529,7 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0,
strecken_group.append((idx, bounds, teileid)) strecken_group.append((idx, bounds, teileid))
elif teileart in OMNIFLO_TEILEARTEN: elif teileart in OMNIFLO_TEILEARTEN:
gruppe = "Omniflo" gruppe = "Omniflo"
omniflo_group.append((idx, bounds, teileid, bbox)) omniflo_group.append((idx, bounds, teileid, bbox, _omniflo_kpoints(item)))
else: else:
gruppe = "(keine Gruppe - wird nicht geprueft)" gruppe = "(keine Gruppe - wird nicht geprueft)"
if dbg: if dbg:
@@ -392,9 +555,11 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0,
_test_group_against_group(kreisel_group, strecken_group, result, dbg) _test_group_against_group(kreisel_group, strecken_group, result, dbg)
if dbg: if dbg:
dbg("[3] Omniflo gegen Omniflo (rasterbasiert vorgefiltert):") dbg("[3] Omniflo gegen Omniflo (STRtree-Broad-Phase + KOS-Verfeinerung):")
# 3. Omniflo gegen Omniflo, rasterbasiert vorgefiltert # 3. Omniflo gegen Omniflo: BBox-Broad-Phase (STRtree bzw. Raster-Fallback),
_test_omniflo_grid(omniflo_group, omniflo_cell_size_mm, result, dbg) # dann KOS-Verfeinerung ueber K1-K4 (siehe _test_omniflo)
_test_omniflo(omniflo_group, omniflo_cell_size_mm,
omniflo_ks_tolerance_mm, result, dbg)
# dict.fromkeys() dedupliziert unter Erhalt der Reihenfolge - relevant # dict.fromkeys() dedupliziert unter Erhalt der Reihenfolge - relevant
# seit dem Kreisel-Split: beruehrt eine Strecke BEIDE Haelften desselben # seit dem Kreisel-Split: beruehrt eine Strecke BEIDE Haelften desselben
+4
View File
@@ -3,4 +3,8 @@
ezdxf==1.4.1 ezdxf==1.4.1
pytest==9.0.2 pytest==9.0.2
# shapely: STRtree-Broad-Phase der Omniflo-Nachbarschaftserkennung
# (lib/export_neighbors.py). Fehlt shapely, faellt die Broad-Phase automatisch
# auf das Raster (omniflo_zellgroesse_mm) zurueck.
shapely==2.1.2