From 9c8f84799c13634ece5219319732e4923ffea035 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Fri, 31 Jul 2026 16:56:10 +0200 Subject: [PATCH] [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) --- Lisp/export.lsp | 40 +++++++ cfg/export.cfg | 25 ++++- lib/export_csv.py | 17 ++- lib/export_neighbors.py | 243 +++++++++++++++++++++++++++++++++------- requirements.txt | 4 + 5 files changed, 282 insertions(+), 47 deletions(-) diff --git a/Lisp/export.lsp b/Lisp/export.lsp index 95de855..c3b0d9b 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -350,6 +350,37 @@ (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 --- ;; [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) (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)) + ;; 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 "") (if include-bbox (progn diff --git a/cfg/export.cfg b/cfg/export.cfg index 435c0f2..0731a07 100644 --- a/cfg/export.cfg +++ b/cfg/export.cfg @@ -104,13 +104,30 @@ format= {x}/{y} toleranz_mm=200 # Rastergroesse (mm) fuer die Omniflo-Nachbarschaftspruefung (Bogen/Weiche/ -# Gerade gegen Bogen/Weiche/Gerade). Omniflo-Elemente werden vor dem -# Ueberschneidungstest nach x/y-Koordinate in Quadranten dieser Groesse -# eingeteilt, geprueft wird dann nur innerhalb derselben und der 8 -# angrenzenden Zellen (statt jedes Element gegen jedes). Sollte groesser +# Gerade gegen Bogen/Weiche/Gerade). Wird nur als FALLBACK verwendet, wenn +# shapely (STRtree) nicht verfuegbar ist - dann werden Omniflo-Elemente vor +# dem Ueberschneidungstest nach x/y-Koordinate in Quadranten dieser Groesse +# 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). 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 # .dbg-Datei nach DXFM_LOG/export_collision.dbg (bzw. /logs/, falls # DXFM_LOG nicht gesetzt ist). Alternativ ein expliziter Dateipfad. Das Log diff --git a/lib/export_csv.py b/lib/export_csv.py index f985e90..ee4b22b 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -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, sondern in drei getrennten Gruppen: 1) Kreisel/Eckrad gegen Kreisel/Eckrad, 2) Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul (diese drei -nicht gegeneinander), 3) Omniflo-Elemente gegen Omniflo-Elemente (rasterbasiert -vorgefiltert nach x/y-Koordinate). Toleranz und Omniflo-Rastergroesse -konfigurierbar ueber [Nachbarschaft] in cfg/export.cfg. +nicht gegeneinander), 3) Omniflo-Elemente gegen Omniflo-Elemente. Fuer die +Omniflo-Gruppe liefert eine shapely-STRtree-Broad-Phase zunaechst alle Paare +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), "nur ein Partner" (ein Partner, aber mindestens zwei noetig) oder leer (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 ( load_neighbor_tolerance_mm, load_omniflo_cell_size_mm, + load_omniflo_ks_tolerance_mm, load_kreisel_durchmesser_mm, load_collision_debug_target, compute_neighbor_ids, @@ -81,6 +88,7 @@ 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() +NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM = load_omniflo_ks_tolerance_mm() KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm() NEIGHBOR_DEBUG_TARGET = load_collision_debug_target() @@ -622,7 +630,8 @@ def process_blocks(blocks, lookup, dbg=None): neighbor_ids = compute_neighbor_ids( 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) for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors): item["nachbarn"] = nachbarn diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index 1d42d2b..f743b09 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -4,9 +4,9 @@ export_neighbors.py - Ermittelt benachbarte Elemente ueber Bounding-Box- Ueberschneidung in der x/y-Ebene (Grundriss, Z/Hoehe wird ignoriert). -Reines Python (keine externe Abhaengigkeit wie shapely) - die Bloecke werden -dazu in drei Gruppen eingeteilt und NUR innerhalb dieser Kombinationen -gegeneinander geprueft (nicht mehr alle Elemente einer Zeichnung gemeinsam): +Die Bloecke werden dazu in drei Gruppen eingeteilt und NUR innerhalb dieser +Kombinationen gegeneinander geprueft (nicht mehr alle Elemente einer Zeichnung +gemeinsam): 1. Kreisel/Eckrad gegen Kreisel/Eckrad 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 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 -per Raster (Zellgroesse siehe [Nachbarschaft] -> omniflo_zellgroesse_mm in -cfg/export.cfg) nach x/y-Koordinate in Quadranten/Zellen eingeteilt; jedes -Element wird nur gegen Elemente in derselben und den 8 angrenzenden Zellen -geprueft (3x3-Nachbarschaft, damit auch Elemente knapp jenseits einer -Zellgrenze noch erkannt werden). +gegen jedes (O(n^2)) waere dort zu teuer. Die Omniflo-Gruppe wird daher in +zwei Phasen geprueft: + + * Broad-Phase: Ein shapely-STRtree ueber die (tolerierten) Bounding-Boxen + liefert alle Paare, deren BBoxen sich in x/y ueberschneiden - die + "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 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 +# 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"} STRECKEN_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"} 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) +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): """Liest den Kreisel-Durchmesser (mm) aus export.cfg. Default: 800.0 (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}]" -def _dbg_pair(dbg, id_a, id_b, overlap): +def _dbg_pair(dbg, id_a, id_b, overlap, detail=""): 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): @@ -253,49 +353,111 @@ def _grid_cell(bbox, 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): - """Omniflo-Elemente nur gegen Elemente in derselben oder einer der 8 - angrenzenden Rasterzellen testen (3x3-Nachbarschaft) statt gegen alle - - haelt den Aufwand bei mehreren hundert Omniflo-Elementen niedrig, ohne - Ueberschneidungen an Zellgrenzen zu verpassen.""" +def _candidate_pairs_strtree(group): + """Broad-Phase per shapely-STRtree: liefert alle Gruppen-Indexpaare (a= idx_b: - continue - pair = (idx_a, idx_b) - if pair in checked: - continue - checked.add(pair) - overlap = _overlaps(bounds_a, bounds_b) - _dbg_pair(dbg, id_a, id_b, overlap) - if overlap: - _add_neighbor(result, idx_a, idx_b, id_a, id_b) + +def _test_omniflo(group, cell_size_mm, ks_tol_mm, result, dbg=None): + """Omniflo-Nachbarschaft in zwei Phasen (siehe Modul-Docstring): + + 1. Broad-Phase (STRtree bzw. Raster-Fallback): alle Paare mit sich + ueberschneidenden Bounding-Boxen ("moegliche Nachbarn"). + 2. Narrow-Phase (KOS-Verfeinerung): ein Paar gilt nur dann als benachbart, + wenn beide Elemente K-Punkte fuehren UND ein K-Punkt des einen naeher + als ks_tol_mm an einem K-Punkt des anderen liegt. Hat mindestens eines + der beiden keine K-Punkte (Altbestand) oder ist ks_tol_mm <= 0, bleibt + es bei der reinen BBox-Ueberschneidung. + """ + 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, - 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. items = Liste von dict mit "teileart", "teileid" und optional "_bbox" ({"cx","cy","dx","dy",...}, siehe export_csv.py bbox_columns). Items 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 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: dbg("=== Nachbarschafts-/Kollisionserkennung ===") 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)") dbg(f"Elemente gesamt: {len(items)}") dbg("") @@ -366,7 +529,7 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, strecken_group.append((idx, bounds, teileid)) elif teileart in OMNIFLO_TEILEARTEN: gruppe = "Omniflo" - omniflo_group.append((idx, bounds, teileid, bbox)) + omniflo_group.append((idx, bounds, teileid, bbox, _omniflo_kpoints(item))) else: gruppe = "(keine Gruppe - wird nicht geprueft)" 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) if dbg: - dbg("[3] Omniflo gegen Omniflo (rasterbasiert vorgefiltert):") - # 3. Omniflo gegen Omniflo, rasterbasiert vorgefiltert - _test_omniflo_grid(omniflo_group, omniflo_cell_size_mm, result, dbg) + dbg("[3] Omniflo gegen Omniflo (STRtree-Broad-Phase + KOS-Verfeinerung):") + # 3. Omniflo gegen Omniflo: BBox-Broad-Phase (STRtree bzw. Raster-Fallback), + # 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 # seit dem Kreisel-Split: beruehrt eine Strecke BEIDE Haelften desselben diff --git a/requirements.txt b/requirements.txt index 59150b5..a959b49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,8 @@ ezdxf==1.4.1 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