diff --git a/cfg/parameter.ini b/cfg/parameter.ini index 7d883d5..014ce3e 100644 --- a/cfg/parameter.ini +++ b/cfg/parameter.ini @@ -169,6 +169,13 @@ min_inliers = 5 ; Ab dieser Drehung (Grad) wird gewarnt. rotation_warn_deg = 0.05 +; Lagebezug ueber die TeileId (derive_from_ids): ein Paar CSV-Objekt/Block +; gleicher ID passt, wenn sein Versatz hoechstens so weit vom Median aller +; Paare der Rolle abweicht (Separator-Bloecke je nach Drehung +-60 mm) ... +id_spread_mm = 150.0 +; ... und so ein Anteil der Paare muss passen (0..1), sonst gilt die Rolle nicht. +id_min_share = 0.8 + [tro_annotate] ; Zeichnungsgroessen, wenn nicht per --marker-size/--text-height angegeben. diff --git a/doc/Python_Scripts.md b/doc/Python_Scripts.md index e0c23a7..cedee50 100644 --- a/doc/Python_Scripts.md +++ b/doc/Python_Scripts.md @@ -437,9 +437,10 @@ new shapes — a type with no prior annotated instance in the drawing must be run through this tool at least once first. CSV and DXF live in different coordinate systems; the offset between them is -handled by `dxf_registration.py` — derived once, verified on every run, and -cached per CSV/DXF filename pair in `%SKEL_CFG%\dxf_registration.json` unless -overridden. +handled by `dxf_registration.py`: by TeileId when the drawing's blocks carry +`ID` attributes (see "Registration by TeileId" below), otherwise derived once, +verified on every run, and cached per CSV/DXF filename pair in +`%SKEL_CFG%\dxf_registration.json` unless overridden. | Switch | Argument | Default | Meaning | |---|---|---|---| @@ -483,11 +484,19 @@ Layers always carry the prefix `TRO_`. Like `tro_flow.py`, it never reads **Output** (in `%SKEL_RESULTS%`, unless `--check`): `_annotated.dxf`, `_registration.json` (the coordinate transform used, with its proof). -**Merged drawing (Dematic).** The automatic registration only sees the -top-level 3D blocks and their insert points, which sit up to ~360 mm away -from the CSV positions; on `dematic_merge..dxf` it chose a wrong dy = +360 mm -(18/59 points). CSV and merged drawing share coordinates, so annotate with -`--offset 0,0`. +**Registration by TeileId (since 2026-10-23).** Drawings built with +dxfmakros carry the attribute `ID` on every layout block — the same number the +CSV lists as `TeileId`. Unless `--offset` is given, `tro_annotate` first +pairs CSV separators with the separator blocks of the same ID +(`dxf_registration.derive_from_ids`; scanners if there are fewer than +`min_inliers` separators) and takes the median offset. That is an exact +pairing, not a vote, and is recomputed on every run (not stored), so it also +fits a moved copy of the drawing. Only if no role has enough consistent pairs +(`id_spread_mm`, `id_min_share` in `cfg/parameter.ini [dxf_registration]`) +does the stored / voted registration below apply. Dematic: the merged drawing +gives exactly 0 / 0, the mechanical-only `dematic.dxf` +466.1 / −163 549.3 mm. +Before, the vote locked onto the scanner blocks (insert point 360 mm off their +CSV position) and was 360 mm wrong on both. **Exit codes:** `0` ok · `1` input/CLI error · `4` coordinate registration could not be proven (or its residual error exceeds 600 mm) — resolve with diff --git a/doc/Workflow_CSV_DXF_to_SCL.md b/doc/Workflow_CSV_DXF_to_SCL.md new file mode 100644 index 0000000..a936563 --- /dev/null +++ b/doc/Workflow_CSV_DXF_to_SCL.md @@ -0,0 +1,288 @@ +# Workflow: from layout CSV and merged DXF to the SCL skeleton + +This page walks through the whole chain for a plant whose mechanical and +electrical layouts are drawn in **one DXF** (the Dematic case), from the first +export to the `FB_Main` SCL file that is imported into TIA Portal. Example +names are those of the Dematic plant; replace `dematic_merge` / `dematic` with +your project's names. + +For the details of each tool and all its switches see +[`Python_Scripts.md`](Python_Scripts.md). + +## Overview + +``` + BricsCAD drawing (mechanics + E-planning in one DXF) + │ + ├── CSV export (ILS 2.0) ──[1] tro_flow ──────────► _tro.json TROs, flow, slots, jam areas + │ │ + ├── merged DXF ─┐ │ + │ ├──────────[2] hw_import ──────► _hw.json devices with BMK, position, + └── TIA import ─┘ │ address, index constant + ▼ + [3] merge_hw_layout ─► _anlage.json devices bound to TRO slots + │ + ┌──────────────────────────────────────┤ + ▼ ▼ + [4] tro_annotate ─► _annotated.dxf [5] scl_skeleton ─► _FB_Main.scl + (viewer: TROs, flow, jam areas) + │ edit jam numbers in BricsCAD (JAM_EDIT) + └─[6] tro_annotate --read-jams ─► cfg/_jam_names.ini ──► back into [5] +``` + +| Step | Tool | Reads | Writes (in `results\` unless noted) | +|---|---|---|---| +| 1 | `tro_flow` | layout CSV | `_tro.json`, flow diagram, TRO report; `cfg\_tro_ids.ini` | +| 2 | `hw_import` | merged DXF, TIA import | `_hw.json`, `_hw.md` | +| 3 | `merge_hw_layout` | TRO-JSON, hardware JSON, rules | `_anlage.json`, `_anlage.md` | +| 4 | `tro_annotate` | CSV, merged DXF, plant JSON | `_annotated.dxf` | +| 5 | `scl_skeleton` | plant JSON | `_FB_Main.scl` | +| 6 | `tro_annotate --read-jams` | annotated DXF (edited) | `cfg\_jam_names.ini` | + +## Before you start + +**Environment** (once per session, Windows): + +```bat +cd C:\develop\sps_skel\bin +activate_venv.bat +``` + +All commands below run from `bin\`. Names without a path are looked up in +`data\` (inputs) or `results\` (intermediate files). + +**Inputs in `data\`:** + +| File | What it is | Where it comes from | +|---|---|---| +| `dematic_merge.csv` | Layout export (ILS 2.0): parts, TeileIds, positions, neighbours, TrackIds | Export from the **same** drawing as the DXF (dxfmakros export) | +| `dematic_merge..dxf` | Merged drawing: 3D layout blocks **and** E-symbols (blocks with attribute `IO` = BMK) | BricsCAD, saved as DXF | +| `dematic_hw-2_TIA.xlsx` | TIA import: PLC tags (address) and constants (`cIn…` / `cOut…`) | E-planning | + +> The CSV must be exported from the same drawing state as the DXF. If the +> TeileIds do not match the IDs of the 3D blocks, the export is from another +> state and every assignment is wrong. + +**Project configuration in `cfg\`:** + +| File | Purpose | +|---|---| +| `e_planning_dematic.ini` | Rules for assigning E-symbols to layout parts (VERW texts, numbering `MB = BG`, distances) | +| `hw_overrides.ini` | Hand corrections per TRO slot role and per conveyor (Dematic: VF_4 / VF_6) | +| `dematic_merge_tro_ids.ini` | TRO IDs, created by step 1, kept stable across exports | +| `dematic_merge_jam_names.ini` | Jam-area numbers changed by hand (only exists after step 6) | +| `parameter.ini` | Tolerances and distances of all tools | + +## Step 0 – put the CSV export in place + +```bat +copy "C:\develop\dxfmakros\results\dematic_merge._export.csv" ..\data\dematic_merge.csv +``` + +The short name decides all following file names (`dematic_merge_*`). + +## Step 1 – CSV → TROs (`tro_flow`) + +```bat +tro_flow.bat --file dematic_merge.csv --json --doc --tosvg +``` + +Derives the material flow from the CSV and from it the **TROs** (Transfer +Route Objects): type (`1Sep`, `1Sep1Swi`, `Vario`, `PinStore_Auto` …), +separators, switches, scanners, predecessors and successors, the **slots** for +the hardware (separator sensor/stopper, switch valve, Vario drive …) and one +**jam area** per TRO entry. + +**Outputs:** `dematic_merge_tro.json` (input for steps 3–5), +`dematic_merge_tro_doc.md` (TRO list), `dematic_merge_tro_flow.svg` (flow +diagram). TRO IDs are stored in `cfg\dematic_merge_tro_ids.ini` and found +again by separator position on the next export. + +**Check:** the TRO list and the notes in `dematic_merge_tro_doc.md`; the flow +diagram. Dematic: 25 TROs, 34 connections, no errors. + +## Step 2 – merged DXF + TIA → hardware JSON (`hw_import`) + +```bat +hw_import.bat --tia dematic_hw-2_TIA.xlsx --dxf "dematic_merge..dxf" --project dematic --conveyor-from-motor +``` + +Reads every E-symbol of the drawing (BMK, VERW, BEZEICHNUNG, station, +position) and the TIA import (address, index constant `cInBG3151` / +`cOutMB3151`), joins them per BMK and pairs each motor with its motor +protection (`MA0055` ↔ `FC0055`). + +- `--dxf` instead of a positions file: the positions are already layout + coordinates. +- `--conveyor-from-motor`: Dematic texts name the motor, not a `K…` number, + so the conveyor number is the motor number (`MA0055` → conveyor `0055`). + +**Output:** `dematic_hw.json`, report `dematic_hw.md`. + +**Check:** the cross-check section of the report. Dematic: 278 devices from +the drawing, 267 also in TIA; only the network devices (scanners `BX…`, +encoder `GF2200`, RFID `BC2050`) are not in TIA, which is expected. + +## Step 3 – combine layout and hardware (`merge_hw_layout`) + +```bat +merge_hw_layout.bat --tro dematic_merge_tro.json --hw dematic_hw.json --rules e_planning_dematic.ini +``` + +Assigns the E-devices to the TRO slots, keyed by **TRO-ID + slot + role**: + +1. no coordinate registration (merged drawing, same coordinates); +2. rules from `e_planning_dematic.ini`: separator sensor by distance, stopper + by number (`MB = BG`), switch valve by the number of its jam sensor or by + distance, always one-to-one; +3. conveyors: every device whose text names a motor votes for the nearest + layout conveyor; this gives motor and motor protection per Kreisel / Vario; +4. hand corrections from `hw_overrides.ini` last. + +Each binding gets a status: `zugeordnet` (assigned), `offen` (open), +`widerspruch` (contradiction, e.g. a BMK not in TIA or used twice). Only +assigned names reach the SCL. + +**Output:** `dematic_merge_anlage.json` (the plant JSON), report +`dematic_merge_anlage.md`. + +**Check:** the report — roles per slot type, conveyor table, open roles, +devices without assignment. Dematic: 123 roles assigned, 33 open, +0 contradictions; 11 conveyors bound. + +**Corrections:** wrong or missing assignments go into `cfg\hw_overrides.ini`, +then re-run steps 3 and 5: + +```ini +[dematic_merge.csv.bindings] +TRO04.separator1.sensor = BG0080 +TRO04.drive.motor = MA0043 + +[dematic_merge.csv.conveyors] +K0045 = 0008 +``` + +## Step 4 – annotated drawing for checking (`tro_annotate`) + +```bat +tro_annotate.bat --file dematic_merge.csv --dxf "dematic_merge..dxf" --flow --legend --anlage dematic_merge_anlage.json +``` + +The same works on the mechanical-only drawing as background — use `--out` +so it does not overwrite the other one: + +```bat +tro_annotate.bat --file dematic_merge.csv --dxf dematic.dxf --flow --legend --anlage dematic_merge_anlage.json --out dematic_annotated.dxf +``` + +Writes a **copy** of the drawing (`results\dematic_merge_annotated.dxf`) with +layers `TRO_*`: + +- one marker per TRO (shape and colour per type, attributes `ID`, `TYPE`); +- flow arrows between the TROs (`--flow`) and a legend (`--legend`); +- one **jam area** per TRO entry (`--anlage`): a small rectangle with the name + `Jam_`, in front of its separator, on layer `TRO_JAM`. + +The position of the drawing relative to the CSV is found automatically: the +layout blocks carry the attribute `ID` = CSV `TeileId`, so the tool pairs +them and measures the offset (console line `Lagebezug … [ids, …]`). This works +for the merged drawing (offset 0) and for a moved copy such as the +mechanical-only drawing alike; no `--offset` needed. + +The drawing is a **viewer**. It is not read back except for the jam numbers +(step 6). + +## Step 5 – SCL skeleton (`scl_skeleton`) + +```bat +scl_skeleton.bat --json dematic_merge_anlage.json --templates --strict +``` + +Writes `results\dematic_merge_FB_Main.scl`: one `REGION` per TRO in material +flow order, built from the reference templates (`lib\scl_templates\`), with: + +- separator sensors and stoppers, switch valves, Vario motors and motor + protections from the plant JSON (`"DB_Inputs".Sensors["cInBG3151"]`, + `"MB3151"`); +- Kreisel release `#fbCarousel0055.xOutConveyorFullSpeed`; +- jam areas `"DB_JamArea0054".stJam0054_3` (see below); +- destination lists per switch exit. + +Everything that is not known stays a visible placeholder marked +`TODO(E-Planung)`. `--strict` stops if any inserted name is not in the TIA +import. + +**Check:** the console summary (names inserted, unknown names, open +placeholders) and the file itself — search for `TODO(E-Planung)`. +Dematic: 200 names inserted, 0 unknown, 494 placeholders left. + +### Jam-area names + +`"DB_JamArea".stJam_`: + +- `NNNN` = the conveyor the jam area sits on — the number of its Kreisel or + Vario motor (Kreisel3 with motor `MA0054` → `0054`); +- `k` = 1, 2, 3 … along the material flow on that conveyor, starting where + the material first comes onto it; +- a 1Sep behind a Vario on the same line belongs to that Vario's conveyor; + a PinStore block has one jam area for the whole block. + +## Step 6 – change jam numbers in BricsCAD (optional) + +1. Open `results\dematic_merge_annotated.dxf` in BricsCAD (SSG_LIB loaded, + `Jam_Edit.lsp` is part of `ssg_load.lsp`). +2. **Double-click a jam area** → dialog `JAM_EDIT`: conveyor (`NNNN`) and + running number (`k`); "Abgeleitet uebernehmen" goes back to the derived + value; a warning appears if the name already exists. +3. **Save as DXF** under the same name. +4. Take the changes over: + + ```bat + tro_annotate.bat --file dematic_merge.csv --read-jams + ``` + + Every jam area whose number differs from the derived one is written to + `cfg\dematic_merge_jam_names.ini` (`TRO20.1 = 0054_9`); areas set back to + the derived value are removed from it. +5. Re-run step 5 (SCL) and, if wanted, step 4 (the drawing then shows the + corrected numbers). + +The key is TRO-ID + entry, so the corrections survive a new layout export. + +## When to re-run what + +| What changed | Re-run | +|---|---| +| Layout (new CSV export) | 0 → 1 → 2 → 3 → 4 → 5 | +| E-planning or TIA import | 2 → 3 → 4 → 5 | +| `hw_overrides.ini` or `e_planning_dematic.ini` | 3 → 4 → 5 | +| Jam numbers in the drawing | 6 → 5 (→ 4) | +| Only the SCL templates | 5 | + +## What is still open in the SCL + +The 494 remaining `TODO(E-Planung)` placeholders (Dematic, 2026-10-23), by +cause: + +| Group | Approx. | Why open | Next step | +|---|---|---|---| +| Program numbering (`cSep`, `cMainTro`, `cSwi`, storage constants) | 140 | Numbering convention of the PLC program, not in any input | Agree a convention with the PLC team | +| Timings marked "pruefen" | 117 | Reference values filled, only marked for review | Commissioning | +| Cabinet / safety / automatic signals | 57 | Plant-wide signals | One decision for all TROs | +| Line and exit jam sensors, Vario sensors | 75 | No assignment rule yet; the devices are in the drawing | Extend `e_planning_dematic.ini` | +| Scanners | 39 | Network devices, signal names come from the TIA hardware configuration | Naming scheme from the PLC side | +| Conveyor signals, unclear meaning | ~45 | Infeed/outfeed conveyors, `xIsBorner`, switch split | Delete where not needed / ask the PLC team | +| Other | ~20 | Acknowledge buttons `SF…` (cabinet, not drawn), PinStore reference sensor, `FC_Direction` carrier, one switch valve | Rules / hand corrections | + +## Known limits + +- **VF_4 / VF_6 (Dematic):** mechanical parts are still missing there; the + assignments are set by hand in `hw_overrides.ini` from the drawing texts. + Seven E-separators without a mechanical part are ignored until the layout + is complete. +- **Registration:** automatic by TeileId for drawings built with dxfmakros. + A drawing without `ID` attributes falls back to the older point vote, which + can be off by a few hundred mm — check the `Lagebezug` line and use + `--offset DX,DY` if needed. +- **The drawing is a viewer:** TRO IDs and types changed in BricsCAD + (`TRO_EDIT`) are not read back; only jam numbers are (step 6). diff --git a/lib/dxf_registration.py b/lib/dxf_registration.py index 11a4112..5c1f846 100644 --- a/lib/dxf_registration.py +++ b/lib/dxf_registration.py @@ -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, diff --git a/lib/tro_annotate.py b/lib/tro_annotate.py index c07252c..86beffa 100644 --- a/lib/tro_annotate.py +++ b/lib/tro_annotate.py @@ -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 .", + file=sys.stderr) + return 1 reg_file = results / f"{csv_file.stem}_registration.json" reg_file.write_text( diff --git a/tests/test_registration_ids.py b/tests/test_registration_ids.py new file mode 100644 index 0000000..fe42caa --- /dev/null +++ b/tests/test_registration_ids.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +"""Tests fuer den Lagebezug ueber die TeileId (dxf_registration.derive_from_ids). + +Nachbau der Dematic-Befunde: dieselbe Anlage einmal als gemeinsame Zeichnung +(Versatz 0) und einmal als verschobene Mechanik-Zeichnung (~163 m in y). Die +Separator-Bloecke sitzen je nach Drehung +-60 mm neben der CSV-Position, die +Scanner-Bloecke 360 mm - die automatische Abstimmung rastete dort auf den +Scannern ein und lag 360 mm daneben. +""" + +import ezdxf +import pytest + +import tro_annotate +from dxf_registration import MIN_INLIERS, derive_from_ids + +SHIFT = (-466.1, 163549.3) # DXF = CSV + SHIFT (Dematic Mechanik) + + +def _csv(n=11): + seps = {f"{38 + i:04d}": ("Separator", 3000.0 + i * 800.0, 12120.0) for i in range(n)} + scans = {f"{60 + i:04d}": ("Scanner", 3000.0 + i * 900.0, 25000.0) for i in range(7)} + return {**seps, **scans} + + +def _dxf(csv_points, shift=SHIFT): + out = {} + for i, (tid, (rolle, x, y)) in enumerate(csv_points.items()): + if rolle == "Separator": + bias = (-60.0 if i % 3 else 60.0, 0.0) + else: + bias = (0.0, -360.0) + out[tid] = (rolle, x + shift[0] + bias[0], y + shift[1] + bias[1]) + return out + + +def test_verschobene_zeichnung_ueber_die_separatoren(): + csv_points = _csv() + t = derive_from_ids(csv_points, _dxf(csv_points), "x.csv", "mech.dxf") + assert t is not None and t.source == "ids" + # DXF -> CSV: Gegenrichtung des Verschubs, Separator-Bias hoechstens 60 mm + assert abs(t.dx - (-SHIFT[0])) <= 60.0 + 1e-6 + assert t.dy == pytest.approx(-SHIFT[1]) + assert "Separator" in t.anchor + + +def test_gemeinsame_zeichnung_versatz_null(): + csv_points = _csv() + t = derive_from_ids(csv_points, _dxf(csv_points, shift=(0.0, 0.0))) + assert abs(t.dx) <= 60.0 + 1e-6 and t.dy == pytest.approx(0.0) + + +def test_ohne_genug_separatoren_die_scanner(): + csv_points = _csv(n=MIN_INLIERS - 1) + t = derive_from_ids(csv_points, _dxf(csv_points)) + assert t is not None and "Scanner" in t.anchor + assert t.dy == pytest.approx(-SHIFT[1] + 360.0) # Scanner-Bias bleibt drin + + +def test_ohne_ids_kein_ergebnis(): + assert derive_from_ids(_csv(), {}) is None + + +def test_uneinheitlicher_versatz_wird_nicht_genommen(): + csv_points = _csv() + dxf = _dxf(csv_points) + # die Haelfte der Separatoren weit daneben (falsche IDs) -> keine Rolle passt + for tid in list(dxf)[:6]: + rolle, x, y = dxf[tid] + dxf[tid] = (rolle, x + 5000.0, y) + for tid in [k for k, v in dxf.items() if v[0] == "Scanner"]: + del dxf[tid] + assert derive_from_ids(csv_points, dxf) is None + + +def test_ids_aus_der_zeichnung(): + doc = ezdxf.new() + blk = doc.blocks.new("Separator_SP_3D") + blk.add_attdef("ID", (0, 0)) + andere = doc.blocks.new("KREISEL_1_STANDARD_3619") + andere.add_attdef("ID", (0, 0)) + msp = doc.modelspace() + msp.add_blockref("Separator_SP_3D", (10, 20)).add_auto_attribs({"ID": "0038"}) + msp.add_blockref("Separator_SP_3D", (30, 40)).add_auto_attribs({"ID": "0039"}) + msp.add_blockref("Separator_SP_3D", (50, 60)).add_auto_attribs({"ID": "0039"}) + msp.add_blockref("Separator_SP_3D", (70, 80)).add_auto_attribs({"ID": ""}) + msp.add_blockref("KREISEL_1_STANDARD_3619", (0, 0)).add_auto_attribs({"ID": "0001"}) + # nur eindeutige Ziffern-IDs von Anker-Bloecken + assert tro_annotate.id_points_dxf(msp) == {"0038": ("Separator", 10.0, 20.0)}