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:
2026-10-23 14:59:16 +02:00
parent e5d1fce41f
commit 9ec1022fb1
6 changed files with 512 additions and 10 deletions
+7
View File
@@ -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.
+17 -8
View File
@@ -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`): `<name>_annotated.dxf`,
`<name>_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
+288
View File
@@ -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 ──────────► <csv>_tro.json TROs, flow, slots, jam areas
│ │
├── merged DXF ─┐ │
│ ├──────────[2] hw_import ──────► <project>_hw.json devices with BMK, position,
└── TIA import ─┘ │ address, index constant
▼
[3] merge_hw_layout ─► <csv>_anlage.json devices bound to TRO slots
│
┌──────────────────────────────────────┤
▼ ▼
[4] tro_annotate ─► <csv>_annotated.dxf [5] scl_skeleton ─► <csv>_FB_Main.scl
(viewer: TROs, flow, jam areas)
│ edit jam numbers in BricsCAD (JAM_EDIT)
└─[6] tro_annotate --read-jams ─► cfg/<csv>_jam_names.ini ──► back into [5]
```
| Step | Tool | Reads | Writes (in `results\` unless noted) |
|---|---|---|---|
| 1 | `tro_flow` | layout CSV | `<csv>_tro.json`, flow diagram, TRO report; `cfg\<csv>_tro_ids.ini` |
| 2 | `hw_import` | merged DXF, TIA import | `<project>_hw.json`, `<project>_hw.md` |
| 3 | `merge_hw_layout` | TRO-JSON, hardware JSON, rules | `<csv>_anlage.json`, `<csv>_anlage.md` |
| 4 | `tro_annotate` | CSV, merged DXF, plant JSON | `<csv>_annotated.dxf` |
| 5 | `scl_skeleton` | plant JSON | `<csv>_FB_Main.scl` |
| 6 | `tro_annotate --read-jams` | annotated DXF (edited) | `cfg\<csv>_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<NNNN>_<k>`, 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<NNNN>".stJam<NNNN>_<k>`:
- `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).
+53
View File
@@ -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
View File
@@ -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(
+89
View File
@@ -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)}