fix(task-14): per-space resolution, mirrored inserts, tags above the box, per-axis scale

Closes the four verified BLOCKING findings on pid_graph.

1. scope="all" attached across layouts: the port grid, bbox candidates,
   vertex grid, junctions and text lookup were pooled from every space.
   The grid is now keyed by (space, cell) and every lookup filters by the
   edge's space; junctions carry `space`.
2. A mirrored INSERT (extrusion -Z, as MIRROR3D / foreign DXFs store it)
   got its catalogue ports on the unmirrored image at 1.0. Both engines
   report `mirrored` on INSERT properties (ezdxf: extrusion z < 0; COM:
   Normal z < 0; default False); transform_port takes `mirrored` and
   applies x -> -x about the WCS insertion point after rotation/scale,
   direction (180 - d) mod 360 — cross-checked against Insert.matrix44.
   resolve_endpoint passes it through too, so draw_line starts on the
   mirror image as well.
3. The text-tag fallback searched a full circle around the box top, so a
   line label or note below the symbol beat the tag above it. Candidates
   must now sit at or above top - height/2 (spec §9.2 step 6), nearest
   wins, and line-number texts are never equipment tags.
4. A non-uniform INSERT scale re-ran the transform with the X factor and
   set `inferred` (reserved for foreign-block ports). transform_port
   gains `strict=False` for the reader: point ports scale per axis
   exactly; a radial port keeps min(sx, sy)·r and the node drops to 0.6
   with notes ["non-uniform scale"]. Node dicts gain `y_scale`,
   `mirrored` and `notes` (default []).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-16 10:35:20 +03:00
co-authored by Claude Opus 5
parent ea47cbdd7e
commit 6ad3734c47
8 changed files with 388 additions and 50 deletions
+27
View File
@@ -171,6 +171,33 @@ async def test_mirrored_symbol_ports_follow_the_mirrored_geometry(backend):
assert (ep["x"], ep["y"], ep["direction_deg"]) == (100.0, 194.0, 270.0)
async def test_an_extrusion_mirrored_symbol_resolves_its_port_on_the_mirror_image(backend):
"""MIRROR3D (and foreign DXFs) store a mirror as extrusion -Z rather than
a negative scale; the port is where the mirrored geometry is."""
from ezdxf.math import Vec3
pump = await place_symbol(backend, "centrifugal_pump", 50, 20, rotation=30.0, tag="P-1")
ent = backend._doc.entitydb.get(pump["handle"])
ent.dxf.extrusion = (0, 0, -1)
ent.dxf.insert = (-50, 20, 0) # the same WCS insertion point
truth = ent.matrix44().transform(Vec3(0, 6, 0))
ep = await resolve_endpoint(backend, {"handle": pump["handle"], "port": "discharge"})
assert (ep["x"], ep["y"]) == pytest.approx((truth.x, truth.y))
assert (ep["x"], ep["y"]) == pytest.approx((53.0, 25.196), abs=1e-3)
assert ep["direction_deg"] == pytest.approx(60.0)
# unrotated: the suction that faced -x at (46, 20) now faces +x at (54, 20)
pump = await place_symbol(backend, "centrifugal_pump", 150, 20, tag="P-2")
ent = backend._doc.entitydb.get(pump["handle"])
ent.dxf.extrusion = (0, 0, -1)
ent.dxf.insert = (-150, 20, 0)
ep = await resolve_endpoint(backend, {"handle": pump["handle"], "port": "suction"})
assert (ep["x"], ep["y"], ep["direction_deg"]) == pytest.approx((154.0, 20.0, 0.0))
result = await draw_line(
backend, {"handle": pump["handle"], "port": "suction"}, {"x": 200, "y": 20}
)
assert result["vertices"][0] == pytest.approx([154.0, 20.0])
async def test_stretched_symbol_is_refused_not_resolved_off_the_geometry(backend):
pump = await place_symbol(backend, "centrifugal_pump", 100, 100, tag="P-1")
payload = await read_payload(backend, pump["handle"])