[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:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user