[ADD] lib/dxf_anlage_export.py: gebaute und von Hand nachbearbeitete Anlage aus der Zeichnung zurueck in Nachbau-Testdaten (Kreisel, GF, VF standard/etage, VF-Linienzug als unveraendertes Journal, Einzelbloecke) - tests/testdata/dematic_arvato.json jetzt aus Dematic_TST.dxf; [ADD] dxf_scan_components.py: Gefaellestrecken aus Staustrecken-LINEs (Layer 6-SP), Start/Ende auf die Kreisel-Ketten verlaengert, AS-Element und Separator auf der Achse werden verbraucht, Steigfoerderer erkannt, --ohne-hoehen und --kreisel-hoehe; [ADD] vf_spec.lsp: Replay-Kern vsp-bau-journal aus vsp-bau-aus-spec herausgeloest (auch fuer aufgezeichnete Journale); [CHANGE] TEST_DEMATIC_ARVATO baut GF, VF standard/etage und Linienzug-Journale (mubea:gf-bauen/-vario-bauen wiederverwendet); [FIX] GF-Winkel aus Kreisel-Hoehen: AS/ES-Abfall mit falschem Vorzeichen an gf-winkel-berechnen uebergeben (3.79 statt ~2.49 Grad, GF endete 258 mm zu tief); [ADD] Tests test_dxf_scan_gf.py, test_dxf_anlage_export.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
dxf_anlage_export.py - Eine mit unseren Makros GEBAUTE (und von Hand
|
||||
nachbearbeitete) Anlage aus der Zeichnung zurueck in Nachbau-Testdaten
|
||||
schreiben.
|
||||
|
||||
Gegenstueck zu lib/dxf_scan_components.py: der Scanner liest FREMDE
|
||||
Zeichnungen (Rohbloecke, Linien) und muss vieles raten. Hier stehen unsere
|
||||
Wrapper-Bloecke in der Zeichnung, und jeder traegt seine Bau-Parameter
|
||||
selbst - als Attribute und XDATA, genau die Werte, die auch der jeweilige
|
||||
Edit-Befehl (Doppelklick) zum Neuaufbau liest. Typischer Ablauf: Scanner-
|
||||
Vorlage nachbauen, in BricsCAD von Hand korrigieren/ergaenzen, Zeichnung als
|
||||
DXF speichern und mit diesem Skript die Testdaten aktualisieren.
|
||||
|
||||
Je Wrapper (Felder wie in tests/testdata/object_data.md bzw. mubea.json):
|
||||
|
||||
KREISEL_n -> Kreisel ("function": "insert", x/y/z = Einfuegepunkt,
|
||||
abstand = Mittenabstand AN8-SP8 aus der Blockdefinition,
|
||||
rotation, typ = KREISELART)
|
||||
GF_n -> Gefaellestrecke Modus 1 (XDATA SSG_GF_EDIT wie
|
||||
gf-xdata-lesen + SEITE_AS/SEITE_ES wie GEFAELLESTRECKE_EDIT).
|
||||
Endet die Strecke auf der Kette eines Kreisels, wird statt
|
||||
des gebauten Winkels dessen Hoehe als hoehe_ende_mm
|
||||
uebernommen - der Treiber rechnet den Winkel daraus neu.
|
||||
VF_n "standard"/"etage"
|
||||
-> VarioFoerderer im mubea-Schema (Parameter wie
|
||||
vfs-edit-basis-lesen: Einfuegepunkt, hz aus XDATA, DELTA_L/
|
||||
DELTA_H/ANTRIEBFAHRTRICHTUNG/SEITE_AS/MOTORSEITE, dazu
|
||||
VF_WINKEL und L_GF1 aus L_GF_m)
|
||||
VF_n "linienzug"
|
||||
-> das Eingabe-Journal UNVERAENDERT ("typ":
|
||||
"linienzug_journal"). Bewusst KEINE Spec: der Bau-Dialog ist
|
||||
neuer als die Spec-Grammatik (vf_linienzug_grammatik.py,
|
||||
vf_spec.lsp), eine Uebersetzung wuerde desynchronisieren.
|
||||
Der Treiber spielt das Journal per vsp-bau-journal ab.
|
||||
sonstige Bloecke (Separator, Scanner, ...)
|
||||
-> Einzel-Insert ("function": "insert", "block" ohne
|
||||
_2D/_3D-Suffix, damit die Dimension der Sitzung gilt)
|
||||
|
||||
Nicht uebernommen (gemeldet): "linienzug2" (Modus 2 - Journal verweist auf
|
||||
Pfad-Objekte der Zeichnung per Handle) und Bloecke ohne Bau-Parameter.
|
||||
|
||||
Aufruf:
|
||||
python dxf_anlage_export.py <dxf> <output.json> [--projekt NAME]
|
||||
|
||||
Beispiel (Dematic/Arvato, Nachbau-Daten fuer TEST_DEMATIC_ARVATO):
|
||||
python lib/dxf_anlage_export.py C:/develop/sps_skel/data/Dematic_TST.dxf \\
|
||||
tests/testdata/dematic_arvato.json --projekt Arvato
|
||||
|
||||
Bewusst OHNE ezdxf: reine Gruppencode-Lesung (vf_journal_export.gruppen),
|
||||
ASCII-DXF.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from vf_journal_export import TRENNER, gruppen # noqa: E402
|
||||
from dxf_scan_components import arrays_einzeilig, kreisel_anschluss # noqa: E402
|
||||
|
||||
PREFIX = "[dxf_anlage_export]"
|
||||
RUND = 2
|
||||
|
||||
# Kreisel-Durchmesser (Radmitte bis Kette), wie component_defaults.json.
|
||||
KREISEL_RADIUS = 400.0
|
||||
|
||||
# Bis zu diesem Abstand (mm) gilt ein GF-Ende als auf der Kette eines
|
||||
# Kreisels (der GF-Bau legt AS-/ES-Ursprung genau auf die Kette).
|
||||
GF_KREISEL_TOL = 50.0
|
||||
|
||||
DIM_SUFFIX = re.compile(r"_(2D|3D)$")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# DXF lesen
|
||||
# ============================================================
|
||||
|
||||
def _float(v):
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def lese_zeichnung(dxf_pfad):
|
||||
"""Modelspace-INSERTs (mit Attributen und XDATA je App) und die INSERTs
|
||||
jeder Blockdefinition.
|
||||
|
||||
Rueckgabe: (inserts, block_inhalt)
|
||||
inserts - Liste {name, handle, p, rot, attribs, xdata}; xdata ist
|
||||
app -> Liste der 1000er-Werte in Reihenfolge
|
||||
block_inhalt - blockname -> Liste {name, p} der direkt enthaltenen
|
||||
INSERTs (fuer den Kreisel-Mittenabstand)
|
||||
"""
|
||||
paare = gruppen(dxf_pfad)
|
||||
inserts = []
|
||||
block_inhalt = {}
|
||||
section = None
|
||||
block = None
|
||||
i, n = 0, len(paare)
|
||||
while i < n:
|
||||
code, wert = paare[i]
|
||||
if code != "0":
|
||||
i += 1
|
||||
continue
|
||||
if wert == "SECTION" and i + 1 < n:
|
||||
section = paare[i + 1][1].strip()
|
||||
elif wert == "ENDSEC":
|
||||
section = None
|
||||
elif wert == "BLOCK":
|
||||
j = i + 1
|
||||
while j < n and paare[j][0] != "0":
|
||||
if paare[j][0] == "2":
|
||||
block = paare[j][1]
|
||||
break
|
||||
j += 1
|
||||
elif wert == "ENDBLK":
|
||||
block = None
|
||||
elif wert == "INSERT" and section in ("ENTITIES", "BLOCKS"):
|
||||
e = {"name": None, "handle": None, "p": [0.0, 0.0, 0.0],
|
||||
"rot": 0.0, "attribs": {}, "xdata": {}, "ps": 0}
|
||||
app = None
|
||||
j = i + 1
|
||||
while j < n and paare[j][0] != "0":
|
||||
c, v = paare[j]
|
||||
if c == "2" and e["name"] is None:
|
||||
e["name"] = v
|
||||
elif c == "5" and e["handle"] is None:
|
||||
e["handle"] = v
|
||||
elif c in ("10", "20", "30") and app is None:
|
||||
e["p"][{"10": 0, "20": 1, "30": 2}[c]] = _float(v)
|
||||
elif c == "50":
|
||||
e["rot"] = _float(v)
|
||||
elif c == "67":
|
||||
e["ps"] = int(_float(v))
|
||||
elif c == "1001":
|
||||
app = v
|
||||
e["xdata"][app] = []
|
||||
elif c == "1000" and app:
|
||||
e["xdata"][app].append(v)
|
||||
j += 1
|
||||
# Attribute: ATTRIB-Entities bis SEQEND (Wert Code 1, Tag Code 2)
|
||||
while j < n and paare[j] == ("0", "ATTRIB"):
|
||||
k = j + 1
|
||||
tag = val = None
|
||||
while k < n and paare[k][0] != "0":
|
||||
if paare[k][0] == "2" and tag is None:
|
||||
tag = paare[k][1]
|
||||
elif paare[k][0] == "1" and val is None:
|
||||
val = paare[k][1]
|
||||
k += 1
|
||||
if tag:
|
||||
e["attribs"][tag] = val if val is not None else ""
|
||||
j = k
|
||||
if section == "ENTITIES":
|
||||
if e["ps"] != 1:
|
||||
inserts.append(e)
|
||||
elif block:
|
||||
block_inhalt.setdefault(block, []).append(
|
||||
{"name": e["name"], "p": e["p"]})
|
||||
i = j
|
||||
continue
|
||||
i += 1
|
||||
return inserts, block_inhalt
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Wrapper -> Eintraege
|
||||
# ============================================================
|
||||
|
||||
def _nummer(name):
|
||||
m = re.search(r"_(\d+)", name)
|
||||
return int(m.group(1)) if m else 0
|
||||
|
||||
|
||||
def _r(v):
|
||||
return round(float(v), RUND) + 0.0
|
||||
|
||||
|
||||
def _richtung(hz):
|
||||
rad = math.radians(hz)
|
||||
return (math.cos(rad), math.sin(rad))
|
||||
|
||||
|
||||
def kreisel_eintrag(e, block_inhalt, projekt):
|
||||
"""Kreisel wie hundm05:bau-kreisel ihn erwartet."""
|
||||
a = e["attribs"]
|
||||
abstand = _float(a.get("ABSTAND"))
|
||||
quelle = "Attribut ABSTAND (ganze mm)"
|
||||
# Genauer aus der Blockdefinition: Mittenabstand der beiden Stationen
|
||||
stationen = [s for s in block_inhalt.get(e["name"], [])
|
||||
if s["name"] and s["name"].upper().startswith(("AN8", "SP8"))]
|
||||
an = [s for s in stationen if s["name"].upper().startswith("AN8")]
|
||||
sp = [s for s in stationen if s["name"].upper().startswith("SP8")]
|
||||
if an and sp:
|
||||
abstand = math.hypot(sp[0]["p"][0] - an[0]["p"][0],
|
||||
sp[0]["p"][1] - an[0]["p"][1])
|
||||
quelle = "Blockdefinition (AN8 -> SP8)"
|
||||
nr = int(_float(a.get("NUMMER"))) or _nummer(e["name"])
|
||||
return collections.OrderedDict([
|
||||
("id", "Kreisel_%s_%02d" % (projekt, nr)),
|
||||
("function", "insert"),
|
||||
("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])),
|
||||
("abstand", _r(abstand)),
|
||||
("rotation", _r(e["rot"])),
|
||||
("typ", a.get("KREISELART") or "STANDARD"),
|
||||
("comment", "aus Zeichnung: %s (ID %s), abstand aus %s"
|
||||
% (e["name"], a.get("ID", "?"), quelle)),
|
||||
])
|
||||
|
||||
|
||||
def kreisel_geo(kreisel):
|
||||
"""kreisel_geo-Form (siehe dxf_scan_components.kreisel_anschluss) aus
|
||||
einem Kreisel-Eintrag: Achse ab AN8-Radmitte = Einfuegepunkt + Radius."""
|
||||
u = _richtung(kreisel["rotation"])
|
||||
return {"id": kreisel["id"],
|
||||
"a": (kreisel["x"] + KREISEL_RADIUS * u[0],
|
||||
kreisel["y"] + KREISEL_RADIUS * u[1]),
|
||||
"u": u, "laenge": kreisel["abstand"], "r": KREISEL_RADIUS,
|
||||
"z": kreisel["z"]}
|
||||
|
||||
|
||||
def kreisel_an_punkt(p, richtung, geo):
|
||||
"""Kreisel, auf dessen Kette p liegt (oder None)."""
|
||||
ans = kreisel_anschluss(p, richtung, geo)
|
||||
if ans and abs(ans[2]) <= GF_KREISEL_TOL:
|
||||
return ans[0]
|
||||
return None
|
||||
|
||||
|
||||
def gf_eintrag(e, geo, projekt, hinweise):
|
||||
"""Gefaellestrecke Modus 1 - Felder wie GEFAELLESTRECKE_EDIT sie liest."""
|
||||
a = e["attribs"]
|
||||
xd = e["xdata"].get("SSG_GF_EDIT")
|
||||
if not xd or len(xd) < 6:
|
||||
hinweise.append("%s ohne SSG_GF_EDIT-XDATA - uebersprungen" % e["name"])
|
||||
return None
|
||||
hz, winkel = _float(xd[0]), _float(xd[1])
|
||||
z_start, deltaL = _float(xd[4]), _float(xd[5])
|
||||
start = (e["p"][0], e["p"][1])
|
||||
r = _richtung(hz)
|
||||
ende = (start[0] + deltaL * r[0], start[1] + deltaL * r[1])
|
||||
eintrag = collections.OrderedDict([
|
||||
("test_id", "GF_%s_%02d" % (projekt, _nummer(e["name"]))),
|
||||
("modus", 1),
|
||||
("deltaL_mm", _r(deltaL)),
|
||||
("hz_grad", _r(hz)),
|
||||
("start_mm", [_r(start[0]), _r(start[1]), _r(z_start)]),
|
||||
("as_seite", a.get("SEITE_AS") or "links"),
|
||||
("es_seite", a.get("SEITE_ES") or "links"),
|
||||
("as_winkel", xd[2] or "90"),
|
||||
("es_winkel", xd[3] or "90"),
|
||||
])
|
||||
k_start = kreisel_an_punkt(start, r, geo)
|
||||
k_ende = kreisel_an_punkt(ende, (-r[0], -r[1]), geo)
|
||||
if k_start:
|
||||
eintrag["kreisel_start"] = k_start["id"]
|
||||
if k_ende:
|
||||
eintrag["kreisel_ende"] = k_ende["id"]
|
||||
eintrag["hoehe_ende_mm"] = k_ende["z"]
|
||||
kommentar = ("Endhoehe = Hoehe von %s (%.1f), Winkel rechnet der "
|
||||
"Treiber neu; gebaut war %.4f Grad -> HOEHE_BIS %s"
|
||||
% (k_ende["id"], k_ende["z"], winkel,
|
||||
a.get("HOEHE_BIS_mm", "?")))
|
||||
else:
|
||||
eintrag["winkel_grad"] = round(winkel, 4)
|
||||
kommentar = "kein Kreisel am Ende - gebauter Winkel uebernommen"
|
||||
eintrag["comment"] = "aus Zeichnung: %s (ID %s); %s" % (
|
||||
e["name"], a.get("ID", "?"), kommentar)
|
||||
return eintrag
|
||||
|
||||
|
||||
def vf_standard_eintrag(e, projekt, typ):
|
||||
"""VarioFoerderer im mubea-Schema (mubea:vario-bauen)."""
|
||||
a = e["attribs"]
|
||||
xd = e["xdata"].get("SSG_VF_EDIT") or []
|
||||
l_gf = [t for t in (a.get("L_GF_m") or "").split(",") if t.strip()]
|
||||
eintrag = collections.OrderedDict([
|
||||
("test_id", "VF_%s_%02d" % (projekt, _nummer(e["name"]))),
|
||||
("typ", typ),
|
||||
("richtung", a.get("ANTRIEBFAHRTRICHTUNG") or "Auf"),
|
||||
("deltaL", _r(_float(a.get("DELTA_L_mm")))),
|
||||
("deltaH", _r(_float(a.get("DELTA_H_mm")))),
|
||||
("winkel", int(_float(a.get("VF_WINKEL")))),
|
||||
("L_GF1", _r(1000.0 * _float(l_gf[0])) if l_gf else 0.0),
|
||||
("seite", a.get("SEITE_AS") or "rechts"),
|
||||
("motorseite", (a.get("MOTORSEITE") or "rechts").split(",")[0]),
|
||||
("hz", _r(_float(xd[1]) if len(xd) > 1 else 0.0)),
|
||||
("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])),
|
||||
("comment", "aus Zeichnung: %s (ID %s), Hoehe %s -> %s; deltaL/"
|
||||
"deltaH aus den (ganzzahligen) Attributen"
|
||||
% (e["name"], a.get("ID", "?"), a.get("HOEHE_VON_mm", "?"),
|
||||
a.get("HOEHE_BIS_mm", "?"))),
|
||||
])
|
||||
return eintrag
|
||||
|
||||
|
||||
def vf_journal_eintrag(e, projekt, nr):
|
||||
"""Linienzug: Journal unveraendert (Tokens 'ART:wert')."""
|
||||
a = e["attribs"]
|
||||
roh = "".join(e["xdata"]["SSG_VF_EDIT"][1:])
|
||||
tokens = [t for t in roh.split(TRENNER) if t]
|
||||
return collections.OrderedDict([
|
||||
("test_id", "VF_%s_LZ_%02d" % (projekt, nr)),
|
||||
("typ", "linienzug_journal"),
|
||||
("journal", tokens),
|
||||
("comment", "aus Zeichnung: %s (ID %s), %d Eingaben, Hoehe %s -> %s; "
|
||||
"wird 1:1 abgespielt (vsp-bau-journal)"
|
||||
% (e["name"], a.get("ID", "?"), len(tokens),
|
||||
a.get("HOEHE_VON_mm", "?"), a.get("HOEHE_BIS_mm", "?"))),
|
||||
])
|
||||
|
||||
|
||||
def einzel_eintrag(e):
|
||||
a = e["attribs"]
|
||||
info = ", ".join("%s %s" % (k, a[k]) for k in ("ID", "ZUORDNUNG", "ARTINR")
|
||||
if a.get(k))
|
||||
return collections.OrderedDict([
|
||||
("function", "insert"),
|
||||
("block", DIM_SUFFIX.sub("", e["name"])),
|
||||
("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])),
|
||||
("rotation", _r(e["rot"])),
|
||||
("comment", "aus Zeichnung: %s%s" % (e["name"],
|
||||
(" (%s)" % info) if info else "")),
|
||||
])
|
||||
|
||||
|
||||
def baue_eintraege(inserts, block_inhalt, projekt):
|
||||
"""Reihenfolge wie mubea.json: Kreisel, VF, GF, Einzelbloecke - die VF
|
||||
vor den GF, weil gf-init-bibliothek globale Elementmasse umsetzt."""
|
||||
kreisel, vf, lz, gf, einzel, hinweise = [], [], [], [], [], []
|
||||
for e in sorted(inserts, key=lambda e: (e["name"] or "", e["p"][0], e["p"][1])):
|
||||
name = e["name"] or ""
|
||||
if name.startswith("KREISEL_"):
|
||||
kreisel.append(kreisel_eintrag(e, block_inhalt, projekt))
|
||||
geo = [kreisel_geo(k) for k in kreisel]
|
||||
for e in inserts:
|
||||
name = e["name"] or ""
|
||||
if name.startswith("KREISEL_"):
|
||||
continue
|
||||
if name.startswith("GF_"):
|
||||
g = gf_eintrag(e, geo, projekt, hinweise)
|
||||
if g:
|
||||
gf.append(g)
|
||||
elif name.startswith("VF_"):
|
||||
xd = e["xdata"].get("SSG_VF_EDIT")
|
||||
marker = xd[0] if xd else None
|
||||
if marker in ("standard", "etage"):
|
||||
vf.append(vf_standard_eintrag(e, projekt, marker))
|
||||
elif marker == "linienzug":
|
||||
lz.append(vf_journal_eintrag(e, projekt, len(lz) + 1))
|
||||
else:
|
||||
hinweise.append("%s: Marker %r nicht nachbaubar - uebersprungen"
|
||||
% (name, marker))
|
||||
elif name.startswith("*") or name.startswith("ZZ_"):
|
||||
continue # anonyme Bloecke, Export-Marker
|
||||
else:
|
||||
einzel.append(einzel_eintrag(e))
|
||||
gf.sort(key=lambda g: g["test_id"])
|
||||
vf.sort(key=lambda v: v["test_id"])
|
||||
kreisel.sort(key=lambda k: k["id"])
|
||||
return kreisel + vf + lz + gf + einzel, hinweise
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(
|
||||
description="Gebaute Anlage (Wrapper-Bloecke) aus einer Zeichnung in "
|
||||
"Nachbau-Testdaten schreiben.")
|
||||
p.add_argument("dxf", help="Zeichnung (ASCII-DXF)")
|
||||
p.add_argument("output", help="Ziel-JSON")
|
||||
p.add_argument("--projekt", help="Kennung in den IDs (Default: Dateiname)")
|
||||
args = p.parse_args()
|
||||
if not os.path.isfile(args.dxf):
|
||||
print("%s FEHLER: DXF nicht gefunden: %s" % (PREFIX, args.dxf))
|
||||
sys.exit(1)
|
||||
projekt = args.projekt or os.path.splitext(os.path.basename(args.dxf))[0]
|
||||
|
||||
print("%s lese %s ..." % (PREFIX, args.dxf))
|
||||
inserts, block_inhalt = lese_zeichnung(args.dxf)
|
||||
eintraege, hinweise = baue_eintraege(inserts, block_inhalt, projekt)
|
||||
|
||||
zaehler = collections.Counter(
|
||||
"Kreisel" if "abstand" in e else
|
||||
"GF" if "modus" in e else
|
||||
"VF Linienzug" if e.get("typ") == "linienzug_journal" else
|
||||
"VF %s" % e["typ"] if "typ" in e and "test_id" in e else
|
||||
e.get("block", "?") for e in eintraege)
|
||||
for k, v in sorted(zaehler.items()):
|
||||
print("%s %-22s %d" % (PREFIX, k, v))
|
||||
for h in hinweise:
|
||||
print("%s ! %s" % (PREFIX, h))
|
||||
|
||||
with open(args.output, "w", encoding="utf-8") as fh:
|
||||
fh.write(arrays_einzeilig(json.dumps(eintraege, ensure_ascii=False,
|
||||
indent=2)))
|
||||
fh.write("\n")
|
||||
print("%s %d Eintraege geschrieben -> %s"
|
||||
% (PREFIX, len(eintraege), args.output))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+413
-3
@@ -11,6 +11,11 @@ also so, dass die Anlage per JSON-Treiber mit den Produktionsmakros
|
||||
nachgebaut werden kann (Vorbild: tests/testdata/mubea.json, das bisher von
|
||||
Hand aus einem DXF abgeleitet wurde).
|
||||
|
||||
Gefaellestrecken, die nicht als Blockpaar, sondern als 3D-LINE auf einem
|
||||
Staustrecken-Layer gezeichnet sind (GF_LINIE_LAYER), werden in einem zweiten
|
||||
Lesedurchgang erkannt und als Modus-1-Eintraege (Schema 2.1) ausgegeben,
|
||||
siehe gf_eintraege_aus_linien.
|
||||
|
||||
Aufruf:
|
||||
python dxf_scan_components.py <dxf> <data_dir> <output.json>
|
||||
[--fenster X0 Y0 X1 Y1]
|
||||
@@ -128,6 +133,43 @@ ACHSE_MAX_QUER = 1500.0
|
||||
# der Antriebsstationen, ohne benachbarte Bauteile zu verwechseln.
|
||||
ZWILLING_MAX_ABSTAND = 1500.0
|
||||
|
||||
# --- Gefaellestrecken aus Staustrecken-Linien -------------------------------
|
||||
# Manche Zeichnungen (Dematic_arvato_reduced.dxf) fuehren die Gefaellestrecke
|
||||
# nicht als Blockpaar, sondern als einfache 3D-LINE auf einem Staustrecken-
|
||||
# Layer vom hohen zum tiefen Kreisel (dort: 27 Linien auf "6-SP", 10654 mm
|
||||
# lang, 370 mm Gefaelle). Nur Modelspace-LINEs; Muster wie FAMILIEN.
|
||||
GF_LINIE_LAYER = ["6-SP"]
|
||||
|
||||
# Mindest-Hoehendifferenz (mm), ab der eine Staustrecken-Linie als Gefaelle
|
||||
# gilt. Darunter ist sie eine waagrechte Strecke - die liegen auf denselben
|
||||
# Layern (Rueckfuehrungen, Kreisel-Verbindungen) und sind keine GF.
|
||||
GF_LINIE_MIN_DZ = 100.0
|
||||
|
||||
# Suchbereich fuer das Ausschleuselement am oberen Linienende, gemessen im
|
||||
# Linien-KS: "entlang" ab Linienstart in Gefaellerichtung, "quer" seitlich.
|
||||
# In Dematic_arvato_reduced.dxf sitzt es 1158 mm entlang und 467 mm quer -
|
||||
# der Nachbar-AS der Linie daneben (800 mm Raster) liegt nur 433 mm quer auf
|
||||
# der ANDEREN Seite. Darum entscheidet die Seite aus dem Blocknamen (links/
|
||||
# rechts), nicht der kleinste Abstand.
|
||||
GF_AS_MAX_ENTLANG = 2000.0
|
||||
GF_AS_MAX_QUER = 700.0
|
||||
|
||||
# Groesste Luecke (mm) zwischen Linienende und Kreisel-Kette, bis zu der die
|
||||
# Linie an diesen Kreisel angeschlossen wird. Gezeichnet ist nur die
|
||||
# Staustrecke: oben fehlt das Ausschleuselement (~430 mm), unten das
|
||||
# Einschleuselement (~480 mm) bis zur Kette.
|
||||
GF_KREISEL_MAX_LUECKE = 1500.0
|
||||
|
||||
# Separatoren auf der GF-Achse: die Gefaellestrecke baut ihren eigenen
|
||||
# (Staustrecke_Separator_SP_300_mm), der gezeichnete waere doppelt. Gilt fuer
|
||||
# Vorkommen bis GF_SEP_MAX_QUER (mm) neben der Achse.
|
||||
GF_SEP_MUSTER = ["S-SP", "S-LP", "Separator*"]
|
||||
GF_SEP_MAX_QUER = 300.0
|
||||
|
||||
# Abstand (mm), bis zu dem eine --kreisel-hoehe X Y Z dem Kreisel mit diesem
|
||||
# Einfuegepunkt (Eintrag x/y) zugeordnet wird.
|
||||
KREISEL_HOEHE_TOL = 50.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen
|
||||
@@ -593,6 +635,62 @@ def lese_inserts(dxf_path):
|
||||
return ms_inserts, block_inserts, block_base, statistik, block_info
|
||||
|
||||
|
||||
def lese_linien(dxf_path, layer_muster):
|
||||
"""Liest alle Modelspace-LINEs auf den Layern aus layer_muster.
|
||||
|
||||
Eigener Durchlauf statt Erweiterung von lese_inserts: dessen Rueckgabe
|
||||
nutzt auch lib/dxf_abbild.py, und die Linien braucht nur die
|
||||
Gefaellestrecken-Erkennung. Kostet einen zweiten Lesedurchgang.
|
||||
|
||||
Rueckgabe: Liste {"layer", "a": [x, y, z], "b": [x, y, z]} in WCS
|
||||
(eine LINE ist immer in WCS angegeben, keine OCS-Umrechnung noetig).
|
||||
"""
|
||||
linien = []
|
||||
section = None
|
||||
warte_auf_sectionname = False
|
||||
rec = None
|
||||
|
||||
def ablegen(r):
|
||||
if r and r["ps"] != 1 and matches_any(r["layer"] or "", layer_muster):
|
||||
linien.append({"layer": r["layer"], "a": r["a"], "b": r["b"]})
|
||||
|
||||
fh = io.open(dxf_path, "rb")
|
||||
try:
|
||||
mm = mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)
|
||||
except ValueError:
|
||||
fh.close()
|
||||
raise
|
||||
try:
|
||||
for code, value in iter_tags(mm):
|
||||
if code == 0:
|
||||
ablegen(rec)
|
||||
rec = None
|
||||
if value == "SECTION":
|
||||
warte_auf_sectionname = True
|
||||
elif value == "ENDSEC":
|
||||
section = None
|
||||
elif value == "LINE" and section == "ENTITIES":
|
||||
rec = {"layer": None, "a": [0.0, 0.0, 0.0],
|
||||
"b": [0.0, 0.0, 0.0], "ps": 0}
|
||||
elif code == 2 and warte_auf_sectionname:
|
||||
section = value
|
||||
warte_auf_sectionname = False
|
||||
elif rec is not None:
|
||||
if code == 8:
|
||||
rec["layer"] = value
|
||||
elif code in (10, 20, 30):
|
||||
rec["a"][{10: 0, 20: 1, 30: 2}[code]] = value
|
||||
elif code in (11, 21, 31):
|
||||
rec["b"][{11: 0, 21: 1, 31: 2}[code]] = value
|
||||
elif code == 67:
|
||||
rec["ps"] = value
|
||||
ablegen(rec)
|
||||
finally:
|
||||
mm.close()
|
||||
fh.close()
|
||||
return linien
|
||||
|
||||
|
||||
def insert_matrix(rec, block_base):
|
||||
"""Transformationsmatrix eines INSERT (inkl. Basispunkt des Zielblocks).
|
||||
|
||||
@@ -1213,15 +1311,242 @@ def sensor_eintrag(t, bekannte_bloecke=None, hinweise=None, treffer=None):
|
||||
}
|
||||
|
||||
|
||||
def as_am_ende(p, richtung, as_liste, belegt):
|
||||
"""Ausschleuselement an einem Linienende suchen.
|
||||
|
||||
p = Linienende (x, y), richtung = Einheitsvektor (x, y) von dort in die
|
||||
Linie hinein. Gesucht wird im Linien-KS (siehe GF_AS_MAX_ENTLANG/-QUER);
|
||||
"quer" ist positiv LINKS der Richtung. Ein AS mit Seite im Blocknamen muss
|
||||
auf dieser Seite liegen - das trennt ihn vom Nachbar-AS der Linie daneben.
|
||||
Bereits vergebene AS (belegt = Menge von id()) zaehlen nicht.
|
||||
|
||||
Rueckgabe: (treffer, zielblock, geraten) oder None.
|
||||
"""
|
||||
beste = None
|
||||
for t in as_liste:
|
||||
if id(t) in belegt:
|
||||
continue
|
||||
vx, vy = t["x"] - p[0], t["y"] - p[1]
|
||||
entlang = vx * richtung[0] + vy * richtung[1]
|
||||
quer = richtung[0] * vy - richtung[1] * vx
|
||||
if not (0.0 <= entlang <= GF_AS_MAX_ENTLANG and abs(quer) <= GF_AS_MAX_QUER):
|
||||
continue
|
||||
block, geraten = mappe_block(t["block"])
|
||||
if "Seite=%s" % DEFAULT_SEITE not in (geraten or []):
|
||||
if block.endswith("_links") and quer < 0:
|
||||
continue
|
||||
if block.endswith("_rechts") and quer > 0:
|
||||
continue
|
||||
if beste is None or abs(quer) < beste[0]:
|
||||
beste = (abs(quer), t, block, geraten or [])
|
||||
return beste[1:] if beste else None
|
||||
|
||||
|
||||
def kreisel_anschluss(p, richtung, kreisel):
|
||||
"""Schnittpunkt einer GF-Achse mit der naechsten Kreisel-Kette.
|
||||
|
||||
p = Linienende (x, y), richtung = Einheitsvektor (x, y) der GF-Achse.
|
||||
Je Kreisel (siehe kreisel_geo in baue_eintraege: Achse von der AN8- zur
|
||||
SP8-Radmitte, Kette im Abstand r beidseits) wird die Kette auf der Seite
|
||||
von p genommen und die Achse bis dorthin verlaengert bzw. gekuerzt. Nur
|
||||
Kreisel, an deren Laenge p liegt und deren Kette hoechstens
|
||||
GF_KREISEL_MAX_LUECKE entfernt ist; bei mehreren gewinnt die kleinste
|
||||
Verschiebung.
|
||||
|
||||
Rueckgabe: (kreisel, punkt (x, y), verschiebung entlang richtung) oder None.
|
||||
"""
|
||||
beste = None
|
||||
for k in kreisel:
|
||||
ux, uy = k["u"]
|
||||
nx, ny = -uy, ux
|
||||
vx, vy = p[0] - k["a"][0], p[1] - k["a"][1]
|
||||
t = vx * ux + vy * uy
|
||||
q = vx * nx + vy * ny
|
||||
if not (-k["r"] <= t <= k["laenge"] + k["r"]):
|
||||
continue
|
||||
if abs(abs(q) - k["r"]) > GF_KREISEL_MAX_LUECKE:
|
||||
continue
|
||||
dn = richtung[0] * nx + richtung[1] * ny
|
||||
if abs(dn) < 1e-6: # GF parallel zum Kreisel
|
||||
continue
|
||||
kette = k["r"] if q >= 0 else -k["r"]
|
||||
lam = (kette - q) / dn
|
||||
if beste is None or abs(lam) < abs(beste[2]):
|
||||
beste = (k, (p[0] + lam * richtung[0], p[1] + lam * richtung[1]), lam)
|
||||
return beste
|
||||
|
||||
|
||||
def gf_eintraege_aus_linien(linien, as_liste, kreisel=None, sep_liste=None,
|
||||
winkel_default=3.0):
|
||||
"""Gefaellestrecken (Modus 1, Schema 2.1) aus Staustrecken-Linien.
|
||||
|
||||
Eine Gefaellestrecke laeuft immer bergab: Start ist das HOEHERE
|
||||
Linienende, unabhaengig von der Zeichenrichtung. Das Ausschleuselement
|
||||
gehoert an dieses obere Ende. Sitzt es dagegen am UNTEREN Ende, faehrt
|
||||
die Strecke vom tiefen Kreisel hinauf - das ist ein Steigfoerderer und
|
||||
keine GF (Dematic_arvato_reduced.dxf: 3 solche Linien); sie wird
|
||||
gemeldet und uebersprungen.
|
||||
|
||||
Anschluss an die Kreisel: gezeichnet ist nur die Staustrecke. Das Makro
|
||||
legt den Ursprung des AS-Elements auf start_mm und dessen Einlauf
|
||||
(KS_EIN) seitlich daneben, auf gleicher Hoehe der Achse; ebenso liegt der
|
||||
Auslauf des ES-Elements neben dessen Ursprung, und deltaL_mm reicht von
|
||||
AS- bis ES-Ursprung (gemessen an GF_Mubea: 9510 -> 399.2 = 9110.8). Damit
|
||||
beide die Kreisel-Kette treffen, werden Start und Ende auf die jeweils
|
||||
naechste Kette geschoben (kreisel_anschluss). Ohne Kreisel in Reichweite
|
||||
bleibt das Linienende stehen und es gibt einen Hinweis.
|
||||
|
||||
Hoehen: tragen beide Kreisel eine Anwender-Hoehe ("z_anwender"), wird
|
||||
start_mm.z die des oberen und hoehe_ende_mm die des unteren Kreisels -
|
||||
den Winkel rechnet dann der LISP-Treiber mit gf-winkel-berechnen
|
||||
(dieselbe Naeherung wie Modus 3, mit den Elementmassen der aktuellen
|
||||
2D/3D-Bibliothek). Sonst ist winkel_default ein Platzhalter.
|
||||
|
||||
Rueckgabe: (eintraege, verbrauchte ids, hinweise). Verbraucht werden das
|
||||
AS am oberen Ende und Separatoren auf der Achse (sep_liste) - beides baut
|
||||
die Gefaellestrecke selbst mit, als Einzel-Insert stuenden sie doppelt in
|
||||
der Zeichnung (und im Sivas-Export). Der AS eines uebersprungenen
|
||||
Steigfoerderers bleibt unverbraucht.
|
||||
"""
|
||||
eintraege, hinweise = [], []
|
||||
belegt = set()
|
||||
waagrecht = 0
|
||||
kandidaten = []
|
||||
for ln in linien:
|
||||
a, b = ln["a"], ln["b"]
|
||||
if abs(a[2] - b[2]) < GF_LINIE_MIN_DZ:
|
||||
waagrecht += 1
|
||||
continue
|
||||
hoch, tief = (a, b) if a[2] > b[2] else (b, a)
|
||||
lxy = math.hypot(tief[0] - hoch[0], tief[1] - hoch[1])
|
||||
if lxy <= 0.0:
|
||||
continue
|
||||
kandidaten.append((hoch, tief, lxy, ln["layer"]))
|
||||
kandidaten.sort(key=lambda k: (round(k[0][0], RUND), round(k[0][1], RUND)))
|
||||
|
||||
for hoch, tief, lxy, layer in kandidaten:
|
||||
r = ((tief[0] - hoch[0]) / lxy, (tief[1] - hoch[1]) / lxy)
|
||||
oben = as_am_ende(hoch, r, as_liste, belegt)
|
||||
if oben is None:
|
||||
unten = as_am_ende(tief, (-r[0], -r[1]), as_liste, belegt)
|
||||
if unten is not None:
|
||||
# AS NICHT verbrauchen: er gehoert zum Steigfoerderer und
|
||||
# bleibt als Einzel-Insert in der Ausgabe stehen.
|
||||
hinweise.append(
|
||||
"Staustrecken-Linie (%.0f, %.0f, %.0f) -> (%.0f, %.0f, "
|
||||
"%.0f): Ausschleuselement am UNTEREN Ende - Steigfoerderer, "
|
||||
"keine Gefaellestrecke, uebersprungen"
|
||||
% (tief[0], tief[1], tief[2], hoch[0], hoch[1], hoch[2]))
|
||||
continue
|
||||
tid = "GF_%s_%02d" % (PROJEKT, len(eintraege) + 1)
|
||||
kommentar = ("aus DXF: LINE auf Layer '%s' (%.2f, %.2f) -> (%.2f, "
|
||||
"%.2f), Laenge XY %.2f"
|
||||
% (layer, hoch[0], hoch[1], tief[0], tief[1], lxy))
|
||||
|
||||
# Start/Ende auf die Kreisel-Ketten schieben
|
||||
start, ende = (hoch[0], hoch[1]), (tief[0], tief[1])
|
||||
k_start = k_ende = None
|
||||
for wo, p, rr in (("oberen", hoch, r), ("unteren", tief, (-r[0], -r[1]))):
|
||||
ans = kreisel_anschluss(p, rr, kreisel or [])
|
||||
if ans is None:
|
||||
hinweise.append("%s: kein Kreisel am %s Ende - Linienende "
|
||||
"bleibt stehen" % (tid, wo))
|
||||
kommentar += "; am %s Ende kein Kreisel in Reichweite" % wo
|
||||
continue
|
||||
k, punkt, lam = ans
|
||||
kommentar += ("; am %s Ende um %.1f mm bis zur Kette von %s "
|
||||
"verlaengert" % (wo, -lam, k["id"]))
|
||||
if wo == "oberen":
|
||||
start, k_start = punkt, k
|
||||
else:
|
||||
ende, k_ende = punkt, k
|
||||
laenge = (ende[0] - start[0]) * r[0] + (ende[1] - start[1]) * r[1]
|
||||
|
||||
eintrag = {
|
||||
"test_id": tid,
|
||||
"modus": 1,
|
||||
"deltaL_mm": round(laenge, RUND),
|
||||
"hz_grad": round(math.degrees(math.atan2(r[1], r[0])) % 360.0, RUND) + 0.0,
|
||||
"start_mm": [round(start[0], RUND) + 0.0, round(start[1], RUND) + 0.0, 0.0],
|
||||
"as_seite": DEFAULT_SEITE,
|
||||
"es_seite": "links",
|
||||
"as_winkel": str(DEFAULT_SCHLEUS_WINKEL),
|
||||
"es_winkel": str(DEFAULT_SCHLEUS_WINKEL),
|
||||
}
|
||||
if k_start:
|
||||
eintrag["kreisel_start"] = k_start["id"]
|
||||
if k_ende:
|
||||
eintrag["kreisel_ende"] = k_ende["id"]
|
||||
if (k_start and k_ende and k_start.get("z_anwender") is not None
|
||||
and k_ende.get("z_anwender") is not None):
|
||||
eintrag["start_mm"][2] = float(k_start["z_anwender"])
|
||||
eintrag["hoehe_ende_mm"] = float(k_ende["z_anwender"])
|
||||
eintrag["hoehe_quelle"] = "Anwender"
|
||||
kommentar += ("; Hoehen vom Anwender (Kreisel %s / %s), den Winkel "
|
||||
"rechnet der Treiber" % (k_start["id"], k_ende["id"]))
|
||||
else:
|
||||
eintrag["winkel_grad"] = winkel_default
|
||||
eintrag["hoehe_unbekannt"] = True
|
||||
kommentar += ("; Hoehen unbekannt - winkel_grad %.1f ist Platzhalter"
|
||||
% winkel_default)
|
||||
|
||||
if oben is not None:
|
||||
t, block, geraten = oben
|
||||
belegt.add(id(t))
|
||||
teile = block.split("_") # AS_Element_<winkel>_<seite>
|
||||
eintrag["as_winkel"], eintrag["as_seite"] = teile[2], teile[3]
|
||||
kommentar += ("; AS aus Block '%s' bei (%.2f, %.2f) -> %s, als "
|
||||
"Einzel-Insert entfernt (baut die GF selbst)"
|
||||
% (t["block"], t["x"], t["y"], block))
|
||||
if geraten:
|
||||
kommentar += " - geraten: %s" % ", ".join(geraten)
|
||||
else:
|
||||
kommentar += ("; KEIN Ausschleuselement am oberen Ende gefunden - "
|
||||
"as_seite/as_winkel sind Defaults")
|
||||
hinweise.append("%s ohne Ausschleuselement am oberen Ende" % tid)
|
||||
|
||||
# Separatoren auf der Achse zwischen Start und Ende
|
||||
weg = []
|
||||
for s in sep_liste or []:
|
||||
if id(s) in belegt or not matches_any(s["block"], GF_SEP_MUSTER):
|
||||
continue
|
||||
vx, vy = s["x"] - start[0], s["y"] - start[1]
|
||||
entlang = vx * r[0] + vy * r[1]
|
||||
quer = r[0] * vy - r[1] * vx
|
||||
if 0.0 <= entlang <= laenge and abs(quer) <= GF_SEP_MAX_QUER:
|
||||
belegt.add(id(s))
|
||||
weg.append("%s bei (%.0f, %.0f)" % (s["block"], s["x"], s["y"]))
|
||||
if weg:
|
||||
kommentar += ("; Separator auf der Achse entfernt (die GF baut "
|
||||
"ihren eigenen): %s" % ", ".join(weg))
|
||||
|
||||
kommentar += "; es_seite/es_winkel sind Defaults (kein ES im DXF)."
|
||||
eintrag["comment"] = kommentar
|
||||
eintraege.append(eintrag)
|
||||
|
||||
if waagrecht:
|
||||
hinweise.append("%d waagrechte Staustrecken-Linie(n) auf %s - keine "
|
||||
"Gefaellestrecke, nicht ausgegeben"
|
||||
% (waagrecht, "/".join(GF_LINIE_LAYER)))
|
||||
return eintraege, belegt, hinweise
|
||||
|
||||
|
||||
def baue_eintraege(treffer, defaults, boegen, weichen,
|
||||
bekannte_bloecke=None):
|
||||
"""Erkannte Vorkommen auf die Schemata aus object_data.md abbilden."""
|
||||
bekannte_bloecke=None, linien=None, kreisel_hoehen=None):
|
||||
"""Erkannte Vorkommen auf die Schemata aus object_data.md abbilden.
|
||||
|
||||
kreisel_hoehen: Liste (x, y, z) vom Anwender - z gilt fuer den Kreisel,
|
||||
dessen Einfuegepunkt (Eintrag x/y) hoechstens KREISEL_HOEHE_TOL entfernt
|
||||
liegt, und fuer die Gefaellestrecken, die an ihm anschliessen.
|
||||
"""
|
||||
nach_familie = {}
|
||||
for t in treffer:
|
||||
nach_familie.setdefault(t["familie"], []).append(t)
|
||||
|
||||
eintraege = []
|
||||
hinweise = []
|
||||
kreisel_geo = [] # Achse/Kette je Kreisel fuer kreisel_anschluss
|
||||
offene_hoehen = list(kreisel_hoehen or [])
|
||||
|
||||
# --- Kreisel: AN8 (Antriebsstation) + SP8 (Spannstation) --------------
|
||||
durchmesser = defaults.get("kreisel", {}).get("durchmesser", 800.0)
|
||||
@@ -1275,10 +1600,47 @@ def baue_eintraege(treffer, defaults, boegen, weichen,
|
||||
sp["block"], sp["layer"], sp["x"], sp["y"], dist,
|
||||
rotation)) + hoehe_hinweis,
|
||||
})
|
||||
kreisel = eintraege[-1]
|
||||
for h in offene_hoehen:
|
||||
if math.hypot(h[0] - kreisel["x"], h[1] - kreisel["y"]) <= KREISEL_HOEHE_TOL:
|
||||
kreisel["z"] = float(h[2])
|
||||
kreisel["hoehe_quelle"] = "Anwender"
|
||||
kreisel["comment"] += " Hoehe %.1f vom Anwender." % h[2]
|
||||
offene_hoehen.remove(h)
|
||||
break
|
||||
kreisel_geo.append({
|
||||
"id": kreisel["id"],
|
||||
"a": (an["x"], an["y"]),
|
||||
"u": (math.cos(richtung), math.sin(richtung)),
|
||||
"laenge": dist,
|
||||
"r": radius,
|
||||
"z_anwender": kreisel["z"] if kreisel.get("hoehe_quelle") else None,
|
||||
})
|
||||
for h in offene_hoehen:
|
||||
hinweise.append("Kreisel-Hoehe %.1f bei (%.2f, %.2f): kein Kreisel-"
|
||||
"Einfuegepunkt in %g mm - nicht verwendet"
|
||||
% (h[2], h[0], h[1], KREISEL_HOEHE_TOL))
|
||||
for sp in sp_liste:
|
||||
hinweise.append("SP8 bei (%.2f, %.2f) ohne AN8-Partner" % (sp["x"], sp["y"]))
|
||||
eintraege.append(sensor_eintrag(sp, bekannte_bloecke, hinweise, treffer))
|
||||
|
||||
# --- Gefaellestrecken aus Staustrecken-Linien ---------------------------
|
||||
# Nach den Kreiseln (Anschluss an deren Ketten) und vor dem Rest-Block:
|
||||
# sie entnehmen ihre Ausschleuselemente und Separatoren aus den Familien
|
||||
# (siehe gf_eintraege_aus_linien), der Rest-Block darf diese nicht mehr
|
||||
# als Einzel-Insert ausgeben.
|
||||
if linien:
|
||||
as_liste = [t for t in nach_familie.get("schleuselement", [])
|
||||
if mappe_block(t["block"])[0].startswith("AS_Element")]
|
||||
gf, belegt, gf_hinweise = gf_eintraege_aus_linien(
|
||||
linien, as_liste, kreisel_geo, nach_familie.get("sensor", []),
|
||||
defaults.get("gefaelle", {}).get("default_winkel", 3.0))
|
||||
eintraege.extend(gf)
|
||||
hinweise.extend(gf_hinweise)
|
||||
for familie in ("schleuselement", "sensor"):
|
||||
nach_familie[familie] = [t for t in nach_familie.get(familie, [])
|
||||
if id(t) not in belegt]
|
||||
|
||||
# --- Foerderer: Antriebs- und Umlenkstation paaren ---------------------
|
||||
gf_winkel_min = defaults.get("gefaelle", {}).get("default_winkel", 3.0)
|
||||
paare, rest_a, rest_u = paare_bilden(nach_familie.get("foerderer_antrieb", []),
|
||||
@@ -1400,6 +1762,37 @@ def filtere_fenster(eintraege, fenster):
|
||||
return behalten, len(eintraege) - len(behalten), ohne_punkte
|
||||
|
||||
|
||||
def hoehen_entfernen(eintraege):
|
||||
"""Alle Hoehen auf 0 setzen, ausser den vom Anwender vorgegebenen.
|
||||
|
||||
Fuer Zeichnungen, deren Z-Werte (noch) nicht belastbar sind: lieber
|
||||
gar keine Hoehe als eine, die nach Einbauhoehe aussieht und keine ist.
|
||||
Ausgenommen sind Eintraege mit hoehe_quelle "Anwender" (--kreisel-hoehe
|
||||
und die daran anschliessenden Gefaellestrecken).
|
||||
Rueckgabe: Anzahl der geaenderten Eintraege.
|
||||
"""
|
||||
n = 0
|
||||
for e in eintraege:
|
||||
if e.get("hoehe_quelle") == "Anwender":
|
||||
continue
|
||||
geaendert = False
|
||||
for feld in ("z", "hoehe"):
|
||||
if feld in e and e[feld] != 0.0:
|
||||
e[feld] = 0.0
|
||||
geaendert = True
|
||||
for feld in ("startpunkt", "start_mm", "endpunkt"):
|
||||
p = e.get(feld)
|
||||
if isinstance(p, list) and len(p) >= 3 and p[2] != 0.0:
|
||||
p[2] = 0.0
|
||||
geaendert = True
|
||||
if any(f in e for f in ("z", "hoehe", "startpunkt", "start_mm")):
|
||||
e["hoehe_unbekannt"] = True
|
||||
if geaendert:
|
||||
e["comment"] = e.get("comment", "") + " [Hoehe entfernt: --ohne-hoehen]"
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def arrays_einzeilig(text):
|
||||
"""Zahlen-Arrays im JSON-Text auf eine Zeile ziehen.
|
||||
|
||||
@@ -1471,6 +1864,15 @@ def main():
|
||||
help="Nur Bauteile in diesem XY-Fenster (mm) "
|
||||
"ausgeben - fuer den Nachbau eines einzelnen "
|
||||
"Anlagenabschnitts statt der ganzen Zeichnung")
|
||||
parser.add_argument("--ohne-hoehen", action="store_true",
|
||||
help="Alle Hoehen auf 0 setzen (Z der Zeichnung nicht "
|
||||
"belastbar); ausgenommen --kreisel-hoehe")
|
||||
parser.add_argument("--kreisel-hoehe", nargs=3, type=float, action="append",
|
||||
metavar=("X", "Y", "Z"),
|
||||
help="Hoehe Z fuer den Kreisel mit Einfuegepunkt X/Y "
|
||||
"(wie im Ausgabe-JSON); gilt auch fuer Start/Ende "
|
||||
"der daran anschliessenden Gefaellestrecken. "
|
||||
"Mehrfach angebbar")
|
||||
parser.add_argument("--projekt",
|
||||
help="Projektkennung in den erzeugten IDs "
|
||||
"(Default: Name der DXF-Datei ohne Endung)")
|
||||
@@ -1529,8 +1931,16 @@ def main():
|
||||
dim_stat["attribute_uebernommen"], dim_stat["nur_2d"],
|
||||
dim_stat["nur_3d"], len(treffer)))
|
||||
|
||||
linien = lese_linien(dxf_path, GF_LINIE_LAYER)
|
||||
print("%s %d Staustrecken-Linien auf %s" % (PREFIX, len(linien),
|
||||
"/".join(GF_LINIE_LAYER)))
|
||||
|
||||
eintraege, hinweise = baue_eintraege(treffer, defaults, boegen, weichen,
|
||||
bekannte_bloecke)
|
||||
bekannte_bloecke, linien,
|
||||
args.kreisel_hoehe)
|
||||
if args.ohne_hoehen:
|
||||
print("%s --ohne-hoehen: Hoehe in %d Eintraegen auf 0 gesetzt"
|
||||
% (PREFIX, hoehen_entfernen(eintraege)))
|
||||
|
||||
if fenster:
|
||||
vorher = len(eintraege)
|
||||
|
||||
Reference in New Issue
Block a user