tro_annotate: Lagebezug ueber die TeileId, Ablaufbeschreibung CSV/DXF -> SCL
dxf_registration.derive_from_ids: Zeichnungen aus dxfmakros tragen an jedem Layout-Block das Attribut ID = TeileId des CSV. tro_annotate paart CSV- Separatoren mit den Separator-Bloecken gleicher ID (sonst Scanner) und nimmt den Median-Versatz - eindeutig statt Abstimmung, bei jedem Lauf neu, gilt auch fuer eine verschobene Kopie der Zeichnung. Vorrang vor abgelegtem und abgestimmtem Bezug; --offset bleibt. Parameter id_spread_mm, id_min_share in cfg/parameter.ini [dxf_registration]. Dematic: gemeinsame Zeichnung 0/0, Mechanik-Zeichnung dematic.dxf +466,1/-163549,3 mm. Die Abstimmung rastete vorher auf den Scanner-Bloecken ein (Einfuegepunkt 360 mm neben der CSV-Position) und lag 360 mm daneben. Gesperrte Ausgabedatei (in BricsCAD geoeffnet) gibt eine Meldung statt Traceback. Neu: doc/Workflow_CSV_DXF_to_SCL.md - der ganze Ablauf vom CSV-Export und der Zeichnung bis zur SCL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,7 @@ __all__ = [
|
||||
"RegistrationError",
|
||||
"ANCHOR_BLOCKS",
|
||||
"derive_transform",
|
||||
"derive_from_ids",
|
||||
"verify_transform",
|
||||
"load_transform",
|
||||
"save_transform",
|
||||
@@ -97,6 +98,8 @@ FIT_TOL = get_float(_P, "fit_tol")
|
||||
DEFAULT_MATCH_TOL = get_float(_P, "default_match_tol")
|
||||
MIN_INLIERS = get_int(_P, "min_inliers")
|
||||
ROTATION_WARN_DEG = get_float(_P, "rotation_warn_deg")
|
||||
ID_SPREAD_MM = get_float(_P, "id_spread_mm")
|
||||
ID_MIN_SHARE = get_float(_P, "id_min_share")
|
||||
|
||||
|
||||
class RegistrationError(RuntimeError):
|
||||
@@ -304,6 +307,56 @@ def derive_transform(
|
||||
return transform
|
||||
|
||||
|
||||
def derive_from_ids(
|
||||
csv_by_id: dict[str, tuple[str, float, float]],
|
||||
dxf_by_id: dict[str, tuple[str, float, float]],
|
||||
csv_file: str = "",
|
||||
dxf_file: str = "",
|
||||
) -> Transform | None:
|
||||
"""
|
||||
Lagebezug ueber die TeileId: CSV-Objekt und DXF-Block mit derselben ID.
|
||||
|
||||
Mit dxfmakros gebaute Zeichnungen tragen an jedem Layout-Block das
|
||||
Attribut ID - dieselbe Nummer, die der CSV-Export als TeileId fuehrt. Das
|
||||
ist eine eindeutige Zuordnung, keine Abstimmung ueber Raster, und gilt
|
||||
genauso fuer eine verschobene Kopie der Zeichnung (nur Mechanik als
|
||||
Hintergrund) wie fuer die Zeichnung, aus der das CSV stammt.
|
||||
|
||||
Parameter:
|
||||
csv_by_id: TeileId -> (Rolle, x, y) aus dem CSV (Rolle wie in
|
||||
ANCHOR_BLOCKS: "Separator", "Scanner").
|
||||
dxf_by_id: ID-Attribut -> (Rolle, x, y) der Bloecke im DXF.
|
||||
|
||||
Rueckgabe:
|
||||
Transform (DXF -> CSV, ohne Drehung) oder None, wenn keine Rolle
|
||||
genug uebereinstimmende Paare hat. Die Rollen werden in der
|
||||
Reihenfolge von ANCHOR_BLOCKS versucht: der Separator-Block sitzt
|
||||
fast genau auf der CSV-Position (Dematic: 60 mm), der Scanner-Block
|
||||
360 mm daneben. Ausgedehnte Objekte (Kreisel, Strecken) zaehlen
|
||||
nicht - ihr Einfuegepunkt liegt je nach Lage weit vom CSV-Mittelpunkt.
|
||||
Eine Rolle gilt, wenn mindestens MIN_INLIERS Paare und ID_MIN_SHARE
|
||||
aller Paare hoechstens ID_SPREAD_MM vom Median entfernt liegen.
|
||||
"""
|
||||
for rolle in ANCHOR_BLOCKS:
|
||||
paare = [(c[1] - d[1], c[2] - d[2])
|
||||
for i, c in csv_by_id.items()
|
||||
if c[0] == rolle and (d := dxf_by_id.get(i)) is not None and d[0] == rolle]
|
||||
if len(paare) < MIN_INLIERS:
|
||||
continue
|
||||
mdx = sorted(p[0] for p in paare)[len(paare) // 2]
|
||||
mdy = sorted(p[1] for p in paare)[len(paare) // 2]
|
||||
passend = [p for p in paare if math.hypot(p[0] - mdx, p[1] - mdy) <= ID_SPREAD_MM]
|
||||
if len(passend) < MIN_INLIERS or len(passend) < ID_MIN_SHARE * len(paare):
|
||||
continue
|
||||
return Transform(
|
||||
dx=mdx, dy=mdy, source="ids", csv_file=csv_file, dxf_file=dxf_file,
|
||||
anchor=(f"TeileId = ID-Attribut: {len(paare)} {rolle}-Paare, "
|
||||
f"{len(passend)} davon hoechstens {ID_SPREAD_MM:.0f} mm um den Median"),
|
||||
created=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def verify_transform(
|
||||
transform: Transform,
|
||||
csv_points: Points,
|
||||
|
||||
+58
-2
@@ -101,6 +101,7 @@ from dxf_registration import (
|
||||
ANCHOR_BLOCKS,
|
||||
RegistrationError,
|
||||
Transform,
|
||||
derive_from_ids,
|
||||
derive_transform,
|
||||
load_transform,
|
||||
save_transform,
|
||||
@@ -229,14 +230,63 @@ def anchor_points_dxf(msp) -> dict[str, list[tuple[float, float]]]:
|
||||
return dict(points)
|
||||
|
||||
|
||||
def _role_of(block_name: str) -> str:
|
||||
"""Rolle eines Blocks nach ANCHOR_BLOCKS ("" wenn keine)."""
|
||||
name = block_name.upper()
|
||||
for role, patterns in ANCHOR_BLOCKS.items():
|
||||
if any(fnmatch.fnmatchcase(name, p.upper()) for p in patterns):
|
||||
return role
|
||||
return ""
|
||||
|
||||
|
||||
def id_points_csv(elements) -> dict[str, tuple[str, float, float]]:
|
||||
"""TeileId -> (Rolle, x, y) der punktartigen CSV-Objekte."""
|
||||
return {e.teile_id: (e.kind, e.x, e.y) for e in elements
|
||||
if e.kind in ANCHOR_BLOCKS and e.position and e.teile_id}
|
||||
|
||||
|
||||
def id_points_dxf(msp) -> dict[str, tuple[str, float, float]]:
|
||||
"""ID-Attribut -> (Rolle, x, y) der Anker-Bloecke im Modellbereich.
|
||||
|
||||
Nur Bloecke einer Rolle aus ANCHOR_BLOCKS mit einer Ziffern-ID; eine ID,
|
||||
die zweimal vorkommt, ist nicht eindeutig und faellt weg.
|
||||
"""
|
||||
found: dict[str, tuple[str, float, float]] = {}
|
||||
doppelt: set[str] = set()
|
||||
for ref in msp.query("INSERT"):
|
||||
role = _role_of(ref.dxf.name)
|
||||
if not role:
|
||||
continue
|
||||
uid = next(((a.dxf.text or "").strip() for a in ref.attribs
|
||||
if a.dxf.tag.upper() == "ID"), "")
|
||||
if not uid.isdigit():
|
||||
continue
|
||||
if uid in found:
|
||||
doppelt.add(uid)
|
||||
found[uid] = (role, ref.dxf.insert.x, ref.dxf.insert.y)
|
||||
return {k: v for k, v in found.items() if k not in doppelt}
|
||||
|
||||
|
||||
def resolve_transform(
|
||||
args, elements, msp, cfg_dir: Path, csv_name: str, dxf_name: str,
|
||||
warnings: list[str],
|
||||
) -> Transform:
|
||||
"""Lagebezug bestimmen: Kommandozeile, Konfiguration oder Ermittlung."""
|
||||
"""Lagebezug bestimmen: Kommandozeile, TeileId, Konfiguration oder Ermittlung.
|
||||
|
||||
Die TeileId (derive_from_ids) geht vor dem abgelegten und dem ermittelten
|
||||
Bezug: sie ordnet eindeutig zu und gilt auch fuer eine verschobene Kopie
|
||||
der Zeichnung. Sie wird jedes Mal neu gerechnet, nicht abgelegt.
|
||||
"""
|
||||
csv_points = anchor_points_csv(elements)
|
||||
dxf_points = anchor_points_dxf(msp)
|
||||
|
||||
if not args.offset:
|
||||
by_ids = derive_from_ids(id_points_csv(elements), id_points_dxf(msp),
|
||||
csv_name, dxf_name)
|
||||
if by_ids is not None:
|
||||
verify_transform(by_ids, csv_points, dxf_points)
|
||||
return by_ids
|
||||
|
||||
if args.offset:
|
||||
try:
|
||||
dx, dy = (float(v) for v in args.offset.split(","))
|
||||
@@ -1241,7 +1291,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
args.text_height, csv_file.name)
|
||||
|
||||
out_file = results / (args.out or f"{csv_file.stem}_annotated.dxf")
|
||||
doc.saveas(str(out_file))
|
||||
try:
|
||||
doc.saveas(str(out_file))
|
||||
except PermissionError:
|
||||
print(f"FEHLER: {out_file} laesst sich nicht schreiben - ist die Zeichnung noch "
|
||||
f"in BricsCAD geoeffnet? Schliessen und neu starten, oder --out <name>.",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
reg_file = results / f"{csv_file.stem}_registration.json"
|
||||
reg_file.write_text(
|
||||
|
||||
Reference in New Issue
Block a user