[ADD] Dematic/Arvato: Nachbau-Vorlage tests/testdata/dematic_arvato.json (Abschnitt Flowpuffer/Kreisel) + Treiber TEST_DEMATIC_ARVATO (Menue Tests, in alltests.json vorerst disabled); [FIX] dxf_scan_components.py: liest jetzt auch ASCII-DXF, Kreisel-Paarung global nach Abstand und nur auf gemeinsamer Achse (vorher klaute ein AN8 die SP8 des Nachbarn), Kreisel-Position aus der Radmitte statt dem Einfuegepunkt (AN8/SP8 Pinband haben das Rad ~15 m daneben), Kreisel als insert mit Mittenabstand statt connect (war an beiden Enden einen Radius zu kurz), AN8/SP8 Pinband erkannt, Projektkennung in den IDs per --projekt statt fest HundM05; [FIX] hundm05:bau-sensor brach bei Eintraegen ohne id mit bad argument type <NIL> ab
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+151
-29
@@ -44,6 +44,7 @@ import struct
|
||||
import sys
|
||||
|
||||
from ezdxf.lldxf.types import BINARY_DATA, INT16, INT32, INT64, DOUBLE, BYTES
|
||||
from ezdxf.lldxf.types import cast_tag_value
|
||||
from ezdxf.math import Matrix44, OCS
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -62,6 +63,10 @@ MAX_PAAR_ABSTAND = 60000.0
|
||||
# 10 mm deckt Rundungs- und Zeichenungenauigkeiten der Fremdzeichnung ab.
|
||||
GERADE_MAX_DELTA_H = 10.0
|
||||
|
||||
# Projektkennung in den erzeugten IDs (Kreisel_<PROJEKT>_01 ...); main()
|
||||
# setzt sie aus --projekt bzw. dem Namen der DXF-Datei.
|
||||
PROJEKT = "HundM05"
|
||||
|
||||
# Toleranz fuer die Dedup-Rundung (Nachkommastellen).
|
||||
RUND = 2
|
||||
|
||||
@@ -190,6 +195,48 @@ def iter_binary_tags(mm):
|
||||
yield code, value
|
||||
|
||||
|
||||
def iter_ascii_tags(mm):
|
||||
"""Streamt (code, value)-Tags aus einem ASCII-DXF.
|
||||
|
||||
Gegenstueck zu iter_binary_tags fuer Zeichnungen, die als Text-DXF
|
||||
gespeichert wurden (z.B. Dematic_arvato.dxf). Die Werte werden ueber die
|
||||
ezdxf-Typtabelle in float/int gewandelt, damit lese_inserts dieselben
|
||||
Typen sieht wie beim binaeren Lesen.
|
||||
"""
|
||||
i = 0
|
||||
n = len(mm)
|
||||
while i < n:
|
||||
end = mm.find(b"\n", i)
|
||||
if end < 0:
|
||||
break
|
||||
code_raw = mm[i:end].strip()
|
||||
i = end + 1
|
||||
end = mm.find(b"\n", i)
|
||||
if end < 0:
|
||||
end = n
|
||||
value_raw = mm[i:end].rstrip(b"\r")
|
||||
i = end + 1
|
||||
if not code_raw:
|
||||
continue
|
||||
code = int(code_raw)
|
||||
value = decode_str(value_raw)
|
||||
if code == 0:
|
||||
value = value.strip()
|
||||
else:
|
||||
try:
|
||||
value = cast_tag_value(code, value)
|
||||
except ValueError:
|
||||
pass
|
||||
yield code, value
|
||||
|
||||
|
||||
def iter_tags(mm):
|
||||
"""Binaer- oder ASCII-DXF anhand der Kopfzeile erkennen."""
|
||||
if mm[:22] == b"AutoCAD Binary DXF\r\n\x1a\x00":
|
||||
return iter_binary_tags(mm)
|
||||
return iter_ascii_tags(mm)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Familien-Erkennung (Blockname -> Bauteilfamilie)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -197,8 +244,8 @@ def iter_binary_tags(mm):
|
||||
# mit Wildcards - die Blocknamen der Fremdzeichnungen enthalten Umlaute und
|
||||
# Sonderzeichen (z.B. "3D_Schraegfoerderer_Antrieb", "TEF_Bogen_90Grad_...").
|
||||
FAMILIEN = [
|
||||
("kreisel_an", ["AN8", "AN8_*"]),
|
||||
("kreisel_sp", ["SP8", "SP8_*"]),
|
||||
("kreisel_an", ["AN8", "AN8_*", "AN8 Pinband*"]),
|
||||
("kreisel_sp", ["SP8", "SP8_*", "SP8 Pinband*"]),
|
||||
("foerderer_antrieb", ["3D_TEF_Antrieb*", "3D_FF_Antrieb*",
|
||||
"3D_Schr*rderer_Antrieb*", "Antrieb FF"]),
|
||||
("foerderer_umlenk", ["3D_TEF_Umlenk*", "3D_FF_Umlenk*",
|
||||
@@ -443,7 +490,7 @@ def lese_inserts(dxf_path):
|
||||
fh.close()
|
||||
raise
|
||||
try:
|
||||
for code, value in iter_binary_tags(mm):
|
||||
for code, value in iter_tags(mm):
|
||||
if code == 0:
|
||||
# Ein ATTRIB endet immer am naechsten 0-Tag. Erst hier steht
|
||||
# sicher fest, dass Tag UND Wert gelesen sind - in einem ATTRIB
|
||||
@@ -496,6 +543,19 @@ def lese_inserts(dxf_path):
|
||||
block_merken(aktueller_block)["kinder"].add(value)
|
||||
elif aktuell == "ATTRIB":
|
||||
attrib_tag = value
|
||||
elif aktuell == "CIRCLE" and aktueller_block and code in (10, 20, 30):
|
||||
# Mittelpunkt des ERSTEN Kreises der Blockdefinition merken -
|
||||
# bei AN8/SP8 ist das das Rad. Die Pinband-Varianten in
|
||||
# Dematic_arvato.dxf haben ihr Rad ~15 m neben dem
|
||||
# Einfuegepunkt; ohne diesen Wert laege der Kreisel dort.
|
||||
# Ein zweiter Kreis beginnt mit Code 10, waehrend "kreis" schon
|
||||
# existiert - ab da ist der erste fertig (Code 30 ist optional).
|
||||
info = block_merken(aktueller_block)
|
||||
if code == 10 and "kreis" in info:
|
||||
info["kreis_fertig"] = True
|
||||
if not info.get("kreis_fertig"):
|
||||
kreis = info.setdefault("kreis", [0.0, 0.0, 0.0])
|
||||
kreis[{10: 0, 20: 1, 30: 2}[code]] = value
|
||||
elif aktuell == "BLOCK" and aktueller_block and code in (10, 20, 30):
|
||||
block_base[aktueller_block][{10: 0, 20: 1, 30: 2}[code]] = value
|
||||
elif aktuell == "INSERT" and rec is not None:
|
||||
@@ -630,6 +690,13 @@ def sammle_vorkommen(ms_inserts, block_inserts, block_base, max_tiefe=12,
|
||||
xachse = mat.transform((1.0, 0.0, 0.0)) - ursprung
|
||||
rot = math.degrees(math.atan2(xachse.y, xachse.x)) % 360.0
|
||||
fam = familie_von(rec["name"])
|
||||
# Kreisel-Stationen: Position = Radmitte, nicht Einfuegepunkt
|
||||
# (siehe "kreis" in lese_inserts). Die Rotation oben bleibt die
|
||||
# des Inserts.
|
||||
kreis = ((block_info or {}).get(rec["name"], {}).get("kreis")
|
||||
if fam in ("kreisel_an", "kreisel_sp") else None)
|
||||
if kreis:
|
||||
ursprung = mat.transform(tuple(kreis))
|
||||
if fam:
|
||||
treffer.append({
|
||||
"familie": fam,
|
||||
@@ -866,6 +933,48 @@ def zwillinge_zusammenfuehren(treffer):
|
||||
# Paarbildung und Schema-Abbildung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Querversatz (mm), bis zu dem AN8 und SP8 noch auf derselben Kreisel-Achse
|
||||
# liegen. Beide Stationen sitzen auf der Mittellinie des Kreisels; in
|
||||
# Dematic_arvato.dxf liegen echte Paare auf 0,2 mm genau auf einer Achse.
|
||||
KREISEL_MAX_QUER = 100.0
|
||||
|
||||
|
||||
def kreisel_paare_bilden(an_liste, sp_liste):
|
||||
"""AN8 und SP8 zu Kreiseln paaren.
|
||||
|
||||
Global nach Abstand (kuerzestes Paar zuerst) statt AN8 fuer AN8 in
|
||||
X-Reihenfolge: dort griff sich ein frueh sortierter AN8 die naechste noch
|
||||
freie SP8 eines Nachbar-Kreisels, und alle folgenden Paare rutschten mit
|
||||
(Dematic_arvato: ein 33,5-m-Kreisel wurde mit der SP8 eines 3,6-m-Kreisels
|
||||
19 m daneben gepaart). Zusaetzlich muessen beide auf einer gemeinsamen
|
||||
Achse liegen (Querversatz <= KREISEL_MAX_QUER).
|
||||
|
||||
Rueckgabe: (paare [(an, sp, abstand)] nach AN-Position sortiert,
|
||||
AN8 ohne Partner, SP8 ohne Partner)
|
||||
"""
|
||||
kandidaten = []
|
||||
for ia, an in enumerate(an_liste):
|
||||
for isp, sp in enumerate(sp_liste):
|
||||
dx = abs(sp["x"] - an["x"])
|
||||
dy = abs(sp["y"] - an["y"])
|
||||
if min(dx, dy) > KREISEL_MAX_QUER:
|
||||
continue
|
||||
kandidaten.append((math.hypot(dx, dy), ia, isp))
|
||||
kandidaten.sort()
|
||||
an_frei = set(range(len(an_liste)))
|
||||
sp_frei = set(range(len(sp_liste)))
|
||||
paare = []
|
||||
for dist, ia, isp in kandidaten:
|
||||
if ia in an_frei and isp in sp_frei:
|
||||
an_frei.discard(ia)
|
||||
sp_frei.discard(isp)
|
||||
paare.append((an_liste[ia], sp_liste[isp], dist))
|
||||
paare.sort(key=lambda p: (p[0]["x"], p[0]["y"]))
|
||||
return (paare,
|
||||
[an_liste[i] for i in sorted(an_frei)],
|
||||
[sp_liste[i] for i in sorted(sp_frei)])
|
||||
|
||||
|
||||
def paare_bilden(antriebe, umlenkungen):
|
||||
"""Gegenseitig naechste Nachbarn paaren.
|
||||
|
||||
@@ -978,7 +1087,7 @@ def foerderer_eintrag(a, u, dist_xy, nummer, gf_winkel_min):
|
||||
# Die echte Hoehe liefert der 3D-Zwilling (siehe Zusammenfuehrung).
|
||||
if a.get("darstellung") == "2D":
|
||||
return {
|
||||
"test_id": "GERADE_HundM05_%02d" % nummer,
|
||||
"test_id": "GERADE_%s_%02d" % (PROJEKT, nummer),
|
||||
"typ": "gerade",
|
||||
"darstellung": "2D",
|
||||
"laenge": round(dist_xy, RUND),
|
||||
@@ -993,7 +1102,7 @@ def foerderer_eintrag(a, u, dist_xy, nummer, gf_winkel_min):
|
||||
|
||||
if abs(dz) < GERADE_MAX_DELTA_H:
|
||||
return {
|
||||
"test_id": "GERADE_HundM05_%02d" % nummer,
|
||||
"test_id": "GERADE_%s_%02d" % (PROJEKT, nummer),
|
||||
"typ": "gerade",
|
||||
"darstellung": a.get("darstellung", "?"),
|
||||
"laenge": round(dist_xy, RUND),
|
||||
@@ -1008,7 +1117,7 @@ def foerderer_eintrag(a, u, dist_xy, nummer, gf_winkel_min):
|
||||
|
||||
if dz < 0 and neigung >= gf_winkel_min:
|
||||
return {
|
||||
"test_id": "GF_HundM05_%02d" % nummer,
|
||||
"test_id": "GF_%s_%02d" % (PROJEKT, nummer),
|
||||
"modus": 1,
|
||||
"darstellung": a.get("darstellung", "?"),
|
||||
"deltaL_mm": round(dist_xy, RUND),
|
||||
@@ -1025,7 +1134,7 @@ def foerderer_eintrag(a, u, dist_xy, nummer, gf_winkel_min):
|
||||
}
|
||||
|
||||
return {
|
||||
"test_id": "VF_HundM05_%02d" % nummer,
|
||||
"test_id": "VF_%s_%02d" % (PROJEKT, nummer),
|
||||
"typ": "standard",
|
||||
"darstellung": a.get("darstellung", "?"),
|
||||
"richtung": "Auf" if dz > 0 else "Ab",
|
||||
@@ -1116,16 +1225,12 @@ def baue_eintraege(treffer, defaults, boegen, weichen,
|
||||
|
||||
# --- Kreisel: AN8 (Antriebsstation) + SP8 (Spannstation) --------------
|
||||
durchmesser = defaults.get("kreisel", {}).get("durchmesser", 800.0)
|
||||
an_liste = sorted(nach_familie.get("kreisel_an", []), key=lambda t: (t["x"], t["y"]))
|
||||
sp_liste = list(nach_familie.get("kreisel_sp", []))
|
||||
for i, an in enumerate(an_liste, start=1):
|
||||
if not sp_liste:
|
||||
hinweise.append("AN8 bei (%.2f, %.2f) ohne SP8-Partner" % (an["x"], an["y"]))
|
||||
eintraege.append(sensor_eintrag(an, bekannte_bloecke, hinweise, treffer))
|
||||
continue
|
||||
sp = min(sp_liste, key=lambda s: math.hypot(s["x"] - an["x"], s["y"] - an["y"]))
|
||||
sp_liste.remove(sp)
|
||||
dist = math.hypot(sp["x"] - an["x"], sp["y"] - an["y"])
|
||||
kreisel_paare, an_rest, sp_liste = kreisel_paare_bilden(
|
||||
nach_familie.get("kreisel_an", []), nach_familie.get("kreisel_sp", []))
|
||||
for an in an_rest:
|
||||
hinweise.append("AN8 bei (%.2f, %.2f) ohne SP8-Partner" % (an["x"], an["y"]))
|
||||
eintraege.append(sensor_eintrag(an, bekannte_bloecke, hinweise, treffer))
|
||||
for i, (an, sp, dist) in enumerate(kreisel_paare, start=1):
|
||||
rotation = math.degrees(math.atan2(sp["y"] - an["y"], sp["x"] - an["x"])) % 360.0
|
||||
# AN8/SP8 sind flache 2D-Symbole auf Z=0 - die Einbauhoehe steht nicht
|
||||
# im DXF. Sie wird aus den 3D-Modellen in der Umgebung abgeleitet
|
||||
@@ -1144,19 +1249,31 @@ def baue_eintraege(treffer, defaults, boegen, weichen,
|
||||
hoehe_hinweis = (" ACHTUNG hoehe=%.1f ist NICHT belegt: "
|
||||
"AN8/SP8 sind flache 2D-Symbole auf Z=0 und "
|
||||
"%s." % (hoehe, grund))
|
||||
# an/sp sind Radmitten (siehe "kreis" in lese_inserts). draw-module
|
||||
# (KreiselInsert.lsp) legt AN8 bei (radius, 0) und SP8 bei
|
||||
# (radius + abstand, 0) in den Block: abstand ist also der
|
||||
# Mittenabstand, und der Einfuegepunkt liegt einen Radius VOR der
|
||||
# AN8-Mitte. Die fruehere "connect"-Ausgabe uebergab die Radmitten an
|
||||
# kreisel-connect-script, das Aussenkanten erwartet - der Kreisel
|
||||
# wurde dadurch an beiden Enden um einen Radius zu kurz.
|
||||
radius = durchmesser / 2.0
|
||||
richtung = math.radians(rotation)
|
||||
eintraege.append({
|
||||
"id": "Kreisel_HundM05_%02d" % i,
|
||||
"function": "connect",
|
||||
"start_x": an["x"], "start_y": an["y"], "start_z": an["z"],
|
||||
"end_x": sp["x"], "end_y": sp["y"], "end_z": sp["z"],
|
||||
"id": "Kreisel_%s_%02d" % (PROJEKT, i),
|
||||
"function": "insert",
|
||||
"x": round(an["x"] - radius * math.cos(richtung), RUND) + 0.0,
|
||||
"y": round(an["y"] - radius * math.sin(richtung), RUND) + 0.0,
|
||||
"z": hoehe,
|
||||
"abstand": round(dist, RUND),
|
||||
"rotation": round(rotation, RUND) + 0.0,
|
||||
"typ": "STANDARD",
|
||||
"darstellung": an.get("darstellung", "?"),
|
||||
"hoehe": hoehe,
|
||||
"comment": ("aus DXF: AN8 (Layer %s) -> SP8 (Layer %s), Abstand "
|
||||
"%.2f; daraus folgt produktionsseitig abstand = %.2f - "
|
||||
"%.1f = %.2f und rotation = %.2f Grad."
|
||||
% (an["layer"], sp["layer"], dist, dist, durchmesser,
|
||||
dist - durchmesser, rotation)) + hoehe_hinweis,
|
||||
"comment": ("aus DXF: Radmitte %s (Layer %s) (%.2f, %.2f) -> "
|
||||
"%s (Layer %s) (%.2f, %.2f), Mittenabstand %.2f, "
|
||||
"rotation %.2f Grad; x/y = AN8-Mitte minus Radius."
|
||||
% (an["block"], an["layer"], an["x"], an["y"],
|
||||
sp["block"], sp["layer"], sp["x"], sp["y"], dist,
|
||||
rotation)) + hoehe_hinweis,
|
||||
})
|
||||
for sp in sp_liste:
|
||||
hinweise.append("SP8 bei (%.2f, %.2f) ohne AN8-Partner" % (sp["x"], sp["y"]))
|
||||
@@ -1188,7 +1305,7 @@ def baue_eintraege(treffer, defaults, boegen, weichen,
|
||||
hinweise.append("Omniflo %s ohne Sivasnr: '%s' (%s)"
|
||||
% (typ, t["block"], grund))
|
||||
eintraege.append({
|
||||
"id": sivasnr or ("%s_HundM05_%02d" % (typ.upper(), i)),
|
||||
"id": sivasnr or ("%s_%s_%02d" % (typ.upper(), PROJEKT, i)),
|
||||
"type": typ,
|
||||
"sivasnr": sivasnr,
|
||||
"x": t["x"], "y": t["y"], "z": t["z"],
|
||||
@@ -1346,7 +1463,7 @@ def main():
|
||||
description="Erkennt ILS-/Omniflo-Komponenten in einem fremden DXF "
|
||||
"und schreibt sie im JSON-Schema aus "
|
||||
"tests/testdata/object_data.md.")
|
||||
parser.add_argument("dxf", help="Quellzeichnung (binaeres DXF)")
|
||||
parser.add_argument("dxf", help="Quellzeichnung (binaeres oder ASCII-DXF)")
|
||||
parser.add_argument("data_dir", help="Datenverzeichnis mit json/")
|
||||
parser.add_argument("output", help="Ziel-JSON")
|
||||
parser.add_argument("--fenster", nargs=4, type=float,
|
||||
@@ -1354,8 +1471,13 @@ def main():
|
||||
help="Nur Bauteile in diesem XY-Fenster (mm) "
|
||||
"ausgeben - fuer den Nachbau eines einzelnen "
|
||||
"Anlagenabschnitts statt der ganzen Zeichnung")
|
||||
parser.add_argument("--projekt",
|
||||
help="Projektkennung in den erzeugten IDs "
|
||||
"(Default: Name der DXF-Datei ohne Endung)")
|
||||
args = parser.parse_args()
|
||||
dxf_path, data_dir, output_path = args.dxf, args.data_dir, args.output
|
||||
global PROJEKT
|
||||
PROJEKT = args.projekt or os.path.splitext(os.path.basename(dxf_path))[0]
|
||||
fenster = None
|
||||
if args.fenster:
|
||||
x0, y0, x1, y1 = args.fenster
|
||||
|
||||
Reference in New Issue
Block a user