- A three- or four-way fitting joined its ports as a star around the port
seen first, so the diameter carried into an unlabelled branch depended on
record order. Every port pair is now bridged; two sizes equally near stay
unassigned whatever the order (six-order parametrised test).
- A reducer was seen only through free ends in its box. It is now also found
where pieces meet at a vertex inside its box (a line broken at the
reducer) and where a straight segment is drawn through the middle of its
box (cut at the foot of the box centre). A direct label holds for its
entity only on its side of every reducer.
- A diameter or direction label nearer to an open line on a layer that is
not read belongs to that line: it is dropped and listed in
stats.labels_skipped instead of landing on the nearest read pipe.
Headless engine only: no new ActiveX call, so no live probe applies.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
build_network reads the segments of a foreign P&ID's piping and electrical
layers into runs: ends merged within tol, a branch ending on a header split
into a tee, a crossing without an end left unconnected, and a line drawn
twice - or a line over a polyline - cut into identical pieces that are kept
once, the doubled length reported as overlap_length. An INSERT holding two to
four free ends is an inline fitting joined through; a reducer word in its
block name (reducer, perekhod, reduksiyon, verloop, reduzier) stops diameter
continuity.
Each diameter label goes to the nearest piece and holds for its drawn entity
(direct); unlabelled pieces take the nearest direct diameter along the run
(continuity) or stay unassigned - two sizes equally near are never guessed.
Labels stay as drawn. A supply or return word near a run overrides its
layer's service for the whole run. Run ends attach to the tag whose labelled
geometry holds them; a shape holding two tags (a room, a skid) is not
equipment. Tests on hand-built drawings and on the synthetic plant pair.
Beyond the plan: a CIP / glycol layer naming no direction (classify_layer
files it `unknown` on purpose) now takes its direction from the word instead
of staying unknown; is_reducer matches on vocab.fold and knows the German
word equipment_kind already had; the fitting, equipment and attachment
lookups go through grid indexes - measured 149 s -> 2.8 s on a 59,200-record
synthetic drawing, results identical to the plan engine on it and on the
plant pair. No ActiveX call is added (pure module).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Track H's foundation, landed on main before its three groups branch.
- engineering/understand/snapshot.py: one bulk read of a drawing into frozen
WCS records (OCS translated through backends/ocs.py, text decoded, an
INSERT boxed by its block geometry without its attribute texts). A DXF is
read with ezdxf after the size check; the headless engine's document is
read under its own lock; the live engine exports the active document with
Document.Export(<tmp>/snap, "DXF", <set of all>) - never renamed, deleted
afterwards - and a DWG is opened read-only, exported and closed. Measured on
AutoCAD 2026: ".dxf" is refused with Invalid argument, the file lands as
snap.DXF, model space, paper space and unused block definitions all reach
it, and the selection set does not narrow a DXF export.
- engineering/understand/labels.py: %%c, \U+2205, the U+2300 sign, o-stroke
and MTEXT format runs all read as one diameter spelling; sizes are kept as
drawn and never equated; Cyrillic look-alike capitals fold into Latin tags;
panels, wiring callouts and electrical data (kW, phases, volts, N, VFD) in
EN / TR / RU / NL / DE. A power that is not written stays None.
- engineering/understand/vocab.py: layer classification with a confidence and
the deciding keyword, supply / return words, room labels and block kinds in
five languages. A CIP layer without a direction word is not guessed.
- MAX_DXF_BYTES defaults to 512 MB (a real layout was 188 MB); the refusal
names the size and the variable. New `office` extra (openpyxl), also in
`full`; uv.lock regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd