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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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(
|
||||
|
||||
@@ -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)}
|
||||
Reference in New Issue
Block a user