fix(task-5): fittings bridge every port pair, reducers found however the pipe meets them, unread-layer lines keep their labels

- 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
This commit is contained in:
Umut CAN
2026-09-24 12:08:47 +03:00
co-authored by Claude Opus 5.5
parent 1426c53fd3
commit 7bfba02ef0
2 changed files with 260 additions and 61 deletions
+99
View File
@@ -245,6 +245,105 @@ def test_continuity_stops_at_a_reducer():
assert result["stats"]["reducers"] == 1
def test_continuity_stops_at_a_reducer_the_line_is_broken_at():
# BREAK-at-point: two LINEs share a vertex inside the reducer's box, no free end there
result = net(
line("1", (0.0, 0.0), (1030.0, 0.0)),
line("2", (1030.0, 0.0), (2000.0, 0.0)),
insert("R", "REDUCER_51_38", (1000.0, -30.0, 1060.0, 30.0)),
text("D", "Ø51", (400.0, 5.0)),
)
by_handle = {edge["handles"]: edge for edge in result["runs"][0].edges}
assert by_handle[("1",)]["diameter"] == "Ø51"
assert by_handle[("2",)]["diameter_source"] == "unassigned"
assert (result["stats"]["fittings"], result["stats"]["reducers"]) == (1, 1)
def test_a_polyline_drawn_through_a_reducer_is_cut_there():
poly = rec("P", "LWPOLYLINE", PIPE, ((0.0, 0.0), (2000.0, 0.0)))
result = net(
poly,
line("BR", (1500.0, 0.0), (1500.0, 800.0)),
insert("R", "REDUCER_51_38", (1000.0, -30.0, 1060.0, 30.0)),
text("D", "Ø51", (400.0, 5.0)),
)
(run,) = result["runs"]
# cut at the foot of the box centre (1030, 0): 1030 + 470 + 500 + 800 branch
assert abs(total(run) - 2800.0) < EPS
by_span = {(edge["a"][0], edge["b"][0], edge["handles"]): edge for edge in run.edges}
labelled = by_span[(0.0, 1030.0, ("P",))]
assert (labelled["diameter"], labelled["diameter_source"]) == ("Ø51", "direct")
# the label holds for the polyline only up to the reducer; nothing crosses it
others = [edge for edge in run.edges if edge is not labelled]
assert len(others) == 3
assert {edge["diameter_source"] for edge in others} == {"unassigned"}
assert result["stats"]["reducers"] == 1
def test_a_pipe_running_beside_a_reducer_is_not_cut():
# a parallel pipe grazing the reducer's box edge does not pass through its middle
result = net(
line("1", (0.0, 30.0), (2000.0, 30.0)),
insert("R", "REDUCER_51_38", (1000.0, -30.0, 1060.0, 30.0)),
text("D", "Ø51", (400.0, 35.0)),
)
(run,) = result["runs"]
assert len(run.edges) == 1
assert run.edges[0]["diameter_source"] == "direct"
assert result["stats"]["reducers"] == 0
@pytest.mark.parametrize("order", ["ABC", "BAC", "CAB", "ACB", "BCA", "CBA"])
def test_a_three_way_valve_guesses_nothing_whatever_the_record_order(order):
# C leaves the valve 1000 from A (Ø51) and 1000 from B (DN20): equally near
records = {
"A": line("A", (0.0, 0.0), (1000.0, 0.0)),
"B": line("B", (1100.0, 0.0), (2100.0, 0.0)),
"C": line("C", (1050.0, 50.0), (1050.0, 1050.0)),
}
result = net(
*[records[name] for name in order],
insert("V", "3WAY_VALVE", (1000.0, -50.0, 1100.0, 50.0)),
text("TA", "Ø51", (400.0, 5.0)),
text("TB", "DN20", (1500.0, 5.0)),
)
(run,) = result["runs"]
c = next(edge for edge in run.edges if edge["handles"] == ("C",))
assert (c["diameter"], c["diameter_source"]) == (None, "unassigned")
assert [u["reason"] for u in result["unassigned"]] == ["two different diameters equally near"]
assert run.ends == ((0.0, 0.0), (1050.0, 1050.0), (2100.0, 0.0))
def test_a_label_nearer_to_a_pipe_on_an_unread_layer_is_not_taken():
# the label stands 5 below the WATER line and 22.5 above PRODUCT (search 25)
drawing = snap(
line("1", (0.0, 0.0), (1000.0, 0.0), PIPE),
line("W", (0.0, 30.0), (1000.0, 30.0), "WATER"),
text("D", "DN20", (400.0, 20.0)),
)
assert "WATER" not in default_layers(drawing)
result = build_network(drawing)
assert result["runs"][0].edges[0]["diameter_source"] == "unassigned"
assert result["stats"]["labels_skipped"] == [
{
"handle": "D",
"text": "DN20",
"reason": "nearer to a line on layer WATER, which is not read",
}
]
def test_an_equipment_outline_does_not_take_a_pipe_label():
# closed outlines are shapes, not pipes: a label beside the pipe end stays the pipe's
result = net(
tank("A", "T4100", (-500.0, -500.0, 0.0, 500.0)),
line("1", (0.0, 0.0), (1000.0, 0.0)),
text("D", "Ø51", (2.0, 5.0)),
)
edge = result["runs"][0].edges[0]
assert (edge["diameter"], edge["diameter_source"]) == ("Ø51", "direct")
def test_continuity_crosses_a_valve():
result = net(
line("1", (0.0, 0.0), (1000.0, 0.0)),