Each TRO marker gains a UID attribute: a unique, permanent four-digit number in
the same space as the TeileId of the CSV export and the ID attribute of SSG_LIB
(dxfmakros/Lisp/ssg_id.lsp). The visible ID (TRO03) is untouched - it stays the
speaking name that TRO_EDIT and TRO_INSERT work with. The UID is a separate tag
on purpose: IDSCHECK treats ID as numeric and would overwrite TRO03 with a number
while correcting duplicates.
Numbers are allocated above the highest already in use, taken from two sources -
the ID attributes of the drawing and the TeileId of every CSV object. The second
source is the one that matters here: a mechanical drawing not built with SSG_LIB
carries no ID attributes at all (in the Mubea plan only 10 of 669 blocks have any
attributes), so without it the run would start at 0001 and take numbers the CSV
already uses for components. With it the markers start at 0062, above TeileId
0061.
With --flow each edge now also gets a CONNECTION_ARROW block at the midpoint of
the arrow, rotated along it, holding FROM_UID/TO_UID plus FROM_TRO/TO_TRO as
readable text and KIND. That makes the relationship extractable from a DXF export
and stable when a marker is moved or renamed. The shaft and head stay separate
geometry: a block with fixed geometry cannot span two arbitrary points, and
scaling it in X would distort the arrowhead.
Block name prefix CONNECTION_ so the SSG_BLOCKEDIT dispatcher in dxfmakros picks
it up via "CONNECTION_*".
Verified on the real drawing: 10 markers with UIDs 0062-0071, 29 connections with
UIDs 0072-0100, every FROM_UID/TO_UID matching its marker, all 39 UIDs unique.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The TRO edit dialog belongs with the other SSG_LIB edit commands, so
cad/tro_edit.lsp and cad/tro_edit.dcl move to dxfmakros (Lisp/Tro_Edit.lsp,
dcl/tro_edit.dcl) where they use the ssg_* helpers, the DXFM_DCL path and the
existing SSG_BLOCKEDIT dispatcher instead of standing on their own. Keeping a
second copy here would only let the two drift apart before the repos are merged.
--emit-lisp now writes the generated type list to DXFM_LISP when that variable is
set, so it lands next to the SSG_LIB modules that read it. Without the variable it
still writes to cad/ and says where the file actually belongs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the generic marker with one shape per type, so the types are
distinguishable in the drawing without relying on colour. The shape belongs to
the type, so it sits in tro_catalog next to the colour - colour groups (6 groups),
shape identifies (10 types):
1Sep circle 1Sep1Swi triangle 1Sep2Swi pentagon
1Sep_SSCC 2 circles Vario rectangle Vario_workStation hexagon
PinStore_Auto square EmptyCarrBuffer diamond
LoadingBoom arrow 2Sep1Swi triangle down
The visible attributes are now ID and TYPE, as those are what the dialog edits;
FB_BLOCK stays visible under --fb and ITEMS/CONFIDENCE/SEPARATORS stay hidden
data. Note this renames the former TRO_ID/TRO_TYPE tags, so drawings annotated
with an earlier version need regenerating.
Marker layers now take the *stroke* colour instead of the fill. A CAD symbol is
line work, and the pastel fills of the palette were nearly invisible as lines -
LoadingBoom in particular came out almost white on white.
cad/tro_edit.dcl and cad/tro_edit.lsp add the TROEDIT command: pick a TRO_SYM_*
block, edit ID and TYPE, write back on OK. TYPE is a picklist rather than free
text so it cannot drift from the catalogue; the list comes from cad/tro_types.lsp,
which "tro_annotate.py --emit-lisp" generates from TRO_CATALOG. If that file is
missing the dialog degrades to the block's current type instead of failing.
The dialog deliberately changes attributes only. The marker shape belongs to the
block definition of the type and FB_BLOCK is derived from it, so both follow on
the next annotation run - the dialog says so, and TROEDIT prints a reminder when
the type was changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Takes the original DXF, the CSV export and the derived TROs and writes a copy of
the drawing with the TRO information overlaid. The original is never touched.
Per TRO a block marker with attributes, not loose geometry: one block definition
per type (TRO_SYM_<type>), inserted at the plant coordinate, carrying TRO_ID,
TRO_TYPE, FB_BLOCK visibly and ITEMS, CONFIDENCE, SEPARATORS as hidden data.
That makes the result queryable and editable inside BricsCAD, and a corrected
TRO_ID can later feed back into the generator.
Layers follow the colour groups of tro_catalog (TRO_MARKER_sep, _vario, _store,
...) with the group colour as true colour and entities BYLAYER, so the layer
manager filters by TRO type. Flow arrows and the legend get their own layers.
Re-runs are idempotent through XDATA, not layer names: every generated entity is
tagged with the SPS_SKEL_TRO appid and a re-run deletes exactly those. Deleting
by layer would also destroy anything a user had put on the same layer.
Switches follow the other tools; --check reports the registration and what would
be drawn without writing, --flow adds the TRO-level arrows, --fb shows the
function block, --offset and --re-register override the stored transform. DWG is
rejected with a clear message since ezdxf cannot read it.
Verified against the real 23.8 MB drawing: 9 markers on the correct layers with
readable attributes, 15 flow arrows, and a re-run replaces its own 46 entities
instead of duplicating them.
Adds ezdxf to requirements and stores the verified Mubea transform in cfg/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CSV export and the BricsCAD drawing sit in different coordinate systems. A
silent offset here puts every annotation in the wrong place while still looking
plausible, so this module derives the transform, proves it, and refuses when it
cannot.
Approach: vote for the translation over point pairs, refine the best candidates
by Procrustes fit (rotation included), pick the one with the most exact matches,
then report the residuals.
Two things the naive version got wrong, both fixed here:
Matching is constrained by role. The plant is built from regular grids - the
store lines, their infeeds and their outfeeds each form a row of 20 points at
800 mm pitch. Voting over all points is therefore ambiguous: a wrong offset hits
the infeed row exactly as well as the right one hits the separator row, and it
won by a single vote (21 to 20), producing an offset 8.6 m out with a spurious
0.52 degree rotation. Separators are now matched only against separator blocks
(S-SP, S-LP) and scanners only against SCAN, which makes the result unique.
The fit uses exact matches only (50 mm). For extended objects the CSV reports the
bounding-box centre while the block reports its insertion point; such pairs are
100-500 mm apart and would tilt the fit if they were included. They still count
towards the reported residuals.
For the Mubea plant this yields dx +50905.7 mm, dy +8414.5 mm, rotation exactly
0, from 20 exact matches - the same values a hand analysis produced.
A verified transform is stored in SKEL_CFG/dxf_registration.json per CSV/DXF
pair, so later runs are reproducible; a stored transform is still re-checked
against the drawing on every run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The new export fills Hoehe_Von_mm / Hoehe_Bis_mm, so the direction of a transport
element no longer has to be guessed from its neighbours. drive_uphill() now
decides in this order:
1. Hoehe_Von_mm -> Hoehe_Bis_mm: the rise of the element itself. Only the sign
is read, since the values are absolute on a Gefaellestrecke (1941 -> 1466)
but relative on a Strecke (0 -> 474).
2. Antriebfahrtrichtung "Auf" (up) or "Ab" (down).
3. A Gefaellestrecke without either runs downhill by definition.
Previously a Gefaellestrecke was assumed downhill outright and everything else
depended on Antriebfahrtrichtung alone; now both are backed by the height data
and the fallback order is explicit.
The node label states what was read and what follows from it, e.g.
"0 -> 474 mm (+474) = aufwaerts", so the arrow can be checked against the export
without opening the CSV.
Adds a plausibility check for the case where Antriebfahrtrichtung and the height
change disagree. The height wins and the conflict is reported, rather than one of
the two silently deciding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Until now the two lanes of a Kreisel were joined by a pair of edges in both
directions, because the travel direction was unknown. The CSV does carry it in
the Drehrichtung merkmal, so the arrow inside a Kreisel is now directed:
UZS clockwise -> material runs right to left, R -> L
GUZ counter-clockwise -> material runs left to right, L -> R
If the merkmal is missing or holds anything else, the circuit stays bidirectional
and that is reported as a warning, so an unknown value cannot silently invent a
direction.
The direction is visible in the drawing: the edge carries the rotation as its
label and the Kreisel cluster header states it in words, e.g.
"Kreisel Kreisel3 [0009] UZS (rechts nach links)".
Since a Kreisel is now traversed one way only, lanes can end up without an
outgoing or without an incoming connection. build_graph reports both cases,
naming the separators that sit on the affected lane, because such a lane means
carriers can either not leave or not reach it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a --use-cords switch to tro_flow.py (--use-coords accepted too). With it,
every TRO node carries its plant coordinate as a pos attribute and the SVG is
produced by "neato -n" instead of dot, so the diagram looks like the plant
layout rather than a computed left-to-right flow.
Scale is 0.25 pt/mm (COORD_SCALE), which turns the ~11 x 20 m plant into roughly
2800 x 5000 pt - large enough that the TRO boxes do not overlap. Without the
switch the pos attributes stay in the DOT as information only, at 1:100, and dot
ignores them; the default layout is unchanged.
Two details neato -n needs: the legend gets a position of its own, placed below
left of the plant bounding box, because with -n a node without pos would land at
0,0 in the middle of the drawing. And the switch is refused with a clear message
if any TRO has no coordinate, since -n cannot lay out a graph with missing
positions.
Verified that neato -n reproduces the given coordinates: the positions it reports
back differ from the input by a constant offset only (spread 0.08 pt, from
Graphviz moving the drawing into the positive quadrant), and no node distance
changes by more than 0.08 pt over spans of up to 3600 pt.
graph_to_svg() in material_flow.py replaces dot_to_svg() to take an engine and
the no-op flag; dot_to_svg() stays as a thin wrapper. It now also treats output
on stderr as an error, so neato's "node has no position" warnings do not pass
silently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sister program to material_flow.py with the same switches, but --tosvg draws the
flow diagram of the TROs instead of the individual objects. It reuses
read_elements and build_graph, so CSV parsing and direction logic exist once.
A TRO is the control logic of one hand-over point, derived from the separator and
what is attached to it, following doc/500573_Mubea/TRO_Identifikation_500573.md:
Gefaellestrecke, >= 2 lines sharing feed/discharge -> PinStore_Auto per group
Gefaellestrecke, single line -> 1Sep
Strecke (driven) -> Vario per segment
Kreisel lane -> 1Sep
A scanner at the separator deliberately does not change the type: in connect.ini
separators with a scanner are predominantly 1Sep, and 1Sep_SSCC is reserved for
the SSCC/end-measurement/WCS case, which a mechanical layout cannot reveal.
Affected points are reported as hints instead. A host with 2 or 3 outgoing flow
edges upgrades 1Sep to 1Sep1Swi/1Sep2Swi.
Connectivity contracts passive nodes: material runs separator to separator, so a
carousel lane carrying no TRO is traversed rather than treated as a dead end.
Without that, a lone separator on a spur loop would be reported as an orphan
purely as an artifact of the model. Each TRO must connect to at least one other;
otherwise an error file is written and the exit code is 3.
Each TRO also carries a plant coordinate - the centroid of the components it was
built from, so a 1Sep1Swi sits between its separator and its switch. Written into
the DOT as a pos attribute (1:100), which dot ignores and "neato -n" can use to
draw the diagram to scale.
Types, function blocks, components and colours come exclusively from
lib/tro_catalog.py.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lib/tro_catalog.py is the single source for everything needed to create a TRO.
It reads no files at runtime; the origin of the data is recorded in comments
only, with CATALOG_AS_OF naming the revision it was taken from.
A TRO type is described by four things:
name short type as used in FB_Main and connect.ini ("1Sep")
fb_block the Siemens function block ("FB_ILS_MTRO_1Sep")
items which components sit inside it and how many
style colour group for drawings
TroDefinition is a pydantic v2 model with validate_assignment, so the get_*/set_*
methods cannot leave an object in an invalid state, and model_dump()/
model_validate() come for free for the planned JSON layout model.
Component lists for the four locally vendored FBs are verified against their VAR
declarations; the rest are .liblink references and follow the composition table
in TRO_Typen.md. Two findings from that comparison, recorded in the comments:
FB_ILS_MTRO_2Sep1Swi also declares two barcode readers, which the doc table
omits, and FB_EmptyCarrBuffer has no separator of its own - its base is a
separate 1Sep TRO (TRO101_IN next to TRO101 in the reference plant).
Colours follow the "Color legend" of the catalog: green means storage, the Vario
is purple. TroStyle renders both Graphviz attributes and Mermaid classDefs, so
the palette exists once and is ready for the BricsCAD symbol.
Enables pydantic in requirements.txt accordingly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reads the CSV export of the plant objects (ILS 2.0) from SKEL_DATA and builds a
directed graph of the material flow, written as Graphviz DOT and optionally SVG
into SKEL_RESULTS.
Node model: one node per Gefaellestrecke and Strecke; a Kreisel becomes two
nodes (-L / -R) wired into a circle, since it is a carousel with one lane per
travel direction. Separators and scanners are no nodes of their own; they are
shown on the label of the object they are assigned to.
Edge direction is not in the CSV - the Nachbarn column is symmetric. It is
derived from geometry instead: a Gefaellestrecke runs downhill away from its
highest neighbour, a driven Strecke follows Antriebfahrtrichtung ("Auf" uphill,
"Ab" downhill). Connections that stay ambiguous, e.g. between two Kreisel at the
same height, are drawn as bidirectional edges and reported.
The lane suffix of a Kreisel is always stated in the *partner's* row, so both
viewpoints of a link are merged before orienting it.
Switches:
--file name of the CSV in SKEL_DATA, or a full path
--tosvg render the DOT via Graphviz
--doc write a Markdown documentation of the plant objects
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>