tro_annotate --read-tros: TRO-Handkorrekturen aus BricsCAD, Staubereiche vor dem TRO
- Marker tragen KEY (ID der ID-Datei) und SLOTS (Bauteil je Slot), die Zeichnung die Bauteiltabelle SPS_SKEL_PARTS (XRECORD) fuer "Im Plan waehlen" im Dialog TRO_EDIT (dxfmakros) - --read-tros schreibt geaenderte, neue und geloeschte Marker als Abschnitte edit/edit_add nach cfg/tro_overrides.ini; tro_flow.apply_edits wendet nur diese Arten an (Bauteile mit Lage, exportfest) - --emit-lisp schreibt dazu *TRO-SLOTS* und *TRO-SLOT-KINDS* - model: Lagerlinien zaehlen je TRO (Katalog nur Vorgabe), Mubea-Speicher jetzt 20 Linien (Golden-Datei) - Staubereich als stehendes graues Rechteck direkt vor seinem TRO-Marker (jam_abstand statt jam_versatz); Marker 150 mm, Text 120 mm Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -207,7 +207,12 @@ byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended cha
|
|||||||
call as `tro_flow.py`) into a copy of the DXF for a visual check in BricsCAD. With
|
call as `tro_flow.py`) into a copy of the DXF for a visual check in BricsCAD. With
|
||||||
`--anlage <csv>_anlage.json` also the jam areas (block `JAM_AREA`, name `Jam<NNNN>_<k>`,
|
`--anlage <csv>_anlage.json` also the jam areas (block `JAM_AREA`, name `Jam<NNNN>_<k>`,
|
||||||
double-click → dialog `JAM_EDIT` in dxfmakros); `--read-jams` takes numbers edited there
|
double-click → dialog `JAM_EDIT` in dxfmakros); `--read-jams` takes numbers edited there
|
||||||
into `cfg/<csv>_jam_names.ini`, which `scl_skeleton` applies.
|
into `cfg/<csv>_jam_names.ini`, which `scl_skeleton` applies. TRO markers carry `KEY` and
|
||||||
|
`SLOTS` (part per slot); the dxfmakros dialog `TRO_EDIT`/`TRO_INSERT` (one dialog for all
|
||||||
|
types, rows from `*TRO-SLOTS*`, pick-in-drawing from the XRECORD `SPS_SKEL_PARTS`) edits
|
||||||
|
them, and `--read-tros` writes the differences as `edit`/`edit_add` sections into
|
||||||
|
`cfg/tro_overrides.ini` — the only kinds of that file `tro_flow` applies
|
||||||
|
(`tro_flow.apply_edits`, keyed on the stable ID-file IDs).
|
||||||
4. **`lib/tro_extract.py`** — **parked**: reads the annotated DXF back into the old
|
4. **`lib/tro_extract.py`** — **parked**: reads the annotated DXF back into the old
|
||||||
`sps_skel-tro-extract-v1` JSON, which `scl_skeleton.py` no longer accepts. Not deleted, not
|
`sps_skel-tro-extract-v1` JSON, which `scl_skeleton.py` no longer accepts. Not deleted, not
|
||||||
extended; if drawing edits (`TRO_INSERT`/`TRO_EDIT`) are ever needed again, it should
|
extended; if drawing edits (`TRO_INSERT`/`TRO_EDIT`) are ever needed again, it should
|
||||||
|
|||||||
+25
-1
@@ -1,4 +1,28 @@
|
|||||||
;; Automatisch erzeugt von lib/tro_annotate.py --emit-lisp
|
;; Automatisch erzeugt von lib/tro_annotate.py --emit-lisp
|
||||||
;; Quelle: TRO_CATALOG in lib/tro_catalog.py - nicht manuell aendern.
|
;; Quelle: TRO_CATALOG in lib/tro_catalog.py, Slots aus lib/model.py
|
||||||
|
;; (slot_names, SLOT_PART_KINDS) - nicht manuell aendern.
|
||||||
(setq *TRO-TYPES* (list "1Sep" "1Sep1Swi" "1Sep2Swi" "1Sep_SSCC" "Vario" "PinStore_Auto" "Vario_workStation" "EmptyCarrBuffer" "LoadingBoom" "2Sep1Swi" "2Sep2Swi" "3Sep2Swi"))
|
(setq *TRO-TYPES* (list "1Sep" "1Sep1Swi" "1Sep2Swi" "1Sep_SSCC" "Vario" "PinStore_Auto" "Vario_workStation" "EmptyCarrBuffer" "LoadingBoom" "2Sep1Swi" "2Sep2Swi" "3Sep2Swi"))
|
||||||
|
;; (Typ Lagerlinien-Vorgabe Slot ...) - Lagerlinien heissen line1..lineN
|
||||||
|
(setq *TRO-SLOTS* '(
|
||||||
|
("1Sep" 0 "separator1")
|
||||||
|
("1Sep1Swi" 0 "separator1" "switch1")
|
||||||
|
("1Sep2Swi" 0 "separator1" "switch1" "switch2" "scanner1")
|
||||||
|
("1Sep_SSCC" 0 "separator1" "scanner1")
|
||||||
|
("Vario" 0 "separator1" "drive")
|
||||||
|
("PinStore_Auto" 19 "separator1" "scanner1")
|
||||||
|
("Vario_workStation" 0 "separator1" "scanner1" "drive")
|
||||||
|
("EmptyCarrBuffer" 5)
|
||||||
|
("LoadingBoom" 0)
|
||||||
|
("2Sep1Swi" 0 "separator1" "separator2" "switch1" "scanner1" "scanner2")
|
||||||
|
("2Sep2Swi" 0 "separator1" "separator2" "switch1" "switch2" "scanner1" "scanner2")
|
||||||
|
("3Sep2Swi" 0 "separator1" "separator2" "separator3" "switch1" "switch2" "scanner1" "scanner2" "scanner3")
|
||||||
|
))
|
||||||
|
;; (Slot-Art Bauteilart ...) - Pruefung beim Waehlen im Plan
|
||||||
|
(setq *TRO-SLOT-KINDS* '(
|
||||||
|
("separator" "Separator")
|
||||||
|
("line" "Separator")
|
||||||
|
("switch" "Ausschleus" "Weiche")
|
||||||
|
("scanner" "Scanner")
|
||||||
|
("drive" "Strecke" "Gefaellestrecke" "Kreisel")
|
||||||
|
))
|
||||||
(princ)
|
(princ)
|
||||||
|
|||||||
+7
-6
@@ -179,16 +179,17 @@ id_min_share = 0.8
|
|||||||
|
|
||||||
[tro_annotate]
|
[tro_annotate]
|
||||||
; Zeichnungsgroessen, wenn nicht per --marker-size/--text-height angegeben.
|
; Zeichnungsgroessen, wenn nicht per --marker-size/--text-height angegeben.
|
||||||
default_marker = 300.0
|
; So klein wie lesbar: ID = 1,35 x Text (~160 mm), Typ ~120 mm.
|
||||||
default_text = 220.0
|
default_marker = 150.0
|
||||||
|
default_text = 120.0
|
||||||
|
|
||||||
; Restfehler des Lagebezugs, ab dem er abgelehnt wird.
|
; Restfehler des Lagebezugs, ab dem er abgelehnt wird.
|
||||||
default_max_residual = 600.0
|
default_max_residual = 600.0
|
||||||
|
|
||||||
; Staubereich (--anlage): das Rechteck sitzt so weit vor seinem Separator,
|
; Staubereich (--anlage): stehendes graues Rechteck direkt vor seinem TRO,
|
||||||
; Richtung Vorgaenger-TRO (gegen den Fluss) - er ist die Strecke, auf der
|
; auf der Seite des Vorgaenger-TRO (gegen den Fluss) - dort warten die Traeger.
|
||||||
; die Traeger vor dem Separator warten.
|
; Luft zwischen Markerrand und Rechteck (mm).
|
||||||
jam_versatz = 700.0
|
jam_abstand = 60.0
|
||||||
|
|
||||||
|
|
||||||
[merge_hw_layout]
|
[merge_hw_layout]
|
||||||
|
|||||||
+109
-31
@@ -89,9 +89,10 @@ Supporting libraries (no CLI of their own, imported by the tools above):
|
|||||||
reads it, finds each TRO again by the position of its separators, assigns
|
reads it, finds each TRO again by the position of its separators, assigns
|
||||||
new IDs, writes/extends the file. Used by `tro_flow.py` (read and write) and
|
new IDs, writes/extends the file. Used by `tro_flow.py` (read and write) and
|
||||||
`tro_annotate.py` (read only). See §4, "TRO-IDs".
|
`tro_annotate.py` (read only). See §4, "TRO-IDs".
|
||||||
- **`lib/tro_overrides.py`** — reads the hand-maintained
|
- **`lib/tro_overrides.py`** — reads `%SKEL_CFG%\tro_overrides.ini`. The
|
||||||
`%SKEL_CFG%\tro_overrides.ini`; only `material_flow.py --connect` still uses
|
hand-maintained kinds are only used by `material_flow.py --connect`; the
|
||||||
it. See §6a.
|
BricsCAD corrections (`edit`/`edit_add`, written by `tro_annotate.py
|
||||||
|
--read-tros`) are applied by `tro_flow.py` and `tro_annotate.py`. See §6a.
|
||||||
|
|
||||||
Separate, documentation-only tool (not part of the generator pipeline):
|
Separate, documentation-only tool (not part of the generator pipeline):
|
||||||
|
|
||||||
@@ -367,9 +368,14 @@ colors all come from `lib/tro_catalog.py` — nothing is hard-coded here.
|
|||||||
| `--renumber` | — | off | Number all TROs afresh along the material flow and rewrite the `[tro]` section of the ID file (early project phase). Renamed IDs are lost; other sections of the file are kept. |
|
| `--renumber` | — | off | Number all TROs afresh along the material flow and rewrite the `[tro]` section of the ID file (early project phase). Renamed IDs are lost; other sections of the file are kept. |
|
||||||
| `--id-tolerance` | `MM` (float) | `100.0` | Maximum distance between an ID-file entry and a separator of a TRO for the TRO to count as found again. |
|
| `--id-tolerance` | `MM` (float) | `100.0` | Maximum distance between an ID-file entry and a separator of a TRO for the TRO to count as found again. |
|
||||||
|
|
||||||
TROs, types and connections always come from the layout alone;
|
TROs, types and connections come from the layout. Errors in the derivation
|
||||||
`cfg/tro_overrides.ini` is not read. Errors in the derivation are fixed in
|
are fixed in `lib/tro_flow.py`, not in a correction file. The one exception
|
||||||
`lib/tro_flow.py`, not in a correction file.
|
(since 2026-10-26) are the corrections the SPS programmers make in BricsCAD
|
||||||
|
(§5, "Editing TROs in BricsCAD"): `tro_flow` reads the `edit`/`edit_add`
|
||||||
|
sections of `cfg/tro_overrides.ini` and applies them right after the
|
||||||
|
derivation (`apply_edits`, before positions and links), so the flow diagram,
|
||||||
|
the TRO-JSON and the SCL show the corrected TROs. All other kinds in that file
|
||||||
|
are still ignored here.
|
||||||
|
|
||||||
**TRO-IDs** (`lib/tro_ids.py`). The IDs belong to the SPS programmers and must
|
**TRO-IDs** (`lib/tro_ids.py`). The IDs belong to the SPS programmers and must
|
||||||
survive a new layout export; the layout `TeileId` changes between exports, the
|
survive a new layout export; the layout `TeileId` changes between exports, the
|
||||||
@@ -419,7 +425,12 @@ with:
|
|||||||
triangle, square, diamond, …; see `TroSymbol` in `tro_catalog.py`) so types
|
triangle, square, diamond, …; see `TroSymbol` in `tro_catalog.py`) so types
|
||||||
are distinguishable even without color;
|
are distinguishable even without color;
|
||||||
- visible attributes `ID`, `TYPE` (and `FB_BLOCK` with `--fb`); hidden
|
- visible attributes `ID`, `TYPE` (and `FB_BLOCK` with `--fb`); hidden
|
||||||
attributes `ITEMS`, `CONFIDENCE`, `SEPARATORS` for downstream tooling;
|
attributes `UID`, `KEY` (the TRO's key in `tro_overrides.ini`: its ID in
|
||||||
|
the ID file), `ITEMS`, `CONFIDENCE` and `SLOTS` (the part per slot,
|
||||||
|
`separator1=0010;switch1=0061;lines=0066,0069`);
|
||||||
|
- the parts of the export that fit a slot (TeileId, kind, drawing position,
|
||||||
|
half bounding box) as XRECORD `SPS_SKEL_PARTS` in the named object
|
||||||
|
dictionary, for the dialog's "pick in drawing" button;
|
||||||
- optional directional flow arrows between TROs (`--flow`) and a legend
|
- optional directional flow arrows between TROs (`--flow`) and a legend
|
||||||
(`--legend`).
|
(`--legend`).
|
||||||
|
|
||||||
@@ -428,13 +439,39 @@ own `TRO_`-prefixed layers and carries XDATA under the app id
|
|||||||
`SPS_SKEL_TRO`; a re-run removes exactly those tagged objects (not by layer
|
`SPS_SKEL_TRO`; a re-run removes exactly those tagged objects (not by layer
|
||||||
name) and rebuilds them, so hand-drawn content on the same layers survives.
|
name) and rebuilds them, so hand-drawn content on the same layers survives.
|
||||||
|
|
||||||
Once at least one marker of a given type has been annotated, the `dxfmakros`
|
**Editing TROs in BricsCAD (since 2026-10-26).** The `dxfmakros` commands
|
||||||
BricsCAD menu (`SSG_LIB > TRO`) can insert further markers of that type by
|
`TRO_EDIT` (double-click on a marker) and `TRO_INSERT` (`SSG_LIB > TRO`)
|
||||||
hand — `TRO_INSERT` reuses the `TRO_SYM_<Typ>` block definition this tool
|
share **one** dialog for all types (`dcl/tro_edit.dcl`): ID, type, and a list
|
||||||
created and only asks for a point, an id and (optionally) a different type;
|
with one row per slot of the type (`separator<n>`, `switch<n>`, `scanner<n>`,
|
||||||
`TRO_EDIT` changes `ID`/`TYPE` on an existing marker. Neither command draws
|
`drive`, storage lines `line1..N`) and its part (TeileId). The rows come from
|
||||||
new shapes — a type with no prior annotated instance in the drawing must be
|
the type (`*TRO-SLOTS*` in `tro_types.lsp`, written by `--emit-lisp` from
|
||||||
run through this tool at least once first.
|
`model.slot_names`), so there is no mask per type; on a type change, slots
|
||||||
|
of the same name keep their part. Below the list: edit the selected slot's
|
||||||
|
TeileId, **"Im Plan waehlen <"** (closes the dialog, takes a point, sets the
|
||||||
|
nearest part of a kind that fits the slot — `model.SLOT_PART_KINDS` — from
|
||||||
|
the `SPS_SKEL_PARTS` table) and +/- for storage lines (the catalog count is
|
||||||
|
only the default). Empty separators, parts used twice or in another marker
|
||||||
|
show as a warning; saving is still allowed. `TRO_INSERT` reuses a
|
||||||
|
`TRO_SYM_<Typ>` block definition this tool created and leaves `KEY` empty
|
||||||
|
(= new TRO); a type with no annotated instance in the drawing must be run
|
||||||
|
through this tool once first. Deleting a marker deletes the TRO.
|
||||||
|
|
||||||
|
Back into the pipeline: save the drawing as DXF and run `--read-tros`. It
|
||||||
|
compares every marker with the derivation **without** corrections: a marker
|
||||||
|
whose `KEY` is a derived TRO becomes `[<csv>.edit.<KEY>]` if type, ID (→
|
||||||
|
`name =`) or parts differ; a marker without such a key becomes
|
||||||
|
`[<csv>.edit_add.<ID>]`; a derived TRO with a position but no marker becomes
|
||||||
|
`delete = true`. Each part is written as `TeileId @ x y` (CSV position) — the
|
||||||
|
TeileId changes between exports, the position does not, so `apply_edits`
|
||||||
|
falls back to the nearest part of the same kind at that spot. The sections
|
||||||
|
replace all earlier `edit`/`edit_add` sections of the CSV in
|
||||||
|
`tro_overrides.ini` (a `reason =` added by hand is kept); the rest of the file
|
||||||
|
stays line for line. The next `tro_flow` / `tro_annotate` run applies them
|
||||||
|
(§4); a re-read of the new drawing gives the same sections. `--check` only
|
||||||
|
reports. If the drawing has no marker of the derived TROs (wrong file, or
|
||||||
|
annotated before the `KEY`/`SLOTS` attributes), nothing is written —
|
||||||
|
re-annotating an old drawing adds the missing attributes to the existing
|
||||||
|
block definitions.
|
||||||
|
|
||||||
CSV and DXF live in different coordinate systems; the offset between them is
|
CSV and DXF live in different coordinate systems; the offset between them is
|
||||||
handled by `dxf_registration.py`: by TeileId when the drawing's blocks carry
|
handled by `dxf_registration.py`: by TeileId when the drawing's blocks carry
|
||||||
@@ -450,22 +487,26 @@ verified on every run, and cached per CSV/DXF filename pair in
|
|||||||
| `--flow` | — | off | Draw directional flow arrows between the TROs. |
|
| `--flow` | — | off | Draw directional flow arrows between the TROs. |
|
||||||
| `--fb` | — | off | Show the Siemens FB block name as a visible attribute on the marker. |
|
| `--fb` | — | off | Show the Siemens FB block name as a visible attribute on the marker. |
|
||||||
| `--legend` | — | off | Draw a legend of the color groups. |
|
| `--legend` | — | off | Draw a legend of the color groups. |
|
||||||
| `--marker-size` | `MM` (float) | `300.0` | Marker radius in mm. |
|
| `--marker-size` | `MM` (float) | `150.0` | Marker radius in mm (`default_marker` in `cfg/parameter.ini`). |
|
||||||
| `--text-height` | `MM` (float) | `220.0` | Text height in mm. |
|
| `--text-height` | `MM` (float) | `120.0` | Text height in mm (`default_text`; the ID is 1.35×). |
|
||||||
| `--ids` | `DATEI` | `<csv-stem>_tro_ids.ini` | ID file as for `tro_flow.py`, **read only**: the markers carry the same IDs as the TRO-JSON. Without the file the TROs are numbered along the material flow, as `tro_flow.py` would. |
|
| `--ids` | `DATEI` | `<csv-stem>_tro_ids.ini` | ID file as for `tro_flow.py`, **read only**: the markers carry the same IDs as the TRO-JSON. Without the file the TROs are numbered along the material flow, as `tro_flow.py` would. |
|
||||||
| `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand instead of deriving/loading it. For a drawing built with dxfmakros (CSV exported from the same drawing) this is `0,0`. |
|
| `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand instead of deriving/loading it. For a drawing built with dxfmakros (CSV exported from the same drawing) this is `0,0`. |
|
||||||
| `--re-register` | — | off | Re-derive the coordinate offset even if a verified one is already cached in `%SKEL_CFG%`. |
|
| `--re-register` | — | off | Re-derive the coordinate offset even if a verified one is already cached in `%SKEL_CFG%`. |
|
||||||
| `--check` | — | off | Dry run: only analyze and report, write no output file. |
|
| `--check` | — | off | Dry run: only analyze and report, write no output file. |
|
||||||
| `--emit-lisp` | — | off | Regenerate `cad/tro_types.lsp` (the type list for the `TRO_INSERT`/`TRO_EDIT` dialogs) from `tro_catalog.py` and exit — no `--dxf` needed. Written under `%DXFM_LISP%` if set (next to the `dxfmakros` SSG_LIB modules that provide the dialogs), otherwise under `<SPS_SKEL>/cad`. |
|
| `--emit-lisp` | — | off | Regenerate `cad/tro_types.lsp` for the `TRO_INSERT`/`TRO_EDIT` dialog — type list (`*TRO-TYPES*`), slots per type with the storage-line default (`*TRO-SLOTS*`), part kinds per slot (`*TRO-SLOT-KINDS*`) — from `tro_catalog.py`/`model.py` and exit — no `--dxf` needed. Written under `%DXFM_LISP%` if set (next to the `dxfmakros` SSG_LIB modules that provide the dialogs), otherwise under `<SPS_SKEL>/cad`. |
|
||||||
| `--anlage` | `NAME` | — | Plant JSON (`<csv>_anlage.json` from `merge_hw_layout`, in `%SKEL_RESULTS%` or a path): also draw the **jam areas** (see below). Must belong to the same CSV. |
|
| `--anlage` | `NAME` | — | Plant JSON (`<csv>_anlage.json` from `merge_hw_layout`, in `%SKEL_RESULTS%` or a path): also draw the **jam areas** (see below). Must belong to the same CSV. |
|
||||||
| `--read-jams` | `[DXF]` | `<csv>_annotated.dxf` | Read the jam-area numbers edited in BricsCAD back from the annotated drawing (saved as DXF) into `%SKEL_CFG%\<csv>_jam_names.ini`, then exit — no `--dxf` needed. |
|
| `--read-jams` | `[DXF]` | `<csv>_annotated.dxf` | Read the jam-area numbers edited in BricsCAD back from the annotated drawing (saved as DXF) into `%SKEL_CFG%\<csv>_jam_names.ini`, then exit — no `--dxf` needed. |
|
||||||
|
| `--read-tros` | `[DXF]` | `<csv>_annotated.dxf` | Read the TRO markers edited in BricsCAD (`TRO_EDIT`/`TRO_INSERT`, deleted markers) back from the annotated drawing (saved as DXF) into the `edit`/`edit_add` sections of `%SKEL_CFG%\tro_overrides.ini`, then exit — no `--dxf` needed. With `--check` only reported. |
|
||||||
|
|
||||||
**Jam areas (`--anlage`).** One block `JAM_AREA` per jam area of the plant
|
**Jam areas (`--anlage`).** One block `JAM_AREA` per jam area of the plant
|
||||||
JSON on layer `TRO_JAM`: a small rectangle with the name inside
|
JSON on layer `TRO_JAM`: an upright grey rectangle with the name written
|
||||||
(`Jam0054_3`, the same name the SCL uses — `scl_context.jam_numbers` plus the
|
vertically inside (`Jam0054_3`, the same name the SCL uses —
|
||||||
corrections file). It sits `jam_versatz` mm (`cfg/parameter.ini
|
`scl_context.jam_numbers` plus the corrections file). It sits directly in
|
||||||
[tro_annotate]`) in front of its separator, towards the predecessor TRO,
|
front of its TRO marker — marker radius + `jam_abstand` mm (`cfg/parameter.ini
|
||||||
where the carriers wait. Hidden attributes: `CONVEYOR` (NNNN), `NUMBER` (k),
|
[tro_annotate]`) from the marker — towards the predecessor TRO (against the
|
||||||
|
flow, where the carriers wait; with only a station before it, opposite to the
|
||||||
|
successor). A second entry (2Sep types) sits next to the first. Only the
|
||||||
|
position is derived this way; connections and flow direction are untouched. Hidden attributes: `CONVEYOR` (NNNN), `NUMBER` (k),
|
||||||
`TRO` + `ENTRY` (the stable key, `scl_context.jam_key`), `AREA` (J0039) and
|
`TRO` + `ENTRY` (the stable key, `scl_context.jam_key`), `AREA` (J0039) and
|
||||||
`AUTO` (the derived NNNN_k). A jam area without a conveyor shows
|
`AUTO` (the derived NNNN_k). A jam area without a conveyor shows
|
||||||
`Jam????_?`. Double-click opens the dialog **`JAM_EDIT`** (dxfmakros:
|
`Jam????_?`. Double-click opens the dialog **`JAM_EDIT`** (dxfmakros:
|
||||||
@@ -478,8 +519,9 @@ that are back at `AUTO`. `scl_skeleton` with a plant JSON reads that file, and
|
|||||||
the next `--anlage` run draws it, so drawing, file and SCL agree. Invalid
|
the next `--anlage` run draws it, so drawing, file and SCL agree. Invalid
|
||||||
values (conveyor not four digits, number < 1) are reported and skipped.
|
values (conveyor not four digits, number < 1) are reported and skipped.
|
||||||
|
|
||||||
Layers always carry the prefix `TRO_`. Like `tro_flow.py`, it never reads
|
Layers always carry the prefix `TRO_`. Like `tro_flow.py`, it reads only the
|
||||||
`cfg/tro_overrides.ini`, so the markers always match the flow diagram.
|
`edit`/`edit_add` sections of `cfg/tro_overrides.ini`, so the markers always
|
||||||
|
match the flow diagram and the TRO-JSON.
|
||||||
|
|
||||||
**Output** (in `%SKEL_RESULTS%`, unless `--check`): `<name>_annotated.dxf`,
|
**Output** (in `%SKEL_RESULTS%`, unless `--check`): `<name>_annotated.dxf`,
|
||||||
`<name>_registration.json` (the coordinate transform used, with its proof).
|
`<name>_registration.json` (the coordinate transform used, with its proof).
|
||||||
@@ -526,7 +568,9 @@ drawing is the current state, not the export.
|
|||||||
It finds the objects `tro_annotate.py` tagged with XDATA (AppID
|
It finds the objects `tro_annotate.py` tagged with XDATA (AppID
|
||||||
`SPS_SKEL_TRO`): the `TRO_SYM_<type>` block references (attributes
|
`SPS_SKEL_TRO`): the `TRO_SYM_<type>` block references (attributes
|
||||||
`ID`/`TYPE`/`ITEMS`/`CONFIDENCE`/`SEPARATORS`, plus `FB_BLOCK` when annotated
|
`ID`/`TYPE`/`ITEMS`/`CONFIDENCE`/`SEPARATORS`, plus `FB_BLOCK` when annotated
|
||||||
with `--fb`), the flow arrows (`"<from>-><to>"`), and the open-point markers
|
with `--fb`; since 2026-10-26 the markers carry `SLOTS` instead of
|
||||||
|
`SEPARATORS`, so the separators come back empty — the way back from the
|
||||||
|
drawing is now `tro_annotate --read-tros`), the flow arrows (`"<from>-><to>"`), and the open-point markers
|
||||||
(`"OFFEN:<name>"`). Each TRO also gets `predecessors`/`successors` computed
|
(`"OFFEN:<name>"`). Each TRO also gets `predecessors`/`successors` computed
|
||||||
from the arrows.
|
from the arrows.
|
||||||
|
|
||||||
@@ -562,7 +606,10 @@ renamed key, or `items`/`fbType` that do not match the TRO type in
|
|||||||
no SCL is written. A file with a different schema (e.g. the old
|
no SCL is written. A file with a different schema (e.g. the old
|
||||||
`tro_extract.py` output) gets a short message asking for `tro_flow --json`.
|
`tro_extract.py` output) gets a short message asking for `tro_flow --json`.
|
||||||
Part counts (`items`) and the FB type are taken from the catalog via the TRO
|
Part counts (`items`) and the FB type are taken from the catalog via the TRO
|
||||||
type.
|
type — except the storage lines (since 2026-10-26): the catalog count (19 for
|
||||||
|
`PinStore_Auto`) is only the default; a TRO with storage lines counts its own
|
||||||
|
(layout or BricsCAD correction), so the Mubea store shows 20. A TRO-JSON
|
||||||
|
written before still reads (the catalog count is accepted too).
|
||||||
|
|
||||||
Produces a **skeleton, not a running program**: one `REGION` per TRO with the
|
Produces a **skeleton, not a running program**: one `REGION` per TRO with the
|
||||||
instance call and every parameter line in the right order, following the real
|
instance call and every parameter line in the right order, following the real
|
||||||
@@ -1037,11 +1084,42 @@ wrong keys/items, SCL rendering from a document) — run with
|
|||||||
|
|
||||||
## 6a. `tro_overrides.py` — manual corrections to the derived TROs (library, no CLI)
|
## 6a. `tro_overrides.py` — manual corrections to the derived TROs (library, no CLI)
|
||||||
|
|
||||||
> **Since 2026-10-05 only `material_flow.py --connect` reads this file.**
|
> **Since 2026-10-05 only `material_flow.py --connect` reads the hand-written
|
||||||
> `tro_flow.py` and `tro_annotate.py` no longer apply corrections: the sections
|
> kinds below** (`tro`, `split`, `merge`, `openpoint`, `connections`).
|
||||||
> were keyed on running TRO numbers and went stale with every new export.
|
> `tro_flow.py` and `tro_annotate.py` no longer apply them: the sections were
|
||||||
> Errors in the TRO derivation are fixed in `lib/tro_flow.py` instead. The
|
> keyed on running TRO numbers and went stale with every new export. Errors in
|
||||||
> format below is kept for `material_flow.py`.
|
> the TRO derivation are fixed in `lib/tro_flow.py` instead.
|
||||||
|
>
|
||||||
|
> **Since 2026-10-26 the file also holds the BricsCAD corrections** (kinds
|
||||||
|
> `edit`, `edit_add`), and those *are* applied by `tro_flow.py` and
|
||||||
|
> `tro_annotate.py` (`tro_flow.apply_edits`). They are keyed on the ID from
|
||||||
|
> `<csv>_tro_ids.ini`, which stays stable across exports, and their parts carry
|
||||||
|
> a position. They are written by `tro_annotate.py --read-tros` (see §5),
|
||||||
|
> not by hand:
|
||||||
|
>
|
||||||
|
> ```ini
|
||||||
|
> [mubea.csv.edit.TRO05] ; derived TRO, key = ID in the ID file
|
||||||
|
> type = 1Sep1Swi ; omitted = as derived
|
||||||
|
> name = Einschleus_K2 ; new ID (the ID file keeps TRO05)
|
||||||
|
> separator1 = 0012 @ -400.0 -3554.9 ; part per slot: TeileId @ x y (CSV)
|
||||||
|
> switch1 = - ; slot deliberately empty
|
||||||
|
> lines = 0066 @ 958.0 12300.0, 0069 @ 958.0 11500.0 ; storage lines
|
||||||
|
> reason = ... ; may be added by hand, survives re-reads
|
||||||
|
>
|
||||||
|
> [mubea.csv.edit.TRO19]
|
||||||
|
> delete = true
|
||||||
|
>
|
||||||
|
> [mubea.csv.edit_add.TRO99] ; new TRO from TRO_INSERT
|
||||||
|
> type = 1Sep
|
||||||
|
> separator1 = 0036 @ 1475.0 -5078.0
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> If slot keys are present they are the complete set (missing slots stay
|
||||||
|
> empty); without them the parts stay as derived. Order of application:
|
||||||
|
> deletes, edits, adds, renames last. A part given to an edited/added TRO is
|
||||||
|
> taken from the TRO that had it; a TRO left without its separators is dropped
|
||||||
|
> (finding). Renamed and added TROs are not written under their new ID into
|
||||||
|
> the ID file (`Analysis.renamed`/`added`).
|
||||||
|
|
||||||
`tro_flow.py` derives TRO types from the mechanical layout alone. Some things
|
`tro_flow.py` derives TRO types from the mechanical layout alone. Some things
|
||||||
are simply not in the CSV export: a switch that the export does not carry, a
|
are simply not in the CSV export: a switch that the export does not carry, a
|
||||||
|
|||||||
+86
-8
@@ -82,6 +82,10 @@ __all__ = [
|
|||||||
"derive_slots",
|
"derive_slots",
|
||||||
"derive_jam_areas",
|
"derive_jam_areas",
|
||||||
"storage_lines",
|
"storage_lines",
|
||||||
|
"SLOT_PART_KINDS",
|
||||||
|
"slot_kind",
|
||||||
|
"slot_names",
|
||||||
|
"has_lines",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Kennung der TRO-JSON. v1 war die Ausgabe von tro_extract (aus der Zeichnung),
|
# Kennung der TRO-JSON. v1 war die Ausgabe von tro_extract (aus der Zeichnung),
|
||||||
@@ -204,9 +208,17 @@ class TroBase(_Positioned):
|
|||||||
@computed_field # type: ignore[prop-decorator]
|
@computed_field # type: ignore[prop-decorator]
|
||||||
@property
|
@property
|
||||||
def items(self) -> dict[str, int]:
|
def items(self) -> dict[str, int]:
|
||||||
"""Bauteile laut Katalog, z. B. {'Separator': 1} ({} wenn Typ unbekannt)."""
|
"""Bauteile laut Katalog, z. B. {'Separator': 1} ({} wenn Typ unbekannt).
|
||||||
|
|
||||||
|
Ausnahme Lagerlinien: der Katalog nennt nur die Vorgabe (PinStore_Auto
|
||||||
|
19); hat der TRO Linien, gilt deren Zahl (Layout oder Handkorrektur im
|
||||||
|
Dialog TRO_EDIT).
|
||||||
|
"""
|
||||||
definition = self.definition()
|
definition = self.definition()
|
||||||
return definition.get_items() if definition else {}
|
if not definition:
|
||||||
|
return {}
|
||||||
|
return _with_line_count(definition.get_items(), self.separators,
|
||||||
|
getattr(self, "hosts", []))
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -219,9 +231,14 @@ class TroBase(_Positioned):
|
|||||||
fb_block = data.pop("fbType", data.pop("fb_block", None))
|
fb_block = data.pop("fbType", data.pop("fb_block", None))
|
||||||
type_name = data.get("type", data.get("type_name"))
|
type_name = data.get("type", data.get("type_name"))
|
||||||
definition = get_tro(type_name) if isinstance(type_name, str) else None
|
definition = get_tro(type_name) if isinstance(type_name, str) else None
|
||||||
expected_items = definition.get_items() if definition else {}
|
expected_items = (_with_line_count(definition.get_items(),
|
||||||
|
data.get("separators", []),
|
||||||
|
data.get("hosts", []))
|
||||||
|
if definition else {})
|
||||||
|
# Dateien vor 2026-10-26 tragen bei Lagerlinien die Katalogvorgabe
|
||||||
|
catalog_items = definition.get_items() if definition else {}
|
||||||
expected_fb = definition.get_fb_block() if definition else ""
|
expected_fb = definition.get_fb_block() if definition else ""
|
||||||
if items is not None and dict(items) != expected_items:
|
if items is not None and dict(items) not in (expected_items, catalog_items):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"items {items} passt nicht zum Typ '{type_name}' laut tro_catalog "
|
f"items {items} passt nicht zum Typ '{type_name}' laut tro_catalog "
|
||||||
f"({expected_items}) - den Typ aendern, nicht die Bauteile"
|
f"({expected_items}) - den Typ aendern, nicht die Bauteile"
|
||||||
@@ -381,6 +398,11 @@ class Analysis(BaseModel):
|
|||||||
# Systemgrenzen (BTMT Be-/Entladestationen) - siehe derive_boundaries; kein
|
# Systemgrenzen (BTMT Be-/Entladestationen) - siehe derive_boundaries; kein
|
||||||
# TRO, nur damit der tatsaechliche Materialein-/ausgang sichtbar wird.
|
# TRO, nur damit der tatsaechliche Materialein-/ausgang sichtbar wird.
|
||||||
boundaries: list[DerivedTro] = Field(default_factory=list)
|
boundaries: list[DerivedTro] = Field(default_factory=list)
|
||||||
|
# Handkorrekturen aus BricsCAD (tro_flow.apply_edits): umbenannte TROs
|
||||||
|
# (neue ID -> ID aus <csv>_tro_ids.ini) und von Hand angelegte TROs - beide
|
||||||
|
# gehoeren nicht unter ihrer neuen Kennung in die ID-Datei.
|
||||||
|
renamed: dict[str, str] = Field(default_factory=dict)
|
||||||
|
added: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
def by_id(self, tro_id: str) -> DerivedTro | None:
|
def by_id(self, tro_id: str) -> DerivedTro | None:
|
||||||
for tro in self.tros + self.boundaries:
|
for tro in self.tros + self.boundaries:
|
||||||
@@ -388,6 +410,11 @@ class Analysis(BaseModel):
|
|||||||
return tro
|
return tro
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def edit_key(self, tro_id: str) -> str:
|
||||||
|
"""Schluessel des TRO in tro_overrides.ini (ID der ID-Datei bzw. Kennung
|
||||||
|
des von Hand angelegten TRO)."""
|
||||||
|
return self.renamed.get(tro_id, tro_id)
|
||||||
|
|
||||||
|
|
||||||
class TroDocument(BaseModel):
|
class TroDocument(BaseModel):
|
||||||
"""
|
"""
|
||||||
@@ -613,6 +640,12 @@ def _at(values: list[str], index: int) -> str:
|
|||||||
return values[index] if index < len(values) else ""
|
return values[index] if index < len(values) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _lines_of(separators: list[str], hosts: list[str]) -> list[str]:
|
||||||
|
seps, hosts = list(separators or []), list(hosts or [])
|
||||||
|
count = len(hosts) if 0 < len(hosts) <= len(seps) else len(seps)
|
||||||
|
return seps[len(seps) - count:]
|
||||||
|
|
||||||
|
|
||||||
def storage_lines(tro: TroBase) -> list[str]:
|
def storage_lines(tro: TroBase) -> list[str]:
|
||||||
"""Separatoren der Lagerlinien eines Speicher-TRO.
|
"""Separatoren der Lagerlinien eines Speicher-TRO.
|
||||||
|
|
||||||
@@ -620,10 +653,55 @@ def storage_lines(tro: TroBase) -> list[str]:
|
|||||||
letzten len(hosts) (eine je Speicherbahn). Fehlen Hosts, zaehlt jeder
|
letzten len(hosts) (eine je Speicherbahn). Fehlen Hosts, zaehlt jeder
|
||||||
Separator als Linie - dieselbe Regel wie scl_context._store.
|
Separator als Linie - dieselbe Regel wie scl_context._store.
|
||||||
"""
|
"""
|
||||||
hosts = list(getattr(tro, "hosts", []) or [])
|
return _lines_of(tro.separators, getattr(tro, "hosts", []))
|
||||||
seps = list(tro.separators)
|
|
||||||
count = len(hosts) if 0 < len(hosts) <= len(seps) else len(seps)
|
|
||||||
return seps[len(seps) - count:]
|
def _with_line_count(items: dict[str, int], separators: list[str],
|
||||||
|
hosts: list[str]) -> dict[str, int]:
|
||||||
|
"""Katalog-Bauteile, Lagerlinien auf die Zahl der Linien des TRO gesetzt."""
|
||||||
|
if items.get(TroItem.STORAGE_LINE) and (lines := _lines_of(separators, hosts)):
|
||||||
|
items = {**items, TroItem.STORAGE_LINE: len(lines)}
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
# Slot-Art (Name ohne Nummer) -> Bauteilarten (Element.kind im CSV), die in
|
||||||
|
# diesen Slot passen. Gebraucht vom Dialog TRO_EDIT (Pruefung beim Waehlen im
|
||||||
|
# Plan, tro_annotate --emit-lisp) und beim Anwenden der Handkorrekturen
|
||||||
|
# (tro_flow.apply_edits). "drive" ist der tragende Foerderer.
|
||||||
|
SLOT_PART_KINDS: dict[str, tuple[str, ...]] = {
|
||||||
|
"separator": ("Separator",),
|
||||||
|
"line": ("Separator",),
|
||||||
|
"switch": ("Ausschleus", "Weiche"),
|
||||||
|
"scanner": ("Scanner",),
|
||||||
|
"drive": ("Strecke", "Gefaellestrecke", "Kreisel"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def slot_kind(slot: str) -> str:
|
||||||
|
"""'separator2' -> 'separator', 'line17' -> 'line', 'drive' -> 'drive'."""
|
||||||
|
return slot.rstrip("0123456789")
|
||||||
|
|
||||||
|
|
||||||
|
def slot_names(type_name: str) -> list[str]:
|
||||||
|
"""Slots eines Typs in der Reihenfolge von derive_slots, ohne Lagerlinien.
|
||||||
|
|
||||||
|
Die Lagerlinien (line1..lineN) haengen am TRO, nicht am Typ - ob ein Typ
|
||||||
|
sie hat, sagt :func:`has_lines`.
|
||||||
|
"""
|
||||||
|
definition = get_tro(type_name)
|
||||||
|
items = definition.get_items() if definition else {}
|
||||||
|
names = [f"separator{i}" for i in range(1, items.get(TroItem.SEPARATOR, 0) + 1)]
|
||||||
|
names += [f"switch{i}" for i in range(1, items.get(TroItem.SWITCH, 0) + 1)]
|
||||||
|
names += [f"scanner{i}" for i in range(1, items.get(TroItem.SCANNER, 0) + 1)]
|
||||||
|
if items.get(TroItem.VARIO_DRIVE):
|
||||||
|
names.append("drive")
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def has_lines(type_name: str) -> int:
|
||||||
|
"""Vorgabe der Lagerlinien laut Katalog (0 = Typ hat keine)."""
|
||||||
|
definition = get_tro(type_name)
|
||||||
|
return definition.get_items().get(TroItem.STORAGE_LINE, 0) if definition else 0
|
||||||
|
|
||||||
|
|
||||||
def derive_slots(tro: DerivedTro) -> list[Slot]:
|
def derive_slots(tro: DerivedTro) -> list[Slot]:
|
||||||
|
|||||||
+476
-66
@@ -35,9 +35,24 @@ Typen auch ohne Farbe unterscheiden. Die Daten stehen als Attribute am Block:
|
|||||||
TYPE 1Sep sichtbar, im Dialog bearbeitbar
|
TYPE 1Sep sichtbar, im Dialog bearbeitbar
|
||||||
FB_BLOCK FB_ILS_... sichtbar nur mit --fb
|
FB_BLOCK FB_ILS_... sichtbar nur mit --fb
|
||||||
UID 0062 unsichtbar, dauerhafte interne Nummer
|
UID 0062 unsichtbar, dauerhafte interne Nummer
|
||||||
|
KEY TRO03 unsichtbar, Schluessel in tro_overrides.ini (ID der
|
||||||
|
ID-Datei; leer bei einem mit TRO_INSERT gesetzten)
|
||||||
ITEMS 1x Separator unsichtbar, reine Daten
|
ITEMS 1x Separator unsichtbar, reine Daten
|
||||||
CONFIDENCE mittel unsichtbar
|
CONFIDENCE mittel unsichtbar
|
||||||
SEPARATORS 0010 unsichtbar
|
SLOTS separator1=0010;switch1=0061 unsichtbar, Bauteil je Slot
|
||||||
|
(TeileId; Lagerlinien als lines=0066,0069,...),
|
||||||
|
im Dialog TRO_EDIT bearbeitbar
|
||||||
|
|
||||||
|
Handkorrekturen (--read-tros)
|
||||||
|
-----------------------------
|
||||||
|
Im Dialog TRO_EDIT (dxfmakros) aendern die SPS-Programmierer Typ, Kennung und
|
||||||
|
die Bauteile je Slot; TRO_INSERT setzt neue Marker, geloeschte Marker fallen
|
||||||
|
weg. Fuer "Im Plan waehlen" legt die Beschriftung die Bauteile des Exports
|
||||||
|
(TeileId, Art, Lage in der Zeichnung) als XRECORD SPS_SKEL_PARTS in das
|
||||||
|
Named Object Dictionary. --read-tros vergleicht die Marker mit der Ableitung
|
||||||
|
und schreibt die Abweichungen als Abschnitte edit/edit_add nach
|
||||||
|
cfg/tro_overrides.ini; tro_flow und tro_annotate wenden sie beim naechsten
|
||||||
|
Lauf an (tro_flow.apply_edits).
|
||||||
|
|
||||||
Zwei Kennungen, mit Absicht:
|
Zwei Kennungen, mit Absicht:
|
||||||
ID die sprechende Kennung (TRO03). Darf sich aendern - der Dialog TRO_EDIT
|
ID die sprechende Kennung (TRO03). Darf sich aendern - der Dialog TRO_EDIT
|
||||||
@@ -122,7 +137,19 @@ from tro_flow import (
|
|||||||
resolve_ids_file,
|
resolve_ids_file,
|
||||||
)
|
)
|
||||||
from tro_ids import TroIds
|
from tro_ids import TroIds
|
||||||
from model import ANLAGE_SCHEMA, AnlageDocument, JamArea
|
from model import (
|
||||||
|
ANLAGE_SCHEMA,
|
||||||
|
SLOT_PART_KINDS,
|
||||||
|
AnlageDocument,
|
||||||
|
DerivedTro,
|
||||||
|
JamArea,
|
||||||
|
derive_slots,
|
||||||
|
has_lines,
|
||||||
|
slot_kind,
|
||||||
|
slot_names,
|
||||||
|
)
|
||||||
|
from tro_ids import id_problem
|
||||||
|
from tro_overrides import PartRef, TroEdit, load_overrides, override_path, save_edits
|
||||||
from scl_context import (
|
from scl_context import (
|
||||||
JAM_NAMES_SUFFIX,
|
JAM_NAMES_SUFFIX,
|
||||||
HwSource,
|
HwSource,
|
||||||
@@ -147,6 +174,12 @@ CONNECTION_BLOCK = "CONNECTION_ARROW"
|
|||||||
# Staubereich (--anlage). Der Name beginnt mit JAM_, damit SSG_BLOCKEDIT ihn
|
# Staubereich (--anlage). Der Name beginnt mit JAM_, damit SSG_BLOCKEDIT ihn
|
||||||
# ueber "JAM_*" an den Dialog JAM_EDIT (dxfmakros/Lisp/Jam_Edit.lsp) gibt.
|
# ueber "JAM_*" an den Dialog JAM_EDIT (dxfmakros/Lisp/Jam_Edit.lsp) gibt.
|
||||||
JAM_BLOCK = "JAM_AREA"
|
JAM_BLOCK = "JAM_AREA"
|
||||||
|
# Bauteile des Exports fuer "Im Plan waehlen" im Dialog TRO_EDIT: XRECORD im
|
||||||
|
# Named Object Dictionary, je Bauteil (1 . TeileId) (3 . Art) (10 . Lage)
|
||||||
|
# (40 . halbe Breite) (41 . halbe Hoehe) in Zeichnungskoordinaten.
|
||||||
|
PARTS_XRECORD = "SPS_SKEL_PARTS"
|
||||||
|
# Unsichtbare Attribute der Marker (build_symbol)
|
||||||
|
HIDDEN_TAGS = ("UID", "KEY", "ITEMS", "CONFIDENCE", "SLOTS")
|
||||||
# Anzeige eines Staubereichs ohne Foerderer (Nummer nicht ableitbar)
|
# Anzeige eines Staubereichs ohne Foerderer (Nummer nicht ableitbar)
|
||||||
JAM_OFFEN = "Jam????_?"
|
JAM_OFFEN = "Jam????_?"
|
||||||
|
|
||||||
@@ -155,7 +188,14 @@ JAM_OFFEN = "Jam????_?"
|
|||||||
DEFAULT_MARKER = get_float("tro_annotate", "default_marker")
|
DEFAULT_MARKER = get_float("tro_annotate", "default_marker")
|
||||||
DEFAULT_TEXT = get_float("tro_annotate", "default_text")
|
DEFAULT_TEXT = get_float("tro_annotate", "default_text")
|
||||||
DEFAULT_MAX_RESIDUAL = get_float("tro_annotate", "default_max_residual")
|
DEFAULT_MAX_RESIDUAL = get_float("tro_annotate", "default_max_residual")
|
||||||
JAM_VERSATZ = get_float("tro_annotate", "jam_versatz")
|
JAM_ABSTAND = get_float("tro_annotate", "jam_abstand")
|
||||||
|
# Staubereich als stehendes Rechteck, Masse relativ zur Texthoehe: halbe
|
||||||
|
# Breite / halbe Hoehe, Name senkrecht darin (Jam0054_3 passt hinein).
|
||||||
|
JAM_HALB_BREITE = 0.5
|
||||||
|
JAM_HALB_HOEHE = 2.6
|
||||||
|
JAM_TEXT = 0.6
|
||||||
|
# Fuellung und Rand (ACI 8 = grau) und Schrift (ACI 250 = fast schwarz)
|
||||||
|
JAM_FUELLUNG = "#d0d0d0"
|
||||||
|
|
||||||
|
|
||||||
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
||||||
@@ -189,10 +229,26 @@ def emit_lisp(cad_dir: Path) -> Path:
|
|||||||
cad_dir.mkdir(parents=True, exist_ok=True)
|
cad_dir.mkdir(parents=True, exist_ok=True)
|
||||||
path = cad_dir / "tro_types.lsp"
|
path = cad_dir / "tro_types.lsp"
|
||||||
types = " ".join(f'"{d.get_name()}"' for d in TRO_CATALOG)
|
types = " ".join(f'"{d.get_name()}"' for d in TRO_CATALOG)
|
||||||
|
|
||||||
|
def lisp_list(values) -> str:
|
||||||
|
return " ".join(f'"{v}"' for v in values)
|
||||||
|
|
||||||
|
# Je Typ die Slots in der Reihenfolge von model.derive_slots, dazu die
|
||||||
|
# Vorgabe der Lagerlinien (0 = keine): ("PinStore_Auto" 19 "separator1" ...)
|
||||||
|
slots = "\n".join(
|
||||||
|
" (" + " ".join([f'"{d.get_name()}"', str(has_lines(d.get_name()))]
|
||||||
|
+ [f'"{s}"' for s in slot_names(d.get_name())]) + ")"
|
||||||
|
for d in TRO_CATALOG)
|
||||||
|
kinds = "\n".join(f' ("{k}" {lisp_list(v)})' for k, v in SLOT_PART_KINDS.items())
|
||||||
path.write_text(
|
path.write_text(
|
||||||
";; Automatisch erzeugt von lib/tro_annotate.py --emit-lisp\n"
|
";; Automatisch erzeugt von lib/tro_annotate.py --emit-lisp\n"
|
||||||
";; Quelle: TRO_CATALOG in lib/tro_catalog.py - nicht manuell aendern.\n"
|
";; Quelle: TRO_CATALOG in lib/tro_catalog.py, Slots aus lib/model.py\n"
|
||||||
|
";; (slot_names, SLOT_PART_KINDS) - nicht manuell aendern.\n"
|
||||||
f"(setq *TRO-TYPES* (list {types}))\n"
|
f"(setq *TRO-TYPES* (list {types}))\n"
|
||||||
|
";; (Typ Lagerlinien-Vorgabe Slot ...) - Lagerlinien heissen line1..lineN\n"
|
||||||
|
f"(setq *TRO-SLOTS* '(\n{slots}\n))\n"
|
||||||
|
";; (Slot-Art Bauteilart ...) - Pruefung beim Waehlen im Plan\n"
|
||||||
|
f"(setq *TRO-SLOT-KINDS* '(\n{kinds}\n))\n"
|
||||||
"(princ)\n",
|
"(princ)\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
@@ -403,7 +459,7 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
|
|||||||
("weiche", "#cc8800"),
|
("weiche", "#cc8800"),
|
||||||
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
|
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
|
||||||
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
|
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
|
||||||
("jam", "#7030a0"),
|
("jam", "#808080"),
|
||||||
("legend", "#808080")):
|
("legend", "#808080")):
|
||||||
name = f"{prefix}{key.upper()}"
|
name = f"{prefix}{key.upper()}"
|
||||||
names[key] = name
|
names[key] = name
|
||||||
@@ -518,6 +574,18 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
|
|||||||
"""
|
"""
|
||||||
name = f"{BLOCK_PREFIX}{type_name}"
|
name = f"{BLOCK_PREFIX}{type_name}"
|
||||||
if name in doc.blocks:
|
if name in doc.blocks:
|
||||||
|
# Blockdefinition aus einem frueheren Lauf (bleibt, weil von Hand
|
||||||
|
# gesetzte Marker sie benutzen): fehlende Datenattribute nachtragen,
|
||||||
|
# sonst haetten die neuen Marker z. B. kein KEY/SLOTS.
|
||||||
|
blk = doc.blocks.get(name)
|
||||||
|
vorhanden = {a.dxf.tag.upper() for a in blk.query("ATTDEF")}
|
||||||
|
offset = min((a.dxf.insert.y for a in blk.query("ATTDEF")), default=0.0)
|
||||||
|
for tag_name in HIDDEN_TAGS:
|
||||||
|
if tag_name not in vorhanden:
|
||||||
|
offset -= text
|
||||||
|
blk.add_attdef(tag_name, insert=(marker * 1.5, offset), height=text * 0.7,
|
||||||
|
dxfattribs={"style": "Standard", "color": 256, "text": "",
|
||||||
|
"invisible": 1})
|
||||||
return name
|
return name
|
||||||
blk = doc.blocks.new(name=name)
|
blk = doc.blocks.new(name=name)
|
||||||
|
|
||||||
@@ -542,9 +610,9 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
|
|||||||
)
|
)
|
||||||
offset -= height * 1.55
|
offset -= height * 1.55
|
||||||
|
|
||||||
# nicht sichtbare Attribute: reine Daten, nicht im Dialog.
|
# nicht sichtbare Attribute: reine Daten; KEY und SLOTS liest der Dialog
|
||||||
# ID zuerst - gleiche Reihenfolge wie *strecke-attr-front* in dxfmakros.
|
# TRO_EDIT. ID zuerst - gleiche Reihenfolge wie *strecke-attr-front* in dxfmakros.
|
||||||
for tag_name in ("UID", "ITEMS", "CONFIDENCE", "SEPARATORS"):
|
for tag_name in HIDDEN_TAGS:
|
||||||
blk.add_attdef(
|
blk.add_attdef(
|
||||||
tag_name,
|
tag_name,
|
||||||
insert=(gap, offset),
|
insert=(gap, offset),
|
||||||
@@ -591,10 +659,11 @@ def place_markers(
|
|||||||
values = {
|
values = {
|
||||||
"ID": tro.tro_id,
|
"ID": tro.tro_id,
|
||||||
"UID": internal[tro.tro_id],
|
"UID": internal[tro.tro_id],
|
||||||
|
"KEY": analysis.edit_key(tro.tro_id),
|
||||||
"TYPE": tro.type_name,
|
"TYPE": tro.type_name,
|
||||||
"ITEMS": ", ".join(f"{c}x {i}" for i, c in sorted(tro.items.items())),
|
"ITEMS": ", ".join(f"{c}x {i}" for i, c in sorted(tro.items.items())),
|
||||||
"CONFIDENCE": tro.confidence,
|
"CONFIDENCE": tro.confidence,
|
||||||
"SEPARATORS": ", ".join(tro.separators),
|
"SLOTS": format_slots(*tro_slots(tro)),
|
||||||
}
|
}
|
||||||
if show_fb:
|
if show_fb:
|
||||||
values["FB_BLOCK"] = tro.fb_block or "-"
|
values["FB_BLOCK"] = tro.fb_block or "-"
|
||||||
@@ -773,7 +842,8 @@ def draw_connections(
|
|||||||
|
|
||||||
def build_jam_symbol(doc, text: float) -> str:
|
def build_jam_symbol(doc, text: float) -> str:
|
||||||
"""
|
"""
|
||||||
Blockdefinition fuer einen Staubereich: Rechteck, der Name mittig darin.
|
Blockdefinition fuer einen Staubereich: stehendes graues Rechteck, der
|
||||||
|
Name senkrecht mittig darin.
|
||||||
|
|
||||||
Attribute:
|
Attribute:
|
||||||
NAME sichtbar - Jam0054_3 (Foerderer + laufende Nummer)
|
NAME sichtbar - Jam0054_3 (Foerderer + laufende Nummer)
|
||||||
@@ -787,15 +857,23 @@ def build_jam_symbol(doc, text: float) -> str:
|
|||||||
TRO + ENTRY ist der stabile Schluessel (scl_context.jam_key).
|
TRO + ENTRY ist der stabile Schluessel (scl_context.jam_key).
|
||||||
"""
|
"""
|
||||||
if JAM_BLOCK in doc.blocks:
|
if JAM_BLOCK in doc.blocks:
|
||||||
|
# Form aus einem frueheren Lauf (liegendes Rechteck) ersetzen; die
|
||||||
|
# Attributdefinitionen bleiben, damit vorhandene Bloecke gueltig sind.
|
||||||
|
blk = doc.blocks.get(JAM_BLOCK)
|
||||||
|
for e in list(blk):
|
||||||
|
if e.dxftype() != "ATTDEF":
|
||||||
|
blk.delete_entity(e)
|
||||||
|
_jam_form(blk, text)
|
||||||
|
for a in blk.query("ATTDEF"):
|
||||||
|
if a.dxf.tag.upper() == "NAME":
|
||||||
|
_jam_name(a, text)
|
||||||
return JAM_BLOCK
|
return JAM_BLOCK
|
||||||
blk = doc.blocks.new(name=JAM_BLOCK)
|
blk = doc.blocks.new(name=JAM_BLOCK)
|
||||||
w, h = text * 4.2, text * 1.2
|
_jam_form(blk, text)
|
||||||
blk.add_lwpolyline([(-w, -h), (w, -h), (w, h), (-w, h)], close=True,
|
w, h = text * JAM_HALB_BREITE, text * JAM_HALB_HOEHE
|
||||||
dxfattribs={"color": 256})
|
name = blk.add_attdef("NAME", insert=(0, 0), height=text * JAM_TEXT,
|
||||||
name = blk.add_attdef("NAME", insert=(0, 0), height=text,
|
dxfattribs={"style": "Standard", "text": JAM_OFFEN})
|
||||||
dxfattribs={"style": "Standard", "color": 256,
|
_jam_name(name, text)
|
||||||
"text": JAM_OFFEN})
|
|
||||||
name.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER)
|
|
||||||
offset = -h - text
|
offset = -h - text
|
||||||
for tag_name in ("CONVEYOR", "NUMBER", "TRO", "ENTRY", "AREA", "AUTO"):
|
for tag_name in ("CONVEYOR", "NUMBER", "TRO", "ENTRY", "AREA", "AUTO"):
|
||||||
blk.add_attdef(tag_name, insert=(-w, offset), height=text * 0.7,
|
blk.add_attdef(tag_name, insert=(-w, offset), height=text * 0.7,
|
||||||
@@ -805,36 +883,70 @@ def build_jam_symbol(doc, text: float) -> str:
|
|||||||
return JAM_BLOCK
|
return JAM_BLOCK
|
||||||
|
|
||||||
|
|
||||||
def _jam_position(area: JamArea, anlage: AnlageDocument) -> tuple[float, float] | None:
|
def _jam_form(blk, text: float) -> None:
|
||||||
"""Lage eines Staubereichs (CSV-Koordinaten): vor seinem Separator.
|
"""Stehendes Rechteck: graue Fuellung, grauer Rand."""
|
||||||
|
w, h = text * JAM_HALB_BREITE, text * JAM_HALB_HOEHE
|
||||||
|
ecken = [(-w, -h), (w, -h), (w, h), (-w, h)]
|
||||||
|
hatch = blk.add_hatch(dxfattribs={"true_color": ezdxf.rgb2int(hex_to_rgb(JAM_FUELLUNG))})
|
||||||
|
hatch.paths.add_polyline_path(ecken, is_closed=True)
|
||||||
|
blk.add_lwpolyline(ecken, close=True, dxfattribs={"color": 8})
|
||||||
|
|
||||||
Ausgangspunkt ist der Separator (sonst die TRO-Position); von dort
|
|
||||||
JAM_VERSATZ in Richtung des ersten Vorgaenger-TRO - gegen den Fluss, wo
|
def _jam_name(attdef, text: float) -> None:
|
||||||
die Traeger warten. Liegt der Vorgaenger naeher, nur die halbe Strecke.
|
"""Name senkrecht (90 Grad) mittig ins Rechteck."""
|
||||||
|
attdef.dxf.height = text * JAM_TEXT
|
||||||
|
attdef.dxf.rotation = 90.0
|
||||||
|
attdef.dxf.color = 250
|
||||||
|
attdef.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER)
|
||||||
|
|
||||||
|
|
||||||
|
def _jam_position(area: JamArea, anlage: AnlageDocument,
|
||||||
|
marker: float, text: float) -> tuple[float, float] | None:
|
||||||
|
"""Lage eines Staubereichs (CSV-Koordinaten): direkt vor seinem TRO.
|
||||||
|
|
||||||
|
Ausgangspunkt ist der TRO-Marker (die TRO-Position, nicht der Separator:
|
||||||
|
bei Speichern und 2Sep-Typen liegt der weit vom Marker). Das Rechteck
|
||||||
|
sitzt gerade so weit davon, dass es den Marker nicht beruehrt
|
||||||
|
(Markerradius + JAM_ABSTAND + halbe Breite), ein zweiter Eingang direkt
|
||||||
|
daneben - in Richtung des ersten Vorgaenger-TRO, gegen den Fluss, wo die
|
||||||
|
Traeger warten. Ohne Vorgaenger mit Lage (Station) gegen die Richtung zum
|
||||||
|
Nachfolger. Liegt der Nachbar naeher, hoechstens die halbe Strecke. Nur
|
||||||
|
Lage - Verbindungen und Fliessrichtung bleiben unberuehrt.
|
||||||
"""
|
"""
|
||||||
layout = anlage.layout
|
layout = anlage.layout
|
||||||
part = layout.part(area.separator) if area.separator else None
|
tro = layout.by_id(area.tro)
|
||||||
if part is not None and part.position is not None:
|
if tro is not None and tro.position:
|
||||||
x, y = part.position[0], part.position[1]
|
x, y = tro.position[0], tro.position[1]
|
||||||
elif area.position is not None:
|
elif area.position is not None:
|
||||||
x, y = area.position[0], area.position[1]
|
x, y = area.position[0], area.position[1]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
for vor in area.predecessors:
|
breite = 2 * text * JAM_HALB_BREITE
|
||||||
tro = layout.by_id(vor)
|
abstand = marker + JAM_ABSTAND + breite / 2 + (area.entry - 1) * (breite + JAM_ABSTAND)
|
||||||
if tro is None or not tro.position:
|
|
||||||
continue
|
def richtung(ids: list[str], vorzeichen: float):
|
||||||
vx, vy = tro.position[0] - x, tro.position[1] - y
|
for other_id in ids:
|
||||||
dist = math.hypot(vx, vy)
|
other = layout.by_id(other_id)
|
||||||
if dist > 1e-6:
|
if other is None or not other.position:
|
||||||
schritt = min(JAM_VERSATZ, dist / 2)
|
continue
|
||||||
return x + vx / dist * schritt, y + vy / dist * schritt
|
vx, vy = other.position[0] - x, other.position[1] - y
|
||||||
return x, y
|
dist = math.hypot(vx, vy)
|
||||||
|
if dist > 1e-6:
|
||||||
|
schritt = min(abstand, dist / 2)
|
||||||
|
return (x + vorzeichen * vx / dist * schritt,
|
||||||
|
y + vorzeichen * vy / dist * schritt)
|
||||||
|
return None
|
||||||
|
|
||||||
|
lage = richtung(area.predecessors or (tro.predecessors if tro else []), 1.0)
|
||||||
|
if lage is None and tro is not None:
|
||||||
|
lage = richtung(tro.successors, -1.0)
|
||||||
|
return lage if lage is not None else (x - abstand, y)
|
||||||
|
|
||||||
|
|
||||||
def place_jams(
|
def place_jams(
|
||||||
msp, doc, anlage: AnlageDocument, overrides: dict[str, tuple[str, int]],
|
msp, doc, anlage: AnlageDocument, overrides: dict[str, tuple[str, int]],
|
||||||
transform: Transform, layers: dict[str, str], text: float, warnings: list[str],
|
transform: Transform, layers: dict[str, str], text: float, warnings: list[str],
|
||||||
|
marker: float = DEFAULT_MARKER,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Je Staubereich der Anlagen-JSON einen JAM_AREA-Block einfuegen.
|
Je Staubereich der Anlagen-JSON einen JAM_AREA-Block einfuegen.
|
||||||
@@ -849,7 +961,7 @@ def place_jams(
|
|||||||
block = build_jam_symbol(doc, text)
|
block = build_jam_symbol(doc, text)
|
||||||
gezeichnet = 0
|
gezeichnet = 0
|
||||||
for area in layout.jam_areas_or_derived():
|
for area in layout.jam_areas_or_derived():
|
||||||
lage = _jam_position(area, anlage)
|
lage = _jam_position(area, anlage, marker, text)
|
||||||
if lage is None:
|
if lage is None:
|
||||||
warnings.append(f"Staubereich {area.jam_id} ({area.tro}): keine Lage - "
|
warnings.append(f"Staubereich {area.jam_id} ({area.tro}): keine Lage - "
|
||||||
f"nicht gezeichnet")
|
f"nicht gezeichnet")
|
||||||
@@ -915,6 +1027,218 @@ def read_jams(dxf_file: Path, ini_file: Path) -> tuple[dict[str, tuple[str, int]
|
|||||||
return overrides, meldungen
|
return overrides, meldungen
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Handkorrekturen an den TROs (Dialog TRO_EDIT, --read-tros)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def tro_slots(tro: DerivedTro) -> tuple[dict[str, str], list[str]]:
|
||||||
|
"""Bauteile je Slot (ohne Lagerlinien) und die Lagerlinien in Reihenfolge."""
|
||||||
|
slots: dict[str, str] = {}
|
||||||
|
lines: list[str] = []
|
||||||
|
for slot in derive_slots(tro):
|
||||||
|
if slot_kind(slot.slot) == "line":
|
||||||
|
lines.append(slot.layout_id)
|
||||||
|
else:
|
||||||
|
slots[slot.slot] = slot.layout_id
|
||||||
|
return slots, lines
|
||||||
|
|
||||||
|
|
||||||
|
def format_slots(slots: dict[str, str], lines: list[str]) -> str:
|
||||||
|
"""Attribut SLOTS: 'separator1=0010;switch1=;lines=0066,0069'."""
|
||||||
|
text = ";".join(f"{k}={v}" for k, v in slots.items())
|
||||||
|
if lines:
|
||||||
|
text += (";" if text else "") + "lines=" + ",".join(lines)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def parse_slots(text: str) -> tuple[dict[str, str], list[str] | None]:
|
||||||
|
"""Gegenstueck zu format_slots; lines ist None, wenn das Attribut keine nennt."""
|
||||||
|
slots: dict[str, str] = {}
|
||||||
|
lines: list[str] | None = None
|
||||||
|
for part in text.split(";"):
|
||||||
|
key, sep, value = (t.strip() for t in part.partition("="))
|
||||||
|
if not sep or not key:
|
||||||
|
continue
|
||||||
|
if key.lower() == "lines":
|
||||||
|
lines = [v.strip() for v in value.split(",") if v.strip()]
|
||||||
|
else:
|
||||||
|
slots[key.lower()] = value
|
||||||
|
return slots, lines
|
||||||
|
|
||||||
|
|
||||||
|
def write_parts(doc, elements, transform: Transform) -> int:
|
||||||
|
"""Bauteile, die in einen Slot passen, als XRECORD in die Zeichnung legen."""
|
||||||
|
kinds = {k for v in SLOT_PART_KINDS.values() for k in v}
|
||||||
|
tags: list[tuple[int, object]] = []
|
||||||
|
for e in elements:
|
||||||
|
if e.kind not in kinds or e.position is None or not e.teile_id:
|
||||||
|
continue
|
||||||
|
x, y = transform.to_dxf(e.x, e.y)
|
||||||
|
w, h = (e.bbox[0] / 2, e.bbox[1] / 2) if e.bbox else (0.0, 0.0)
|
||||||
|
tags += [(1, e.teile_id), (3, e.kind), (10, (x, y, 0.0)), (40, w), (41, h)]
|
||||||
|
root = doc.rootdict
|
||||||
|
if PARTS_XRECORD in root:
|
||||||
|
root.remove(PARTS_XRECORD)
|
||||||
|
xrecord = root.add_xrecord(PARTS_XRECORD)
|
||||||
|
xrecord.reset(tags)
|
||||||
|
return len(tags) // 5
|
||||||
|
|
||||||
|
|
||||||
|
def read_markers(dxf_file: Path) -> list[dict[str, str]]:
|
||||||
|
"""Attribute aller TRO-Marker (Bloecke TRO_SYM_*) der Zeichnung."""
|
||||||
|
doc = ezdxf.readfile(str(dxf_file))
|
||||||
|
markers = []
|
||||||
|
for ref in doc.modelspace().query("INSERT"):
|
||||||
|
if not ref.dxf.name.upper().startswith(BLOCK_PREFIX):
|
||||||
|
continue
|
||||||
|
a = {att.dxf.tag.upper(): (att.dxf.text or "").strip() for att in ref.attribs}
|
||||||
|
a["_BLOCK"] = ref.dxf.name
|
||||||
|
markers.append(a)
|
||||||
|
return markers
|
||||||
|
|
||||||
|
|
||||||
|
def read_tros(markers: list[dict[str, str]], raw: Analysis, elements,
|
||||||
|
csv_name: str, cfg_dir: Path | None = None,
|
||||||
|
write: bool = True) -> tuple[list[TroEdit], list[str]]:
|
||||||
|
"""Marker der Zeichnung mit der Ableitung vergleichen -> Handkorrekturen.
|
||||||
|
|
||||||
|
``raw`` ist die Ableitung OHNE Handkorrekturen (IDs aus der ID-Datei). Je
|
||||||
|
Marker mit KEY eines abgeleiteten TRO entsteht ein edit-Abschnitt, wenn
|
||||||
|
Typ, Kennung oder Bauteile abweichen; ein Marker ohne passenden KEY ist
|
||||||
|
ein neuer TRO (edit_add); ein abgeleiteter TRO mit Lage, zu dem kein
|
||||||
|
Marker mehr da ist, wurde geloescht. Die Abschnitte ersetzen alle
|
||||||
|
bisherigen edit-/edit_add-Abschnitte der CSV in tro_overrides.ini
|
||||||
|
(Begruendungen bleiben). Rueckgabe: (Korrekturen, Meldungen).
|
||||||
|
"""
|
||||||
|
meldungen: list[str] = []
|
||||||
|
by_id = {e.teile_id: e for e in elements}
|
||||||
|
alt = {(e.section_kind, e.key): e
|
||||||
|
for e in load_overrides(csv_name, cfg_dir).edits}
|
||||||
|
|
||||||
|
def ref(teile_id: str) -> PartRef:
|
||||||
|
if not teile_id:
|
||||||
|
return PartRef()
|
||||||
|
element = by_id.get(teile_id.split("-", 1)[0])
|
||||||
|
if element is None or element.position is None:
|
||||||
|
return PartRef(teile_id=teile_id)
|
||||||
|
return PartRef(teile_id=teile_id, x=element.x, y=element.y)
|
||||||
|
|
||||||
|
def pruefen(where: str, type_name: str, slots: dict[str, str],
|
||||||
|
lines: list[str]) -> None:
|
||||||
|
for slot in slot_names(type_name):
|
||||||
|
teile_id = slots.get(slot, "")
|
||||||
|
if not teile_id:
|
||||||
|
if slot.startswith("separator"):
|
||||||
|
meldungen.append(f"{where}: {slot} ist leer")
|
||||||
|
continue
|
||||||
|
element = by_id.get(teile_id.split("-", 1)[0])
|
||||||
|
kinds = SLOT_PART_KINDS[slot_kind(slot)]
|
||||||
|
if element is None:
|
||||||
|
meldungen.append(f"{where}: {slot} = {teile_id} steht nicht im Export")
|
||||||
|
elif element.kind not in kinds:
|
||||||
|
meldungen.append(f"{where}: {slot} = {teile_id} ist ein {element.kind}, "
|
||||||
|
f"erwartet {'/'.join(kinds)}")
|
||||||
|
if has_lines(type_name) and not lines:
|
||||||
|
meldungen.append(f"{where}: Speicher ohne Lagerlinie")
|
||||||
|
|
||||||
|
raw_by_id = {t.tro_id: t for t in raw.tros}
|
||||||
|
edits: list[TroEdit] = []
|
||||||
|
gesehen: set[str] = set()
|
||||||
|
vergeben: dict[str, str] = {}
|
||||||
|
ids: dict[str, str] = {}
|
||||||
|
for m in markers:
|
||||||
|
tro_id, key = m.get("ID", ""), m.get("KEY", "")
|
||||||
|
type_name = m.get("TYPE", "") or m["_BLOCK"][len(BLOCK_PREFIX):]
|
||||||
|
where = f"Marker {tro_id or '?'}"
|
||||||
|
if not tro_id:
|
||||||
|
meldungen.append(f"{where} ({m['_BLOCK']}): ohne ID - uebersprungen")
|
||||||
|
continue
|
||||||
|
if get_tro(type_name) is None:
|
||||||
|
meldungen.append(f"{where}: Typ '{type_name}' steht nicht im Katalog - "
|
||||||
|
f"uebersprungen")
|
||||||
|
continue
|
||||||
|
if problem := id_problem(tro_id):
|
||||||
|
meldungen.append(f"{where}: {problem} - uebersprungen")
|
||||||
|
continue
|
||||||
|
if tro_id in ids:
|
||||||
|
meldungen.append(f"{where}: Kennung doppelt (auch {ids[tro_id]})")
|
||||||
|
ids[tro_id] = key or tro_id
|
||||||
|
slots, lines = parse_slots(m.get("SLOTS", ""))
|
||||||
|
if key in gesehen:
|
||||||
|
# kopierter Marker: gleicher KEY wie ein schon gelesener
|
||||||
|
meldungen.append(f"{where}: KEY {key} doppelt (Marker kopiert?) - als "
|
||||||
|
f"neuer TRO gelesen")
|
||||||
|
key = ""
|
||||||
|
derived = raw_by_id.get(key)
|
||||||
|
if derived is not None:
|
||||||
|
gesehen.add(key)
|
||||||
|
raw_slots, raw_lines = tro_slots(derived)
|
||||||
|
else:
|
||||||
|
raw_slots, raw_lines = {}, []
|
||||||
|
if lines is None:
|
||||||
|
lines = raw_lines if has_lines(type_name) else []
|
||||||
|
pruefen(where, type_name, slots, lines)
|
||||||
|
for teile_id in [v for v in slots.values() if v] + lines:
|
||||||
|
if teile_id in vergeben and vergeben[teile_id] != tro_id:
|
||||||
|
meldungen.append(f"{where}: Bauteil {teile_id} steckt auch in "
|
||||||
|
f"{vergeben[teile_id]}")
|
||||||
|
vergeben[teile_id] = tro_id
|
||||||
|
|
||||||
|
names = slot_names(type_name)
|
||||||
|
if derived is None:
|
||||||
|
edit = TroEdit(key=tro_id, add=True, type_name=type_name,
|
||||||
|
slots={s: ref(slots.get(s, "")) for s in names},
|
||||||
|
lines=[ref(t) for t in lines] if has_lines(type_name) else None)
|
||||||
|
else:
|
||||||
|
edit = TroEdit(key=key)
|
||||||
|
if type_name != derived.type_name:
|
||||||
|
edit.type_name = type_name
|
||||||
|
if tro_id != key:
|
||||||
|
edit.name = tro_id
|
||||||
|
if any(slots.get(s, "") != raw_slots.get(s, "") for s in names):
|
||||||
|
edit.slots = {s: ref(slots.get(s, "")) for s in names}
|
||||||
|
if has_lines(type_name) and lines != raw_lines:
|
||||||
|
edit.lines = [ref(t) for t in lines]
|
||||||
|
if edit.is_empty():
|
||||||
|
continue
|
||||||
|
if (vorher := alt.get((edit.section_kind, edit.key))) is not None:
|
||||||
|
edit.reason = vorher.reason
|
||||||
|
edits.append(edit)
|
||||||
|
meldungen.append(_describe_edit(edit))
|
||||||
|
|
||||||
|
for tro in raw.tros:
|
||||||
|
if tro.tro_id in gesehen or not tro.position:
|
||||||
|
continue
|
||||||
|
edit = TroEdit(key=tro.tro_id, delete=True)
|
||||||
|
if (vorher := alt.get(("edit", tro.tro_id))) is not None:
|
||||||
|
edit.reason = vorher.reason
|
||||||
|
edits.append(edit)
|
||||||
|
meldungen.append(_describe_edit(edit))
|
||||||
|
|
||||||
|
if write:
|
||||||
|
save_edits(csv_name, edits, cfg_dir)
|
||||||
|
return edits, meldungen
|
||||||
|
|
||||||
|
|
||||||
|
def _describe_edit(edit: TroEdit) -> str:
|
||||||
|
if edit.delete:
|
||||||
|
return f"{edit.key}: geloescht"
|
||||||
|
teile = []
|
||||||
|
if edit.add:
|
||||||
|
teile.append(f"neu ({edit.type_name})")
|
||||||
|
elif edit.type_name:
|
||||||
|
teile.append(f"Typ -> {edit.type_name}")
|
||||||
|
if edit.name:
|
||||||
|
teile.append(f"Kennung -> {edit.name}")
|
||||||
|
if edit.slots is not None and not edit.add:
|
||||||
|
teile.append("Bauteile " + ", ".join(
|
||||||
|
f"{k}={r.teile_id or '-'}" for k, r in edit.slots.items()))
|
||||||
|
if edit.lines is not None:
|
||||||
|
teile.append(f"{len(edit.lines)} Lagerlinien")
|
||||||
|
return f"{edit.key}: {'; '.join(teile)}"
|
||||||
|
|
||||||
|
|
||||||
def draw_legend(
|
def draw_legend(
|
||||||
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
|
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
|
||||||
layers: dict[str, str], marker: float, text: float, source: str,
|
layers: dict[str, str], marker: float, text: float, source: str,
|
||||||
@@ -1000,7 +1324,12 @@ def report(
|
|||||||
print(f" Restfehler = max {transform.residual_max:.1f} mm, "
|
print(f" Restfehler = max {transform.residual_max:.1f} mm, "
|
||||||
f"mittel {transform.residual_mean:.1f} mm "
|
f"mittel {transform.residual_mean:.1f} mm "
|
||||||
f"({transform.matched}/{transform.candidates} Punkte)")
|
f"({transform.matched}/{transform.candidates} Punkte)")
|
||||||
print(f"TROs = {len(analysis.tros)}, beschriftet: {counts['marker']}")
|
print(f"TROs = {len(analysis.tros)}, beschriftet: {counts['marker']}"
|
||||||
|
+ (f" (Handkorrekturen: {len(analysis.renamed)} umbenannt, "
|
||||||
|
f"{len(analysis.added)} neu)" if analysis.renamed or analysis.added else ""))
|
||||||
|
if counts.get("parts"):
|
||||||
|
print(f"Bauteile = {counts['parts']} (XRECORD {PARTS_XRECORD}, fuer "
|
||||||
|
f"TRO_EDIT 'Im Plan waehlen'; uebernehmen mit --read-tros)")
|
||||||
print(f"Verbindungen = {counts['flow']}"
|
print(f"Verbindungen = {counts['flow']}"
|
||||||
+ (" (Block CONNECTION_ARROW mit FROM_UID/TO_UID)" if counts["flow"] else ""))
|
+ (" (Block CONNECTION_ARROW mit FROM_UID/TO_UID)" if counts["flow"] else ""))
|
||||||
if counts.get("jams"):
|
if counts.get("jams"):
|
||||||
@@ -1039,6 +1368,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|||||||
" tro_annotate.bat --file dematic_merge.csv --dxf dematic_merge..dxf --flow "
|
" tro_annotate.bat --file dematic_merge.csv --dxf dematic_merge..dxf --flow "
|
||||||
"--anlage dematic_merge_anlage.json\n"
|
"--anlage dematic_merge_anlage.json\n"
|
||||||
" tro_annotate.bat --file dematic_merge.csv --read-jams\n"
|
" tro_annotate.bat --file dematic_merge.csv --read-jams\n"
|
||||||
|
" tro_annotate.bat --file mubea.csv --read-tros --check\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Exit-Codes:\n"
|
"Exit-Codes:\n"
|
||||||
" 0 ok\n"
|
" 0 ok\n"
|
||||||
@@ -1085,6 +1415,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|||||||
"<csv>_annotated.dxf in %%SKEL_RESULTS%%) und nach "
|
"<csv>_annotated.dxf in %%SKEL_RESULTS%%) und nach "
|
||||||
"%%SKEL_CFG%%\\<csv>_jam_names.ini schreiben, dann beenden. "
|
"%%SKEL_CFG%%\\<csv>_jam_names.ini schreiben, dann beenden. "
|
||||||
"scl_skeleton liest die Datei.")
|
"scl_skeleton liest die Datei.")
|
||||||
|
parser.add_argument("--read-tros", nargs="?", const="", default=None, metavar="DXF",
|
||||||
|
help="Im Dialog TRO_EDIT/TRO_INSERT geaenderte, neue und "
|
||||||
|
"geloeschte TRO-Marker aus der beschrifteten Zeichnung "
|
||||||
|
"uebernehmen (Standard: <csv>_annotated.dxf in "
|
||||||
|
"%%SKEL_RESULTS%%) und als Abschnitte edit/edit_add nach "
|
||||||
|
"%%SKEL_CFG%%\\tro_overrides.ini schreiben, dann beenden. "
|
||||||
|
"tro_flow und tro_annotate wenden sie an.")
|
||||||
parser.add_argument("--check", action="store_true",
|
parser.add_argument("--check", action="store_true",
|
||||||
help="Nur pruefen und berichten, keine Datei schreiben.")
|
help="Nur pruefen und berichten, keine Datei schreiben.")
|
||||||
parser.add_argument("--emit-lisp", action="store_true",
|
parser.add_argument("--emit-lisp", action="store_true",
|
||||||
@@ -1125,6 +1462,96 @@ def _main_read_jams(args, csv_file: Path) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _derive(args, csv_file: Path, edits: list[TroEdit] | None):
|
||||||
|
"""CSV lesen und die TROs ableiten - (Elemente, Analysis) oder None.
|
||||||
|
|
||||||
|
Derselbe Aufruf wie in tro_flow.main (mit der Separator-Gegenprobe aus
|
||||||
|
<name>_raw.json, der ID-Datei und den Handkorrekturen), damit die
|
||||||
|
Zeichnung genau die TROs der TRO-JSON zeigt - sie ist nur Ansicht, keine
|
||||||
|
Datenquelle. --read-tros vergleicht mit der Ableitung ohne Korrekturen
|
||||||
|
(edits=None).
|
||||||
|
"""
|
||||||
|
read_warnings: list[str] = []
|
||||||
|
try:
|
||||||
|
elements = read_elements(csv_file, read_warnings)
|
||||||
|
except (OSError, ValueError, csv_module.Error) as exc:
|
||||||
|
print(f"FEHLER: {csv_file} nicht lesbar: {exc}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
graph = build_graph(elements, csv_file.name, read_warnings)
|
||||||
|
raw_json_file = find_sibling_raw_json(csv_file)
|
||||||
|
sepliste_counts = (load_sepliste_counts(raw_json_file, read_warnings)
|
||||||
|
if raw_json_file is not None else {})
|
||||||
|
# Dieselbe ID-Datei wie tro_flow, aber nur lesen: neue TROs bekommen
|
||||||
|
# dieselbe vorlaeufige ID, die tro_flow anhaengen wuerde.
|
||||||
|
ids_file = resolve_ids_file(args.ids, csv_file)
|
||||||
|
try:
|
||||||
|
tro_ids = TroIds.load(ids_file) if ids_file.is_file() else None
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
||||||
|
tro_ids=tro_ids, edits=edits)
|
||||||
|
if not analysis.tros:
|
||||||
|
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
return elements, analysis
|
||||||
|
|
||||||
|
|
||||||
|
def _main_read_tros(args, csv_file: Path) -> int:
|
||||||
|
"""--read-tros: Marker der beschrifteten Zeichnung -> tro_overrides.ini."""
|
||||||
|
results = env_dir("SKEL_RESULTS", "results")
|
||||||
|
cfg_dir = env_dir("SKEL_CFG", "cfg")
|
||||||
|
name = args.read_tros or args.out or f"{csv_file.stem}_annotated.dxf"
|
||||||
|
dxf_file = Path(name)
|
||||||
|
if not dxf_file.is_absolute() and dxf_file.parent == Path("."):
|
||||||
|
dxf_file = results / name
|
||||||
|
if not dxf_file.is_file():
|
||||||
|
print(f"FEHLER: beschriftete Zeichnung nicht gefunden: {dxf_file}", file=sys.stderr)
|
||||||
|
print(" In BricsCAD als DXF speichern (Dateiname wie beim Beschriften).",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if not csv_file.is_file():
|
||||||
|
print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
try:
|
||||||
|
markers = read_markers(dxf_file)
|
||||||
|
except (OSError, ezdxf.DXFError) as exc:
|
||||||
|
print(f"FEHLER: {dxf_file} nicht lesbar: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
derived = _derive(args, csv_file, None)
|
||||||
|
if derived is None:
|
||||||
|
return 1
|
||||||
|
elements, raw = derived
|
||||||
|
# Schutz: eine falsche Zeichnung (ohne oder mit fremden Markern) wuerde
|
||||||
|
# sonst alle TROs als geloescht eintragen.
|
||||||
|
keys = {m.get("KEY", "") for m in markers}
|
||||||
|
if not markers or not keys & {t.tro_id for t in raw.tros}:
|
||||||
|
print(f"FEHLER: {dxf_file.name} enthaelt keine Marker zu den TROs aus "
|
||||||
|
f"{csv_file.name} ({len(markers)} Marker) - nichts geschrieben. Zeichnung "
|
||||||
|
f"mit dem aktuellen tro_annotate neu beschriften?", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
try:
|
||||||
|
edits, meldungen = read_tros(markers, raw, elements, csv_file.name, cfg_dir,
|
||||||
|
write=not args.check)
|
||||||
|
except (OSError, configparser.Error) as exc:
|
||||||
|
print(f"FEHLER: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print("")
|
||||||
|
print("================================================================")
|
||||||
|
print("TRO-HANDKORREKTUREN UEBERNEHMEN")
|
||||||
|
print("================================================================")
|
||||||
|
print(f"Zeichnung = {dxf_file} ({len(markers)} Marker)")
|
||||||
|
ziel = override_path(cfg_dir)
|
||||||
|
print(f"Korrekturen = {len(edits)} -> "
|
||||||
|
f"{'- (--check, nichts geschrieben)' if args.check else ziel}")
|
||||||
|
for m in meldungen:
|
||||||
|
print(f" {m}")
|
||||||
|
print("Danach: tro_flow --json (und tro_annotate) neu laufen lassen.")
|
||||||
|
print("================================================================")
|
||||||
|
print("")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
args = parse_args(argv)
|
args = parse_args(argv)
|
||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
@@ -1145,10 +1572,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
|
|
||||||
if args.read_jams is not None:
|
if args.read_jams is not None:
|
||||||
return _main_read_jams(args, csv_file)
|
return _main_read_jams(args, csv_file)
|
||||||
|
if args.read_tros is not None:
|
||||||
|
return _main_read_tros(args, csv_file)
|
||||||
|
|
||||||
if not args.dxf:
|
if not args.dxf:
|
||||||
print("FEHLER: --dxf fehlt (nur --emit-lisp und --read-jams gehen ohne "
|
print("FEHLER: --dxf fehlt (nur --emit-lisp, --read-jams und --read-tros gehen "
|
||||||
"Zeichnung).", file=sys.stderr)
|
"ohne Zeichnung).", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
if not csv_file.is_file():
|
if not csv_file.is_file():
|
||||||
print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr)
|
print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr)
|
||||||
@@ -1192,33 +1621,13 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
f"nicht zu '{csv_file.name}'.", file=sys.stderr)
|
f"nicht zu '{csv_file.name}'.", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# TROs aus dem CSV
|
# TROs aus dem CSV, mit den Handkorrekturen aus tro_overrides.ini
|
||||||
read_warnings: list[str] = []
|
overrides = load_overrides(csv_file.name, cfg_dir)
|
||||||
try:
|
warnings += [w for w in overrides.warnings if ".edit" in w]
|
||||||
elements = read_elements(csv_file, read_warnings)
|
derived = _derive(args, csv_file, overrides.edits)
|
||||||
except (OSError, ValueError, csv_module.Error) as exc:
|
if derived is None:
|
||||||
print(f"FEHLER: {csv_file} nicht lesbar: {exc}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
graph = build_graph(elements, csv_file.name, read_warnings)
|
|
||||||
# Derselbe Aufruf wie in tro_flow.main (ohne cfg/tro_overrides.ini, mit
|
|
||||||
# der Separator-Gegenprobe aus <name>_raw.json und der ID-Datei), damit die Zeichnung genau
|
|
||||||
# die TROs der TRO-JSON zeigt - sie ist nur Ansicht, keine Datenquelle.
|
|
||||||
raw_json_file = find_sibling_raw_json(csv_file)
|
|
||||||
sepliste_counts = (load_sepliste_counts(raw_json_file, read_warnings)
|
|
||||||
if raw_json_file is not None else {})
|
|
||||||
# Dieselbe ID-Datei wie tro_flow, aber nur lesen: neue TROs bekommen
|
|
||||||
# dieselbe vorlaeufige ID, die tro_flow anhaengen wuerde.
|
|
||||||
ids_file = resolve_ids_file(args.ids, csv_file)
|
|
||||||
try:
|
|
||||||
tro_ids = TroIds.load(ids_file) if ids_file.is_file() else None
|
|
||||||
except (OSError, ValueError) as exc:
|
|
||||||
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
|
||||||
tro_ids=tro_ids)
|
|
||||||
if not analysis.tros:
|
|
||||||
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
|
|
||||||
return 1
|
return 1
|
||||||
|
elements, analysis = derived
|
||||||
|
|
||||||
# Zeichnung
|
# Zeichnung
|
||||||
try:
|
try:
|
||||||
@@ -1274,6 +1683,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
counts["marker"] = len(placed)
|
counts["marker"] = len(placed)
|
||||||
counts["blocks"] = sum(1 for b in doc.blocks if b.name.startswith(BLOCK_PREFIX))
|
counts["blocks"] = sum(1 for b in doc.blocks if b.name.startswith(BLOCK_PREFIX))
|
||||||
counts["first_id"] = next_id
|
counts["first_id"] = next_id
|
||||||
|
counts["parts"] = write_parts(doc, elements, transform)
|
||||||
|
|
||||||
|
|
||||||
if args.flow:
|
if args.flow:
|
||||||
@@ -1285,7 +1695,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
)
|
)
|
||||||
if anlage is not None:
|
if anlage is not None:
|
||||||
counts["jams"] = place_jams(msp, doc, anlage, jam_overrides, transform, layers,
|
counts["jams"] = place_jams(msp, doc, anlage, jam_overrides, transform, layers,
|
||||||
args.text_height, warnings)
|
args.text_height, warnings, marker=args.marker_size)
|
||||||
if args.legend:
|
if args.legend:
|
||||||
draw_legend(msp, analysis, placed, layers, args.marker_size,
|
draw_legend(msp, analysis, placed, layers, args.marker_size,
|
||||||
args.text_height, csv_file.name)
|
args.text_height, csv_file.name)
|
||||||
|
|||||||
+223
-5
@@ -115,7 +115,10 @@ ausschliesslich derive_tros(), Schritt fuer Schritt:
|
|||||||
Alle anderen Funktionen (link_tros, locate_tros, derive_boundaries, ...)
|
Alle anderen Funktionen (link_tros, locate_tros, derive_boundaries, ...)
|
||||||
aendern den Typ nicht mehr. cfg/tro_overrides.ini wird hier seit
|
aendern den Typ nicht mehr. cfg/tro_overrides.ini wird hier seit
|
||||||
2026-10-05 nicht mehr angewendet (siehe tro_overrides.py) - ein falsch
|
2026-10-05 nicht mehr angewendet (siehe tro_overrides.py) - ein falsch
|
||||||
erkannter Typ wird in derive_tros() korrigiert, nicht per INI.
|
erkannter Typ wird in derive_tros() korrigiert, nicht per INI. Ausnahme seit
|
||||||
|
2026-10-26: die Handkorrekturen aus BricsCAD (Abschnitte edit/edit_add, von
|
||||||
|
tro_annotate --read-tros geschrieben) wendet apply_edits nach derive_tros an;
|
||||||
|
sie haengen an den stabilen IDs der ID-Datei.
|
||||||
|
|
||||||
Nicht abgeleitet werden bisher: 1Sep_SSCC (nur Hinweis, Schritt 6),
|
Nicht abgeleitet werden bisher: 1Sep_SSCC (nur Hinweis, Schritt 6),
|
||||||
EmptyCarrBuffer und LoadingBoom. Schritt 3c braucht eine stimmige
|
EmptyCarrBuffer und LoadingBoom. Schritt 3c braucht eine stimmige
|
||||||
@@ -181,12 +184,23 @@ from material_flow import (
|
|||||||
resolve_input,
|
resolve_input,
|
||||||
)
|
)
|
||||||
from kreisel_junctions import Junction, find_junctions, separators_before
|
from kreisel_junctions import Junction, find_junctions, separators_before
|
||||||
from model import Analysis, DerivedTro, LayoutPart, TroDocument
|
from model import (
|
||||||
|
SLOT_PART_KINDS,
|
||||||
|
Analysis,
|
||||||
|
DerivedTro,
|
||||||
|
LayoutPart,
|
||||||
|
TroDocument,
|
||||||
|
has_lines,
|
||||||
|
slot_kind,
|
||||||
|
slot_names,
|
||||||
|
storage_lines,
|
||||||
|
)
|
||||||
from parameter import get_float, get_int
|
from parameter import get_float, get_int
|
||||||
from tro_ids import (
|
from tro_ids import (
|
||||||
DEFAULT_TOLERANCE,
|
DEFAULT_TOLERANCE,
|
||||||
TroIds,
|
TroIds,
|
||||||
assign_ids,
|
assign_ids,
|
||||||
|
id_problem,
|
||||||
ids_path,
|
ids_path,
|
||||||
insert_entries,
|
insert_entries,
|
||||||
other_sections,
|
other_sections,
|
||||||
@@ -203,6 +217,7 @@ from tro_catalog import (
|
|||||||
get_tro,
|
get_tro,
|
||||||
mermaid_classdefs,
|
mermaid_classdefs,
|
||||||
)
|
)
|
||||||
|
from tro_overrides import PartRef, TroEdit, load_overrides
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Konstanten
|
# Konstanten
|
||||||
@@ -2229,12 +2244,197 @@ def locate_tros(tros: list[DerivedTro], elements: list[Element], graph: Graph) -
|
|||||||
tro.position_from = source if tro.position else "keine Position"
|
tro.position_from = source if tro.position else "keine Position"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_part(ref: PartRef, slot: str, elements: list[Element],
|
||||||
|
by_id: dict[str, Element], where: str, tolerance: float,
|
||||||
|
findings: list[str]) -> str:
|
||||||
|
"""TeileId fuer einen Slot der Handkorrektur im aktuellen Export.
|
||||||
|
|
||||||
|
Erst die TeileId selbst (passende Art, Lage innerhalb der Toleranz), sonst
|
||||||
|
das naechste Bauteil derselben Art an der Lage - die TeileId wechselt
|
||||||
|
zwischen zwei Exporten, die Lage nicht. "" wenn nichts passt.
|
||||||
|
"""
|
||||||
|
if not ref.teile_id:
|
||||||
|
return ""
|
||||||
|
kinds = SLOT_PART_KINDS.get(slot_kind(slot), ())
|
||||||
|
base = ref.teile_id.split("-", 1)[0]
|
||||||
|
element = by_id.get(base)
|
||||||
|
if element is not None and element.kind in kinds:
|
||||||
|
if ref.x is None or element.position is None or math.hypot(
|
||||||
|
element.x - ref.x, element.y - ref.y) <= tolerance:
|
||||||
|
return ref.teile_id
|
||||||
|
if ref.x is not None and ref.y is not None:
|
||||||
|
naechste = min(
|
||||||
|
((math.hypot(e.x - ref.x, e.y - ref.y), e) for e in elements
|
||||||
|
if e.kind in kinds and e.position is not None),
|
||||||
|
default=None, key=lambda p: p[0])
|
||||||
|
if naechste is not None and naechste[0] <= tolerance:
|
||||||
|
findings.append(
|
||||||
|
f"{where}: {slot} {ref.teile_id} steht im Export nicht mehr dort - "
|
||||||
|
f"{naechste[1].kind} {naechste[1].teile_id} an derselben Lage "
|
||||||
|
f"uebernommen ({naechste[0]:.0f} mm)")
|
||||||
|
return naechste[1].teile_id
|
||||||
|
findings.append(f"{where}: {slot} {ref.render()} ist kein "
|
||||||
|
f"{'/'.join(kinds) or 'Bauteil'} des Exports - Slot bleibt leer")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _host_of(sep_id: str, by_id: dict[str, Element], graph: Graph) -> str:
|
||||||
|
"""Flussknoten, auf dem ein Separator sitzt (CSV-Spalte 'Zuordnung')."""
|
||||||
|
element = by_id.get(sep_id)
|
||||||
|
host = element.assignment if element is not None else ""
|
||||||
|
return host if host and host != UNASSIGNED and host in graph.nodes else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _set_parts(tro: DerivedTro, edit: TroEdit, elements: list[Element],
|
||||||
|
by_id: dict[str, Element], graph: Graph, tolerance: float,
|
||||||
|
findings: list[str]) -> None:
|
||||||
|
"""Bauteile eines TRO aus den Slots der Handkorrektur setzen.
|
||||||
|
|
||||||
|
Umkehrung von model.derive_slots: separator<i> -> separators, line<n> ->
|
||||||
|
die Lagerlinien am Ende von separators (je Linie ein Host), switch<i> ->
|
||||||
|
switches, scanner<i> -> scanners, drive -> hosts[0].
|
||||||
|
"""
|
||||||
|
where = f"Handkorrektur {edit.key}"
|
||||||
|
slots = edit.slots or {}
|
||||||
|
names = slot_names(tro.type_name)
|
||||||
|
|
||||||
|
def teil(slot: str) -> str:
|
||||||
|
ref = slots.get(slot)
|
||||||
|
return _resolve_part(ref, slot, elements, by_id, where, tolerance,
|
||||||
|
findings) if ref is not None else ""
|
||||||
|
|
||||||
|
alte_seps = list(tro.separators)
|
||||||
|
if edit.slots is not None:
|
||||||
|
seps = [teil(n) for n in names if n.startswith("separator")]
|
||||||
|
while seps and not seps[-1]:
|
||||||
|
seps.pop()
|
||||||
|
tro.switches = [teil(n) for n in names if n.startswith("switch")]
|
||||||
|
tro.scanners = [s for s in (teil(n) for n in names if n.startswith("scanner")) if s]
|
||||||
|
drive = teil("drive") if "drive" in names else ""
|
||||||
|
else:
|
||||||
|
seps = alte_seps[:len(alte_seps) - len(storage_lines(tro))] \
|
||||||
|
if has_lines(tro.type_name) else alte_seps
|
||||||
|
drive = ""
|
||||||
|
|
||||||
|
if has_lines(tro.type_name):
|
||||||
|
if edit.lines is not None:
|
||||||
|
lines = [s for s in (
|
||||||
|
_resolve_part(r, f"line{n}", elements, by_id, where, tolerance, findings)
|
||||||
|
for n, r in enumerate(edit.lines, start=1)) if s]
|
||||||
|
else:
|
||||||
|
lines = storage_lines(tro)
|
||||||
|
seps = [s for s in seps if s and s not in lines]
|
||||||
|
tro.separators = seps + lines
|
||||||
|
# Je Linie ein Host - danach zaehlt model.storage_lines die Linien
|
||||||
|
tro.hosts = [_host_of(s, by_id, graph) or s for s in lines]
|
||||||
|
else:
|
||||||
|
tro.separators = seps
|
||||||
|
if set(seps) != set(alte_seps):
|
||||||
|
hosts = list(dict.fromkeys(h for s in seps if (h := _host_of(s, by_id, graph))))
|
||||||
|
if hosts:
|
||||||
|
tro.hosts = hosts
|
||||||
|
if drive:
|
||||||
|
tro.hosts = [drive] + [h for h in tro.hosts if h != drive]
|
||||||
|
|
||||||
|
|
||||||
|
def apply_edits(tros: list[DerivedTro], edits: list[TroEdit], elements: list[Element],
|
||||||
|
graph: Graph, findings: list[str], analysis: Analysis,
|
||||||
|
tolerance: float = DEFAULT_TOLERANCE) -> list[DerivedTro]:
|
||||||
|
"""Handkorrekturen aus BricsCAD (tro_overrides.ini, edit/edit_add) anwenden.
|
||||||
|
|
||||||
|
Laeuft nach derive_tros (die IDs aus der ID-Datei stehen fest) und vor
|
||||||
|
Verortung und Verknuepfung, damit Koordinaten und Kanten den korrigierten
|
||||||
|
TRO zeigen. Ein Bauteil, das ein korrigierter TRO bekommt, verliert sein
|
||||||
|
bisheriger TRO; bleibt der ohne Separator, entfaellt er (Befund).
|
||||||
|
Umbenennungen kommen zuletzt, die Schluessel bleiben bis dahin die IDs der
|
||||||
|
ID-Datei. Ergebnis in ``analysis.renamed``/``analysis.added``.
|
||||||
|
"""
|
||||||
|
by_id = {e.teile_id: e for e in elements}
|
||||||
|
tros = list(tros)
|
||||||
|
renames: list[tuple[DerivedTro, TroEdit]] = []
|
||||||
|
# Erst loeschen, dann aendern, zuletzt neu anlegen: ein neuer TRO darf die
|
||||||
|
# Bauteile eines geloeschten uebernehmen, ohne dass der als "verdraengt" gilt.
|
||||||
|
for edit in sorted(edits, key=lambda e: 0 if e.delete else 2 if e.add else 1):
|
||||||
|
where = f"Handkorrektur {edit.key}"
|
||||||
|
tro = next((t for t in tros if t.tro_id == edit.key), None)
|
||||||
|
if edit.add:
|
||||||
|
if tro is not None:
|
||||||
|
findings.append(f"{where}: neuer TRO, aber {edit.key} gibt es schon - "
|
||||||
|
f"ignoriert")
|
||||||
|
continue
|
||||||
|
tro = DerivedTro(tro_id=edit.key, type_name=edit.type_name,
|
||||||
|
label=f"von Hand angelegt ({edit.key})", hosts=[],
|
||||||
|
scanners=[], reason="von Hand in BricsCAD angelegt",
|
||||||
|
confidence="bestaetigt")
|
||||||
|
tros.append(tro)
|
||||||
|
analysis.added.append(edit.key)
|
||||||
|
elif tro is None:
|
||||||
|
findings.append(f"{where}: TRO {edit.key} wird nicht (mehr) abgeleitet - "
|
||||||
|
f"Korrektur ignoriert")
|
||||||
|
continue
|
||||||
|
if edit.delete:
|
||||||
|
tros.remove(tro)
|
||||||
|
findings.append(f"{where}: TRO {edit.key} ({tro.type_name}) von Hand "
|
||||||
|
f"entfernt" + (f" - {edit.reason}" if edit.reason else ""))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if edit.type_name and edit.type_name != tro.type_name:
|
||||||
|
findings.append(f"{where}: Typ {tro.type_name} -> {edit.type_name}")
|
||||||
|
tro.type_name = edit.type_name
|
||||||
|
if edit.slots is not None or edit.lines is not None or edit.add:
|
||||||
|
_set_parts(tro, edit, elements, by_id, graph, tolerance, findings)
|
||||||
|
if not edit.add:
|
||||||
|
tro.reason = f"{tro.reason}; Handkorrektur BricsCAD"
|
||||||
|
if edit.reason:
|
||||||
|
tro.reason = f"{tro.reason} ({edit.reason})"
|
||||||
|
tro.confidence = "bestaetigt"
|
||||||
|
|
||||||
|
# Bauteile, die dieser TRO jetzt hat, den anderen TROs nehmen
|
||||||
|
eigene = {p for p in tro.separators + tro.scanners + tro.switches if p}
|
||||||
|
for other in list(tros):
|
||||||
|
if other is tro:
|
||||||
|
continue
|
||||||
|
weg = eigene & set(other.separators + other.scanners + other.switches)
|
||||||
|
if not weg:
|
||||||
|
continue
|
||||||
|
other.separators = [s for s in other.separators if s not in weg]
|
||||||
|
other.scanners = [s for s in other.scanners if s not in weg]
|
||||||
|
other.switches = ["" if s in weg else s for s in other.switches]
|
||||||
|
if other.separators or not other.items.get(TroItem.SEPARATOR):
|
||||||
|
findings.append(f"{where}: {', '.join(sorted(weg))} von {other.tro_id} "
|
||||||
|
f"zu {tro.tro_id} verschoben")
|
||||||
|
else:
|
||||||
|
tros.remove(other)
|
||||||
|
findings.append(f"{where}: {other.tro_id} hat keinen Separator mehr "
|
||||||
|
f"({', '.join(sorted(weg))} an {tro.tro_id}) - entfaellt")
|
||||||
|
if edit.name and edit.name != tro.tro_id:
|
||||||
|
renames.append((tro, edit))
|
||||||
|
|
||||||
|
for tro, edit in renames:
|
||||||
|
if tro not in tros:
|
||||||
|
continue
|
||||||
|
problem = id_problem(edit.name)
|
||||||
|
if not problem and any(t.tro_id == edit.name for t in tros):
|
||||||
|
problem = f"'{edit.name}' ist schon vergeben"
|
||||||
|
if problem:
|
||||||
|
findings.append(f"Handkorrektur {edit.key}: Umbenennung nicht moeglich - "
|
||||||
|
f"{problem}")
|
||||||
|
continue
|
||||||
|
if tro.tro_id in analysis.added:
|
||||||
|
analysis.added[analysis.added.index(tro.tro_id)] = edit.name
|
||||||
|
else:
|
||||||
|
analysis.renamed[edit.name] = tro.tro_id
|
||||||
|
tro.tro_id = edit.name
|
||||||
|
return tros
|
||||||
|
|
||||||
|
|
||||||
def analyse_tros(
|
def analyse_tros(
|
||||||
graph: Graph,
|
graph: Graph,
|
||||||
elements: list[Element],
|
elements: list[Element],
|
||||||
*,
|
*,
|
||||||
sepliste_counts: dict[str, int] | None = None,
|
sepliste_counts: dict[str, int] | None = None,
|
||||||
tro_ids: TroIds | None = None,
|
tro_ids: TroIds | None = None,
|
||||||
|
edits: list[TroEdit] | None = None,
|
||||||
) -> Analysis:
|
) -> Analysis:
|
||||||
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.
|
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.
|
||||||
|
|
||||||
@@ -2250,6 +2450,8 @@ def analyse_tros(
|
|||||||
Roh-JSON (load_sepliste_counts) - nur Gegenprobe.
|
Roh-JSON (load_sepliste_counts) - nur Gegenprobe.
|
||||||
tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu
|
tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu
|
||||||
nummerieren.
|
nummerieren.
|
||||||
|
edits: Handkorrekturen aus BricsCAD (tro_overrides.load_overrides
|
||||||
|
(...).edits), angewandt direkt nach derive_tros (apply_edits).
|
||||||
|
|
||||||
Rueckgabe:
|
Rueckgabe:
|
||||||
Analysis mit tros, boundaries, edges/edge_kinds, unknown_types
|
Analysis mit tros, boundaries, edges/edge_kinds, unknown_types
|
||||||
@@ -2259,6 +2461,11 @@ def analyse_tros(
|
|||||||
analysis = Analysis()
|
analysis = Analysis()
|
||||||
analysis.tros = derive_tros(graph, elements, analysis.findings,
|
analysis.tros = derive_tros(graph, elements, analysis.findings,
|
||||||
sepliste_counts=sepliste_counts, tro_ids=tro_ids)
|
sepliste_counts=sepliste_counts, tro_ids=tro_ids)
|
||||||
|
if edits:
|
||||||
|
analysis.tros = apply_edits(analysis.tros, edits, elements, graph,
|
||||||
|
analysis.findings, analysis,
|
||||||
|
tolerance=tro_ids.tolerance if tro_ids
|
||||||
|
else DEFAULT_TOLERANCE)
|
||||||
analysis.boundaries = derive_boundaries(elements, graph)
|
analysis.boundaries = derive_boundaries(elements, graph)
|
||||||
# Fuer Verortung und Verknuepfung zaehlen TROs und Systemgrenzen zusammen:
|
# Fuer Verortung und Verknuepfung zaehlen TROs und Systemgrenzen zusammen:
|
||||||
# link_tros() registriert eine Kante nur zu einem Knoten, der Host eines
|
# link_tros() registriert eine Kante nur zu einem Knoten, der Host eines
|
||||||
@@ -3281,16 +3488,20 @@ def write_ids_file(tro_ids: TroIds, analysis: Analysis, elements: list[Element],
|
|||||||
if result is None:
|
if result is None:
|
||||||
return "unveraendert"
|
return "unveraendert"
|
||||||
anchors = separator_anchors(elements)
|
anchors = separator_anchors(elements)
|
||||||
|
# Die ID-Datei kennt die TROs unter ihrer abgeleiteten ID: Umbenennungen
|
||||||
|
# und neue TROs aus BricsCAD stehen in tro_overrides.ini, nicht hier.
|
||||||
|
tros = [t.model_copy(update={"tro_id": analysis.edit_key(t.tro_id)})
|
||||||
|
for t in analysis.tros if t.tro_id not in analysis.added]
|
||||||
if result.renumbered:
|
if result.renumbered:
|
||||||
tro_ids.path.parent.mkdir(parents=True, exist_ok=True)
|
tro_ids.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
rest = other_sections(tro_ids.path)
|
rest = other_sections(tro_ids.path)
|
||||||
text = render_ids(analysis.tros, anchors, source)
|
text = render_ids(tros, anchors, source)
|
||||||
tro_ids.path.write_text(text + (f"\n{rest}\n" if rest else ""),
|
tro_ids.path.write_text(text + (f"\n{rest}\n" if rest else ""),
|
||||||
encoding="utf-8")
|
encoding="utf-8")
|
||||||
return "neu geschrieben" if tro_ids.exists else "angelegt"
|
return "neu geschrieben" if tro_ids.exists else "angelegt"
|
||||||
if result.new:
|
if result.new:
|
||||||
insert_entries(tro_ids.path,
|
insert_entries(tro_ids.path,
|
||||||
render_new_entries(analysis.tros, anchors, result.new, source))
|
render_new_entries(tros, anchors, result.new, source))
|
||||||
return f"{len(result.new)} neue(r) TRO(s) angehaengt"
|
return f"{len(result.new)} neue(r) TRO(s) angehaengt"
|
||||||
return "unveraendert"
|
return "unveraendert"
|
||||||
|
|
||||||
@@ -3379,8 +3590,15 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
# Handkorrekturen aus BricsCAD (nur die Arten edit/edit_add, siehe
|
||||||
|
# tro_overrides.py); die uebrigen Abschnitte der Datei gelten hier nicht.
|
||||||
|
overrides = load_overrides(csv_file.name, env_dir("SKEL_CFG", "cfg"))
|
||||||
|
warnings += [w for w in overrides.warnings if ".edit" in w]
|
||||||
|
if overrides.edits and args.renumber:
|
||||||
|
warnings.append(f"--renumber: {len(overrides.edits)} Handkorrektur(en) in "
|
||||||
|
f"{overrides.source} haengen an den alten IDs - pruefen")
|
||||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
||||||
tro_ids=tro_ids)
|
tro_ids=tro_ids, edits=overrides.edits)
|
||||||
if not analysis.tros:
|
if not analysis.tros:
|
||||||
print(f"FEHLER: {csv_file} enthaelt keine Separatoren - keine TROs "
|
print(f"FEHLER: {csv_file} enthaelt keine Separatoren - keine TROs "
|
||||||
f"ableitbar.", file=sys.stderr)
|
f"ableitbar.", file=sys.stderr)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ __all__ = [
|
|||||||
"render_new_entries",
|
"render_new_entries",
|
||||||
"insert_entries",
|
"insert_entries",
|
||||||
"other_sections",
|
"other_sections",
|
||||||
|
"id_problem",
|
||||||
]
|
]
|
||||||
|
|
||||||
SECTION = "tro"
|
SECTION = "tro"
|
||||||
@@ -133,6 +134,15 @@ def _parser() -> configparser.ConfigParser:
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def id_problem(tro_id: str) -> str:
|
||||||
|
"""Warum `tro_id` keine gueltige TRO-ID ist ("" = gueltig)."""
|
||||||
|
if not _ID_PATTERN.match(tro_id):
|
||||||
|
return f"'{tro_id}' ist kein gueltiger SCL-Bezeichner"
|
||||||
|
if _RESERVED.match(tro_id):
|
||||||
|
return f"'{tro_id}' ist fuer Systemgrenzen reserviert"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def load_ids(path: Path) -> list[IdEntry]:
|
def load_ids(path: Path) -> list[IdEntry]:
|
||||||
"""ID-Datei lesen. ValueError bei doppelter ID, falscher Lage oder ID."""
|
"""ID-Datei lesen. ValueError bei doppelter ID, falscher Lage oder ID."""
|
||||||
parser = _parser()
|
parser = _parser()
|
||||||
|
|||||||
+229
-2
@@ -36,6 +36,31 @@ mehrere Anlagen in einer Datei stehen koennen):
|
|||||||
|
|
||||||
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
|
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
|
||||||
|
|
||||||
|
Handkorrekturen aus BricsCAD (seit 2026-10-26)
|
||||||
|
-----------------------------------------------
|
||||||
|
Die Abschnitte oben wendet tro_flow nicht mehr an (siehe unten). Die Arten
|
||||||
|
`edit` und `edit_add` dagegen schon: sie schreibt `tro_annotate --read-tros`
|
||||||
|
aus den im Dialog TRO_EDIT/TRO_INSERT (dxfmakros) geaenderten Markern, und
|
||||||
|
`tro_flow.apply_edits` wendet sie nach der Ableitung an. Sie haengen an der
|
||||||
|
ID aus `<csv>_tro_ids.ini`, die ueber die Lage des Separators stabil bleibt -
|
||||||
|
anders als die laufenden Nummern, an denen die alten Abschnitte veraltet sind.
|
||||||
|
|
||||||
|
[<csv>.edit.<ID>] Korrektur an einem abgeleiteten TRO
|
||||||
|
type = 1Sep1Swi neuer Katalogtyp (fehlt = wie abgeleitet)
|
||||||
|
name = Einschleus_1 neue Kennung (fehlt = <ID> bleibt)
|
||||||
|
delete = true TRO entfaellt
|
||||||
|
separator1 = 0010 @ 1234.5 -567.8 Bauteil je Slot: TeileId @ x y (mm,
|
||||||
|
switch1 = - CSV); "-" = Slot bewusst leer
|
||||||
|
lines = 0066 @ 958 12300, 0069 @ 958 12600 Lagerlinien in Reihenfolge
|
||||||
|
reason = ... Begruendung (bleibt beim Neuschreiben erhalten)
|
||||||
|
|
||||||
|
[<csv>.edit_add.<ID>] neuer TRO (TRO_INSERT), Schluessel wie oben
|
||||||
|
|
||||||
|
Stehen Slots im Abschnitt, gelten sie vollstaendig (fehlende Slots des Typs
|
||||||
|
bleiben leer); stehen keine, bleiben die Bauteile wie abgeleitet. Die Lage
|
||||||
|
hinter der TeileId macht den Eintrag exportfest: passt die TeileId im neuen
|
||||||
|
Export nicht mehr zur Lage, gilt das naechste Bauteil derselben Art dort.
|
||||||
|
|
||||||
Die Felder heissen wie in lib/model.py (`type_name`, `separators`, `hosts`,
|
Die Felder heissen wie in lib/model.py (`type_name`, `separators`, `hosts`,
|
||||||
`reason`, `confidence`); Verbindungen und offene Punkte sind direkt
|
`reason`, `confidence`); Verbindungen und offene Punkte sind direkt
|
||||||
`model.Connection` bzw. `model.TroCandidate`.
|
`model.Connection` bzw. `model.TroCandidate`.
|
||||||
@@ -45,7 +70,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from model import Connection, TroCandidate
|
from model import Connection, TroCandidate
|
||||||
@@ -122,9 +149,84 @@ class MergeSpec:
|
|||||||
confidence: str = DEFAULT_CONFIDENCE
|
confidence: str = DEFAULT_CONFIDENCE
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PartRef:
|
||||||
|
"""Bauteil in einem Slot: TeileId plus Lage (mm, CSV) als Rueckfallebene.
|
||||||
|
|
||||||
|
Die TeileId wechselt zwischen zwei Exporten derselben Anlage, die Lage
|
||||||
|
nicht (wie bei der ID-Datei, lib/tro_ids.py). ``teile_id == ""`` ist ein
|
||||||
|
bewusst leerer Slot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
teile_id: str = ""
|
||||||
|
x: float | None = None
|
||||||
|
y: float | None = None
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
"""'0010 @ 1234.5 -567.8', '0010' ohne Lage, '-' fuer leer."""
|
||||||
|
if not self.teile_id:
|
||||||
|
return "-"
|
||||||
|
if self.x is None or self.y is None:
|
||||||
|
return self.teile_id
|
||||||
|
return f"{self.teile_id} @ {self.x:.1f} {self.y:.1f}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, raw: str) -> PartRef:
|
||||||
|
"""Gegenstueck zu :meth:`render` (ValueError bei falscher Form)."""
|
||||||
|
text = raw.strip()
|
||||||
|
if text in ("", "-"):
|
||||||
|
return cls()
|
||||||
|
teile_id, at, pos = (t.strip() for t in text.partition("@"))
|
||||||
|
if not teile_id or " " in teile_id:
|
||||||
|
raise ValueError(f"'{raw}' ist kein Bauteil '<TeileId> @ x y'")
|
||||||
|
if not at:
|
||||||
|
return cls(teile_id=teile_id)
|
||||||
|
werte = pos.replace(",", " ").split()
|
||||||
|
if len(werte) != 2:
|
||||||
|
raise ValueError(f"'{raw}': Lage hinter @ braucht x und y")
|
||||||
|
return cls(teile_id=teile_id, x=float(werte[0]), y=float(werte[1]))
|
||||||
|
|
||||||
|
|
||||||
|
# Slot-Schluessel in einem edit-Abschnitt (lines steht fuer alle Lagerlinien)
|
||||||
|
_SLOT_KEY = re.compile(r"^(separator|switch|scanner)\d+$|^drive$")
|
||||||
|
_EDIT_KEYS = ("type", "name", "delete", "reason", "lines")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TroEdit:
|
||||||
|
"""Handkorrektur aus BricsCAD (Abschnitt edit bzw. edit_add).
|
||||||
|
|
||||||
|
``key`` ist die ID aus ``<csv>_tro_ids.ini`` (edit) bzw. die Kennung des
|
||||||
|
neuen TRO (edit_add). ``slots``/``lines`` sind None, wenn die Bauteile wie
|
||||||
|
abgeleitet bleiben.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
add: bool = False
|
||||||
|
delete: bool = False
|
||||||
|
name: str = ""
|
||||||
|
type_name: str = ""
|
||||||
|
slots: dict[str, PartRef] | None = None
|
||||||
|
lines: list[PartRef] | None = None
|
||||||
|
reason: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def section_kind(self) -> str:
|
||||||
|
return "edit_add" if self.add else "edit"
|
||||||
|
|
||||||
|
def is_empty(self) -> bool:
|
||||||
|
"""Nichts zu tun (ein edit-Abschnitt ohne Aenderung entfaellt)."""
|
||||||
|
return not (self.add or self.delete or self.name or self.type_name
|
||||||
|
or self.slots is not None or self.lines is not None)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Overrides:
|
class Overrides:
|
||||||
"""Alle Korrekturen zu einer CSV-Datei."""
|
"""Alle Korrekturen zu einer CSV-Datei.
|
||||||
|
|
||||||
|
``edits`` (Handkorrekturen aus BricsCAD) zaehlen fuer ``bool()`` nicht mit:
|
||||||
|
material_flow --connect liest nur die uebrigen Arten.
|
||||||
|
"""
|
||||||
|
|
||||||
source: Path | None = None
|
source: Path | None = None
|
||||||
tros: dict[str, TroOverride] = field(default_factory=dict)
|
tros: dict[str, TroOverride] = field(default_factory=dict)
|
||||||
@@ -133,6 +235,8 @@ class Overrides:
|
|||||||
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
|
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
|
||||||
connections: list[Connection] = field(default_factory=list)
|
connections: list[Connection] = field(default_factory=list)
|
||||||
candidates: list[TroCandidate] = field(default_factory=list)
|
candidates: list[TroCandidate] = field(default_factory=list)
|
||||||
|
# Handkorrekturen aus BricsCAD in Dateireihenfolge (tro_flow.apply_edits)
|
||||||
|
edits: list[TroEdit] = field(default_factory=list)
|
||||||
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
||||||
applied: list[str] = field(default_factory=list)
|
applied: list[str] = field(default_factory=list)
|
||||||
warnings: list[str] = field(default_factory=list)
|
warnings: list[str] = field(default_factory=list)
|
||||||
@@ -294,11 +398,134 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
|||||||
note=values.get("note", "").strip(),
|
note=values.get("note", "").strip(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
elif kind in ("edit", "edit_add"):
|
||||||
|
edit = _parse_edit(section, name, kind == "edit_add", values,
|
||||||
|
result.warnings)
|
||||||
|
if edit is not None:
|
||||||
|
result.edits.append(edit)
|
||||||
else:
|
else:
|
||||||
result.warnings.append(
|
result.warnings.append(
|
||||||
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
|
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
|
||||||
f"'merge' oder 'openpoint'"
|
f"'merge', 'openpoint', 'edit' oder 'edit_add'"
|
||||||
)
|
)
|
||||||
|
|
||||||
result.candidates.sort(key=lambda p: p.name)
|
result.candidates.sort(key=lambda p: p.name)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_edit(section: str, name: str, add: bool, values,
|
||||||
|
warnings: list[str]) -> TroEdit | None:
|
||||||
|
"""Einen edit-/edit_add-Abschnitt lesen (None bei einem Fehler)."""
|
||||||
|
type_name = values.get("type", "").strip()
|
||||||
|
if type_name and get_tro(type_name) is None:
|
||||||
|
warnings.append(f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
||||||
|
f"- Korrektur ignoriert")
|
||||||
|
return None
|
||||||
|
if add and not type_name:
|
||||||
|
warnings.append(f"[{section}]: neuer TRO ohne 'type' - nicht angelegt")
|
||||||
|
return None
|
||||||
|
edit = TroEdit(
|
||||||
|
key=name,
|
||||||
|
add=add,
|
||||||
|
delete=values.get("delete", "").strip().lower() in ("true", "ja", "1", "yes"),
|
||||||
|
name=values.get("name", "").strip(),
|
||||||
|
type_name=type_name,
|
||||||
|
reason=values.get("reason", "").strip(),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
slots = {key: PartRef.parse(raw) for key, raw in values.items()
|
||||||
|
if key not in _EDIT_KEYS}
|
||||||
|
if "lines" in values:
|
||||||
|
edit.lines = [PartRef.parse(t) for t in values["lines"].split(",") if t.strip()]
|
||||||
|
except ValueError as exc:
|
||||||
|
warnings.append(f"[{section}]: {exc} - Korrektur ignoriert")
|
||||||
|
return None
|
||||||
|
falsch = sorted(k for k in slots if not _SLOT_KEY.match(k))
|
||||||
|
if falsch:
|
||||||
|
warnings.append(f"[{section}]: unbekannte Schluessel {', '.join(falsch)} - "
|
||||||
|
f"erwartet type, name, delete, reason, lines oder Slots "
|
||||||
|
f"(separator1, switch1, scanner1, drive)")
|
||||||
|
return None
|
||||||
|
if slots:
|
||||||
|
edit.slots = slots
|
||||||
|
return edit
|
||||||
|
|
||||||
|
|
||||||
|
# Kopfzeile des von --read-tros geschriebenen Blocks; beim Neuschreiben
|
||||||
|
# wird sie samt aller edit-/edit_add-Abschnitte der CSV ersetzt.
|
||||||
|
_EDIT_HEAD = "; --- Handkorrekturen aus BricsCAD zu {csv}"
|
||||||
|
_HEADER = re.compile(r"^\s*\[(.+)\]\s*$")
|
||||||
|
|
||||||
|
|
||||||
|
def render_edit(csv_name: str, edit: TroEdit) -> str:
|
||||||
|
"""Einen Abschnitt als INI-Text."""
|
||||||
|
lines = [f"[{csv_name}.{edit.section_kind}.{edit.key}]"]
|
||||||
|
if edit.delete:
|
||||||
|
lines.append("delete = true")
|
||||||
|
if edit.type_name:
|
||||||
|
lines.append(f"type = {edit.type_name}")
|
||||||
|
if edit.name:
|
||||||
|
lines.append(f"name = {edit.name}")
|
||||||
|
for slot, ref in (edit.slots or {}).items():
|
||||||
|
lines.append(f"{slot} = {ref.render()}")
|
||||||
|
if edit.lines is not None:
|
||||||
|
lines.append(f"lines = {', '.join(r.render() for r in edit.lines)}")
|
||||||
|
if edit.reason:
|
||||||
|
lines.append(f"reason = {edit.reason}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def save_edits(csv_name: str, edits: list[TroEdit], cfg_dir: Path | None = None) -> Path:
|
||||||
|
"""Alle edit-/edit_add-Abschnitte zu `csv_name` durch `edits` ersetzen.
|
||||||
|
|
||||||
|
Der Rest der Datei (von Hand gepflegt, mit Kommentaren) bleibt Zeile fuer
|
||||||
|
Zeile erhalten; der neue Block steht am Ende. Kommentarzeilen direkt vor
|
||||||
|
dem naechsten Abschnitt gehoeren zu diesem und bleiben stehen.
|
||||||
|
"""
|
||||||
|
path = override_path(cfg_dir)
|
||||||
|
text = path.read_text(encoding="utf-8") if path.is_file() else ""
|
||||||
|
prefix = csv_name.lower()
|
||||||
|
head = _EDIT_HEAD.format(csv=csv_name).lower()
|
||||||
|
|
||||||
|
def ist_edit(section: str) -> bool:
|
||||||
|
parts = section.split(".")
|
||||||
|
return (len(parts) >= 3 and parts[-2].lower() in ("edit", "edit_add")
|
||||||
|
and ".".join(parts[:-2]).lower() == prefix)
|
||||||
|
|
||||||
|
def ist_rand(line: str) -> bool:
|
||||||
|
return not line.strip() or line.lstrip().startswith(";")
|
||||||
|
|
||||||
|
keep: list[str] = []
|
||||||
|
skipped: list[str] = []
|
||||||
|
skipping = False
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.strip().lower().startswith(head):
|
||||||
|
continue
|
||||||
|
if m := _HEADER.match(line):
|
||||||
|
if skipping:
|
||||||
|
# Kommentare/Leerzeilen am Ende gehoeren zum naechsten Abschnitt
|
||||||
|
tail: list[str] = []
|
||||||
|
while skipped and ist_rand(skipped[-1]):
|
||||||
|
tail.insert(0, skipped.pop())
|
||||||
|
keep.extend(tail)
|
||||||
|
skipping = ist_edit(m.group(1).strip())
|
||||||
|
skipped = []
|
||||||
|
if skipping:
|
||||||
|
continue
|
||||||
|
if skipping:
|
||||||
|
skipped.append(line)
|
||||||
|
else:
|
||||||
|
keep.append(line)
|
||||||
|
|
||||||
|
while keep and not keep[-1].strip():
|
||||||
|
keep.pop()
|
||||||
|
if edits:
|
||||||
|
if keep:
|
||||||
|
keep += ["", ""]
|
||||||
|
keep.append(_EDIT_HEAD.format(csv=csv_name)
|
||||||
|
+ f" - tro_annotate --read-tros, {datetime.now():%Y-%m-%d}")
|
||||||
|
for edit in edits:
|
||||||
|
keep += [render_edit(csv_name, edit), ""]
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("\n".join(keep).rstrip("\n") + "\n", encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|||||||
Vendored
+1
-1
@@ -138,7 +138,7 @@ REGION TRO18 (PinStore_Auto) Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087,
|
|||||||
#TRO18.stInBarcodeReader.Settings.tTimeout := T#3000ms;
|
#TRO18.stInBarcodeReader.Settings.tTimeout := T#3000ms;
|
||||||
// TODO(E-Planung): Cognex-Interface verdrahten (#arCognexInterface["cScanner____"])
|
// TODO(E-Planung): Cognex-Interface verdrahten (#arCognexInterface["cScanner____"])
|
||||||
|
|
||||||
// TODO(E-Planung): Linienspeicher mit 19 Linien - Parametrierung je Linie
|
// TODO(E-Planung): Linienspeicher mit 20 Linien - Parametrierung je Linie
|
||||||
|
|
||||||
#TRO18.stInPriorityManager.xReleaseOk := TRUE;
|
#TRO18.stInPriorityManager.xReleaseOk := TRUE;
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,25 @@ def test_staubereiche_als_block_mit_name_und_schluessel():
|
|||||||
t2 = jams["TRO02"]
|
t2 = jams["TRO02"]
|
||||||
assert (t2["CONVEYOR"], t2["NUMBER"], t2["ENTRY"], t2["AREA"], t2["AUTO"]) == (
|
assert (t2["CONVEYOR"], t2["NUMBER"], t2["ENTRY"], t2["AREA"], t2["AUTO"]) == (
|
||||||
"0042", "2", "1", "J0102", "0042_2")
|
"0042", "2", "1", "J0102", "0042_2")
|
||||||
# vor dem Separator, Richtung Vorgaenger TRO01 (gegen den Fluss)
|
# direkt vor dem Marker, Richtung Vorgaenger TRO01 (gegen den Fluss):
|
||||||
assert t2["pos"] == (5000 - round(tro_annotate.JAM_VERSATZ), 0)
|
# Markerradius + Luft + halbe Rechteckbreite
|
||||||
# TRO01 hat nur eine Station als Vorgaenger: am Separator
|
abstand = (tro_annotate.DEFAULT_MARKER + tro_annotate.JAM_ABSTAND
|
||||||
assert jams["TRO01"]["pos"] == (0, 0)
|
+ 200.0 * tro_annotate.JAM_HALB_BREITE)
|
||||||
|
assert t2["pos"] == (5000 - round(abstand), 0)
|
||||||
|
# TRO01 hat nur eine Station als Vorgaenger: gegen die Richtung zum Nachfolger
|
||||||
|
assert jams["TRO01"]["pos"] == (-round(abstand), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_staubereich_ist_stehendes_graues_rechteck():
|
||||||
|
doc, _, _ = _zeichnen()
|
||||||
|
blk = doc.blocks.get(tro_annotate.JAM_BLOCK)
|
||||||
|
rahmen = next(iter(blk.query("LWPOLYLINE")))
|
||||||
|
xs = [p[0] for p in rahmen.get_points()]
|
||||||
|
ys = [p[1] for p in rahmen.get_points()]
|
||||||
|
assert max(ys) - min(ys) > 3 * (max(xs) - min(xs))
|
||||||
|
assert rahmen.dxf.color == 8 and len(blk.query("HATCH")) == 1
|
||||||
|
name = next(a for a in blk.query("ATTDEF") if a.dxf.tag == "NAME")
|
||||||
|
assert name.dxf.rotation == 90.0
|
||||||
|
|
||||||
|
|
||||||
def test_korrektur_wird_gezeichnet():
|
def test_korrektur_wird_gezeichnet():
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Tests fuer die Handkorrekturen aus BricsCAD (Dialog TRO_EDIT).
|
||||||
|
|
||||||
|
Weg: Marker der Zeichnung (Attribute KEY/ID/TYPE/SLOTS) -> tro_annotate.read_tros
|
||||||
|
-> Abschnitte edit/edit_add in tro_overrides.ini (tro_overrides.save_edits) ->
|
||||||
|
tro_flow.apply_edits beim naechsten Lauf.
|
||||||
|
|
||||||
|
Fixture: Testanlage test_system (TRO01 3Sep2Swi, TRO02 2Sep1Swi, TRO03 2Sep2Swi,
|
||||||
|
TRO04-06 1Sep1Swi an den Separatoren 0012/0011/0010).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from material_flow import build_graph, read_elements
|
||||||
|
from model import TroDocument, derive_slots
|
||||||
|
from tro_annotate import emit_lisp, format_slots, parse_slots, read_tros, tro_slots
|
||||||
|
from tro_flow import analyse_tros
|
||||||
|
from tro_overrides import PartRef, TroEdit, load_overrides, save_edits
|
||||||
|
|
||||||
|
FIXTURE = Path(__file__).parent / "fixtures" / "test_system_export.csv"
|
||||||
|
CSV = FIXTURE.name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def layout():
|
||||||
|
elements = read_elements(FIXTURE, [])
|
||||||
|
return elements, build_graph(elements, CSV, [])
|
||||||
|
|
||||||
|
|
||||||
|
def _analyse(layout, edits=None):
|
||||||
|
elements, graph = layout
|
||||||
|
return analyse_tros(graph, elements, edits=edits)
|
||||||
|
|
||||||
|
|
||||||
|
def _by_sep(analysis, sep: str):
|
||||||
|
return next(t for t in analysis.tros if sep in t.separators)
|
||||||
|
|
||||||
|
|
||||||
|
def _markers(analysis) -> list[dict[str, str]]:
|
||||||
|
"""Marker, wie tro_annotate.place_markers sie in die Zeichnung schreibt."""
|
||||||
|
return [{"ID": t.tro_id, "KEY": analysis.edit_key(t.tro_id), "TYPE": t.type_name,
|
||||||
|
"SLOTS": format_slots(*tro_slots(t)), "_BLOCK": f"TRO_SYM_{t.type_name}"}
|
||||||
|
for t in analysis.tros]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# INI: lesen und schreiben
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_partref_hin_und_zurueck():
|
||||||
|
ref = PartRef.parse("0010 @ 1234.5 -567.8")
|
||||||
|
assert (ref.teile_id, ref.x, ref.y) == ("0010", 1234.5, -567.8)
|
||||||
|
assert PartRef.parse(ref.render()) == ref
|
||||||
|
assert PartRef.parse("-") == PartRef()
|
||||||
|
assert PartRef.parse("0010").x is None
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
PartRef.parse("0010 @ 1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_overrides_liest_edit_abschnitte(tmp_path):
|
||||||
|
(tmp_path / "tro_overrides.ini").write_text(
|
||||||
|
f"[{CSV}.edit.TRO04]\ntype = 2Sep1Swi\nname = Weiche_K8\n"
|
||||||
|
f"separator1 = 0012 @ 1 2\nswitch1 = -\nreason = Begehung\n\n"
|
||||||
|
f"[{CSV}.edit_add.TRO99]\ntype = 1Sep\nseparator1 = 0011\n\n"
|
||||||
|
f"[{CSV}.edit.TRO05]\ndelete = true\n\n"
|
||||||
|
f"[{CSV}.edit.TRO06]\nweiche = 0023\n\n"
|
||||||
|
f"[{CSV}.edit_add.TRO98]\nseparator1 = 0011\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
overrides = load_overrides(CSV, tmp_path)
|
||||||
|
edits = {e.key: e for e in overrides.edits}
|
||||||
|
assert set(edits) == {"TRO04", "TRO99", "TRO05"}
|
||||||
|
assert edits["TRO04"].name == "Weiche_K8"
|
||||||
|
assert edits["TRO04"].slots == {"separator1": PartRef("0012", 1.0, 2.0),
|
||||||
|
"switch1": PartRef()}
|
||||||
|
assert edits["TRO04"].reason == "Begehung"
|
||||||
|
assert edits["TRO99"].add and edits["TRO99"].lines is None
|
||||||
|
assert edits["TRO05"].delete
|
||||||
|
# unbekannter Schluessel und neuer TRO ohne Typ: gemeldet, nicht angewandt
|
||||||
|
assert len(overrides.warnings) == 2
|
||||||
|
# material_flow --connect sieht die Handkorrekturen nicht
|
||||||
|
assert not overrides
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_edits_ersetzt_nur_die_eigenen_abschnitte(tmp_path):
|
||||||
|
ini = tmp_path / "tro_overrides.ini"
|
||||||
|
ini.write_text(
|
||||||
|
"; Kopf\n\n"
|
||||||
|
f"[{CSV}.tro.TRO03]\ntype = 1Sep\n\n"
|
||||||
|
f"[{CSV}.edit.TRO01]\ntype = 1Sep\n\n"
|
||||||
|
"; Kommentar zum naechsten Abschnitt\n"
|
||||||
|
f"[andere.csv.edit.TRO01]\ntype = 1Sep\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
save_edits(CSV, [TroEdit(key="TRO02", name="Neu"),
|
||||||
|
TroEdit(key="TRO07", add=True, type_name="1Sep",
|
||||||
|
slots={"separator1": PartRef("0010", 5, 6)})], tmp_path)
|
||||||
|
text = ini.read_text(encoding="utf-8")
|
||||||
|
assert f"[{CSV}.edit.TRO01]" not in text
|
||||||
|
assert f"[{CSV}.tro.TRO03]" in text and "[andere.csv.edit.TRO01]" in text
|
||||||
|
assert "; Kommentar zum naechsten Abschnitt\n[andere.csv.edit.TRO01]" in text
|
||||||
|
assert text.index("[andere.csv") < text.index(f"[{CSV}.edit.TRO02]")
|
||||||
|
assert "separator1 = 0010 @ 5.0 6.0" in text
|
||||||
|
assert {e.key for e in load_overrides(CSV, tmp_path).edits} == {"TRO02", "TRO07"}
|
||||||
|
|
||||||
|
# erneut schreiben ersetzt den Block, leer entfernt ihn samt Kopfzeile
|
||||||
|
save_edits(CSV, [TroEdit(key="TRO02", name="Neu")], tmp_path)
|
||||||
|
assert ini.read_text(encoding="utf-8").count("Handkorrekturen aus BricsCAD") == 1
|
||||||
|
save_edits(CSV, [], tmp_path)
|
||||||
|
text = ini.read_text(encoding="utf-8")
|
||||||
|
assert "Handkorrekturen" not in text and f"{CSV}.edit." not in text
|
||||||
|
assert text.startswith("; Kopf\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# tro_flow.apply_edits
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_typ_und_bauteile_aendern_verdraengt_den_nachbarn(layout):
|
||||||
|
raw = _analyse(layout)
|
||||||
|
key = _by_sep(raw, "0012").tro_id
|
||||||
|
verdraengt = _by_sep(raw, "0011").tro_id
|
||||||
|
edit = TroEdit(key=key, type_name="2Sep1Swi", slots={
|
||||||
|
"separator1": PartRef("0012"), "separator2": PartRef("0011"),
|
||||||
|
"switch1": PartRef("0024")})
|
||||||
|
analysis = _analyse(layout, [edit])
|
||||||
|
tro = analysis.by_id(key)
|
||||||
|
assert (tro.type_name, tro.separators, tro.switches) == ("2Sep1Swi", ["0012", "0011"],
|
||||||
|
["0024"])
|
||||||
|
assert tro.confidence == "bestaetigt"
|
||||||
|
assert analysis.by_id(verdraengt) is None
|
||||||
|
assert any(f"{verdraengt} hat keinen Separator mehr" in f for f in analysis.findings)
|
||||||
|
# 2Sep1Swi hat dazu zwei Scanner-Slots, hier leer
|
||||||
|
assert [s.layout_id for s in derive_slots(tro)] == ["0012", "0011", "0024", "", ""]
|
||||||
|
|
||||||
|
|
||||||
|
def test_umbenennen_loeschen_und_neu_anlegen(layout):
|
||||||
|
raw = _analyse(layout)
|
||||||
|
a, b = _by_sep(raw, "0012").tro_id, _by_sep(raw, "0010").tro_id
|
||||||
|
edits = [
|
||||||
|
TroEdit(key=a, name="Weiche_K8"),
|
||||||
|
TroEdit(key="TRO99", add=True, type_name="1Sep",
|
||||||
|
slots={"separator1": PartRef("0010")}),
|
||||||
|
TroEdit(key=b, delete=True),
|
||||||
|
TroEdit(key=_by_sep(raw, "0011").tro_id, name="SYS1"),
|
||||||
|
]
|
||||||
|
analysis = _analyse(layout, edits)
|
||||||
|
assert analysis.by_id("Weiche_K8") is not None and analysis.by_id(a) is None
|
||||||
|
assert analysis.renamed == {"Weiche_K8": a}
|
||||||
|
assert analysis.edit_key("Weiche_K8") == a
|
||||||
|
neu = analysis.by_id("TRO99")
|
||||||
|
assert neu.separators == ["0010"] and neu.position is not None
|
||||||
|
assert analysis.added == ["TRO99"]
|
||||||
|
assert analysis.by_id(b) is None
|
||||||
|
# das Loeschen laeuft vor dem Anlegen: 0010 wird nicht "verdraengt"
|
||||||
|
assert not any("verschoben" in f or "keinen Separator" in f for f in analysis.findings)
|
||||||
|
assert any("Umbenennung nicht moeglich" in f for f in analysis.findings)
|
||||||
|
# die korrigierten TROs laufen durch das Modell der TRO-JSON
|
||||||
|
TroDocument.from_json(TroDocument.from_analysis(analysis, CSV).to_json())
|
||||||
|
|
||||||
|
|
||||||
|
def test_teileid_aus_altem_export_wird_ueber_die_lage_gefunden(layout):
|
||||||
|
elements, _ = layout
|
||||||
|
sep = next(e for e in elements if e.teile_id == "0012")
|
||||||
|
raw = _analyse(layout)
|
||||||
|
key = _by_sep(raw, "0012").tro_id
|
||||||
|
edit = TroEdit(key=key, slots={"separator1": PartRef("9999", sep.x, sep.y),
|
||||||
|
"switch1": PartRef("0024")})
|
||||||
|
analysis = _analyse(layout, [edit])
|
||||||
|
assert analysis.by_id(key).separators == ["0012"]
|
||||||
|
assert any("9999 steht im Export nicht mehr dort" in f for f in analysis.findings)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# tro_annotate.read_tros
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_slots_attribut_hin_und_zurueck():
|
||||||
|
slots, lines = {"separator1": "0010", "switch1": ""}, ["0066", "0069"]
|
||||||
|
assert format_slots(slots, lines) == "separator1=0010;switch1=;lines=0066,0069"
|
||||||
|
assert parse_slots(format_slots(slots, lines)) == (slots, lines)
|
||||||
|
assert parse_slots("separator1=0010") == ({"separator1": "0010"}, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unveraenderte_zeichnung_ergibt_keine_korrektur(layout, tmp_path):
|
||||||
|
elements, _ = layout
|
||||||
|
raw = _analyse(layout)
|
||||||
|
edits, meldungen = read_tros(_markers(raw), raw, elements, CSV, tmp_path)
|
||||||
|
assert edits == []
|
||||||
|
assert not (tmp_path / "tro_overrides.ini").read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dialogaenderungen_werden_zu_korrekturen(layout, tmp_path):
|
||||||
|
elements, _ = layout
|
||||||
|
raw = _analyse(layout)
|
||||||
|
markers = _markers(raw)
|
||||||
|
by_key = {m["KEY"]: m for m in markers}
|
||||||
|
a = _by_sep(raw, "0012").tro_id
|
||||||
|
b = _by_sep(raw, "0010").tro_id
|
||||||
|
by_key[a].update(ID="Weiche_K8", TYPE="2Sep1Swi",
|
||||||
|
SLOTS="separator1=0012;separator2=;switch1=;scanner1=0010")
|
||||||
|
markers.remove(by_key[b])
|
||||||
|
markers.append({"ID": "TRO99", "KEY": "", "TYPE": "1Sep",
|
||||||
|
"SLOTS": "separator1=0010", "_BLOCK": "TRO_SYM_1Sep"})
|
||||||
|
edits, meldungen = read_tros(markers, raw, elements, CSV, tmp_path)
|
||||||
|
by = {(e.section_kind, e.key): e for e in edits}
|
||||||
|
assert set(by) == {("edit", a), ("edit_add", "TRO99"), ("edit", b)}
|
||||||
|
assert by[("edit", a)].name == "Weiche_K8"
|
||||||
|
assert by[("edit", a)].type_name == "2Sep1Swi"
|
||||||
|
assert by[("edit", a)].slots["separator2"] == PartRef()
|
||||||
|
assert by[("edit", a)].slots["switch1"] == PartRef()
|
||||||
|
assert any("scanner1 = 0010 ist ein Separator" in m for m in meldungen)
|
||||||
|
assert by[("edit", b)].delete
|
||||||
|
# Lage aus dem CSV hinter der TeileId
|
||||||
|
assert by[("edit_add", "TRO99")].slots["separator1"].x is not None
|
||||||
|
assert any("separator2 ist leer" in m for m in meldungen)
|
||||||
|
|
||||||
|
# Rundlauf: angewandt, neu beschriftet, wieder gelesen -> dieselben Korrekturen
|
||||||
|
edited = _analyse(layout, load_overrides(CSV, tmp_path).edits)
|
||||||
|
again, _ = read_tros(_markers(edited), raw, elements, CSV, tmp_path, write=False)
|
||||||
|
assert {(e.section_kind, e.key) for e in again} == set(by)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kopierter_marker_wird_neuer_tro(layout, tmp_path):
|
||||||
|
elements, _ = layout
|
||||||
|
raw = _analyse(layout)
|
||||||
|
markers = _markers(raw)
|
||||||
|
kopie = dict(markers[0], ID="TRO77")
|
||||||
|
edits, meldungen = read_tros(markers + [kopie], raw, elements, CSV, tmp_path,
|
||||||
|
write=False)
|
||||||
|
assert [(e.section_kind, e.key) for e in edits] == [("edit_add", "TRO77")]
|
||||||
|
assert any("doppelt" in m for m in meldungen)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Slot-Listen fuer den Dialog
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_emit_lisp_schreibt_slots_je_typ(tmp_path):
|
||||||
|
text = emit_lisp(tmp_path).read_text(encoding="utf-8")
|
||||||
|
assert ('("2Sep1Swi" 0 "separator1" "separator2" "switch1" "scanner1" "scanner2")'
|
||||||
|
in text)
|
||||||
|
assert '("PinStore_Auto" 19 "separator1" "scanner1")' in text
|
||||||
|
assert '("switch" "Ausschleus" "Weiche")' in text
|
||||||
Reference in New Issue
Block a user