fix(task-7): crossings test the exact arc, so a line through a jump's apex counts
flatten_bulges chops a semicircle into an even number of chords (36 for r=50, 12 for r=5), so a flattened vertex lands exactly on the apex — the axis of symmetry, the one place a P&ID line most plausibly meets a crossing jump. segments_cross treats a vertex hit as a junction, so both neighbouring chords dropped it and the arc contributed 0: the (45,0)->(55,0) bulge -1 jump reported crossings 0 at x=50 and 1 at x=50.3 or x=49, the plan's own (0,0)->(100,0) semicircle 0 at x=50, and the mirrored frame (extrusion -Z) 0 at x=-50. count_crossings now takes bulges= (per-chain DXF bulge lists, zero-padded) and tests every bulged edge as its exact arc through the new segment_arc_crossings — circle/segment intersection restricted to the sweep — with the same junction rule as the straight case: a hit within _EPS of the segment's ends, of the arc's own two ends, or a tangent, is not a crossing. Nothing along the sweep is a vertex any more. The arc parametrisation is factored into _arc_of_bulge and shared with flatten_bulges, which keeps its promised signature and its ezdxf pin but no longer feeds the crossing count. draw_line passes the vertices and bulges through unflattened. Pinned in tests: the apex case on the semicircle (x=50, y 20->60), the r=5 jump at x=50 / 50.3 / 49 on +Z and -Z frames, the junction cases (T-touch on the arc, through the arc's endpoint, chord side, tangent along the apex, in-and-out at y=4.9 counts 2), and 4x400 seeded random segment/arc pairs against ezdxf's ConstructionArc.intersect_line (a 20 000-case run in the worktree found 0 mismatches). The eight new assertions fail on the flattened rule and pass on the exact one. Headless finding, headless evidence; no new ActiveX call was written, so the shared live AutoCAD was not touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
9cb4bee587
commit
efcc8bbba1
@@ -467,7 +467,11 @@ def test_payload_helpers_accept_only_well_formed_values():
|
||||
async def test_crossings_follow_a_bulged_line_not_its_chord(backend):
|
||||
"""A foreign P&ID line with a semicircular jump (bulge -1 from (0,0) to
|
||||
(100,0) bows to +Y, apex (50,50)): a vertical utility line at x=30 from
|
||||
y=20 to y=60 crosses the arc once at y≈45.8 and its chord never."""
|
||||
y=20 to y=60 crosses the arc once at y≈45.8 and its chord never — and so
|
||||
does one at x=50, straight through the apex. The apex is the axis of
|
||||
symmetry, where a flattened semicircle (an even chord count) puts a
|
||||
vertex; a vertex hit is a junction to ``segments_cross``, so a chord
|
||||
chain reported 0 there. The arc is tested exactly, not flattened."""
|
||||
msp = backend._msp()
|
||||
msp.add_lwpolyline(
|
||||
[(0, 0, 0, 0, -1.0), (100, 0, 0, 0, 0)],
|
||||
@@ -480,10 +484,39 @@ async def test_crossings_follow_a_bulged_line_not_its_chord(backend):
|
||||
assert lines[0]["bulges"] == [-1.0, 0.0]
|
||||
result = await draw_line(backend, {"x": 30, "y": 20}, {"x": 30, "y": 60}, line_class="utility")
|
||||
assert result["crossings"] == 1
|
||||
apex = await draw_line(backend, {"x": 50, "y": 20}, {"x": 50, "y": 60}, line_class="utility")
|
||||
assert apex["crossings"] == 1, "through the apex — the flattened vertex — is still one crossing"
|
||||
below = await draw_line(backend, {"x": 70, "y": -20}, {"x": 70, "y": -60}, line_class="utility")
|
||||
assert below["crossings"] == 0, "the chord side of the arc is empty"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("extrusion", [(0, 0, 1), (0, 0, -1)])
|
||||
async def test_crossings_count_a_line_through_a_jump_apex(backend, extrusion):
|
||||
"""The jump a P&ID draws where two lines cross: (45,0)→(55,0) bulge -1, a
|
||||
5 mm semicircle with its apex at (50,5). A line at x=50 meets the apex
|
||||
exactly (36 chords of r=50, 12 of r=5: always even, always a vertex on
|
||||
the axis) and is one crossing, the same as at x=50.3 or x=49 — on a
|
||||
mirrored frame (extrusion -Z, WCS x negated) too."""
|
||||
msp = backend._msp()
|
||||
msp.add_lwpolyline(
|
||||
[(0, 0, 0, 0, 0), (45, 0, 0, 0, -1.0), (55, 0, 0, 0, 0), (100, 0, 0, 0, 0)],
|
||||
format="xyseb",
|
||||
dxfattribs={"layer": "PROCESS-PIPING-MAIN", "extrusion": extrusion},
|
||||
)
|
||||
backend._mark_dirty()
|
||||
sign = extrusion[2]
|
||||
for x in (50.0, 50.3, 49.0):
|
||||
drawn = await draw_line(
|
||||
backend, {"x": sign * x, "y": -20}, {"x": sign * x, "y": 20}, line_class="utility"
|
||||
)
|
||||
assert drawn["crossings"] == 1, f"x={sign * x}"
|
||||
await backend.entity_delete(drawn["handle"])
|
||||
beside = await draw_line(
|
||||
backend, {"x": sign * 60, "y": 2}, {"x": sign * 60, "y": 20}, line_class="utility"
|
||||
)
|
||||
assert beside["crossings"] == 0, "clear of the jump and above the straight run"
|
||||
|
||||
|
||||
async def test_com_existing_lines_carry_bulges(monkeypatch):
|
||||
payload = line_payload("process_major", "100-P-7", "100", "P", None, None, 7, None, None)
|
||||
poly = _com_pid_polyline("3E", (0.0, 0.0, 100.0, 0.0), payload)
|
||||
|
||||
@@ -206,6 +206,72 @@ def test_flatten_bulges_matches_ezdxf_flattening(bulge):
|
||||
assert mine[len(mine) // 2] == pytest.approx(reference[len(reference) // 2])
|
||||
|
||||
|
||||
def test_crossings_test_the_exact_arc_so_a_flattened_vertex_hides_nothing():
|
||||
"""(45,0)→(55,0) bulge -1 is the r=5 jump; its apex (50,5) is a vertex of
|
||||
the flattened chain (12 chords, even), so the chord chain drops a line at
|
||||
x=50 from both neighbours. The exact arc counts it once — as it does at
|
||||
x=50.3 — and keeps the junction rule everywhere it belongs."""
|
||||
from engineering.pid.lines import count_crossings, flatten_bulges, segment_arc_crossings
|
||||
|
||||
jump, bulges = [(45.0, 0.0), (55.0, 0.0)], [-1.0, 0.0]
|
||||
flat = flatten_bulges(jump, bulges)
|
||||
assert (50.0, 5.0) in flat, "the apex is a flattened vertex — the case that was missed"
|
||||
assert count_crossings([(50, -20), (50, 20)], [flat]) == 0, "chords: the hit is a vertex"
|
||||
assert count_crossings([(50, -20), (50, 20)], [jump], bulges=[bulges]) == 1
|
||||
assert count_crossings([(50.3, -20), (50.3, 20)], [jump], bulges=[bulges]) == 1
|
||||
assert count_crossings([(50, 20), (50, 60)], [[(0, 0), (100, 0)]], bulges=[[-1.0, 0.0]]) == 1
|
||||
# Junctions stay junctions: the segment ending on the arc, a line through
|
||||
# the arc's own endpoint (the polyline's vertex), the empty chord side,
|
||||
# and a tangent along the apex that touches without crossing.
|
||||
assert segment_arc_crossings((50, 5), (50, 20), *jump, -1.0) == 0
|
||||
assert segment_arc_crossings((45, -5), (45, 5), *jump, -1.0) == 0
|
||||
assert segment_arc_crossings((50, -20), (50, -1), *jump, -1.0) == 0
|
||||
assert segment_arc_crossings((40, 5), (60, 5), *jump, -1.0) == 0
|
||||
assert segment_arc_crossings((40, 4.9), (60, 4.9), *jump, -1.0) == 2, "in and out again"
|
||||
assert segment_arc_crossings((50, -20), (50, 20), *jump, 1.0) == 1, "bulge +1 bows to -Y"
|
||||
assert segment_arc_crossings((50, -20), (50, 20), *jump, 0.0) == 1, "bulge 0 is the chord"
|
||||
# A short or missing bulge list is zero-padded: straight edges as before.
|
||||
assert count_crossings([(5, -5), (5, 5)], [[(0, 0), (10, 0), (10, 10)]], bulges=[[]]) == 1
|
||||
assert count_crossings([(5, -5), (5, 5)], [[(0, 0), (10, 0)]], bulges=None) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", range(4))
|
||||
def test_segment_arc_crossings_match_ezdxf_arc_line_intersection(seed):
|
||||
"""Pinned against ezdxf's own ``ConstructionArc.intersect_line`` (hits
|
||||
strictly inside the segment) over random segments and bulges of either
|
||||
sign, on both sides of a semicircle."""
|
||||
import random
|
||||
|
||||
from ezdxf.math import ConstructionArc, ConstructionLine, Vec2, bulge_to_arc
|
||||
|
||||
from engineering.pid.lines import segment_arc_crossings
|
||||
|
||||
rng = random.Random(seed)
|
||||
checked = 0
|
||||
for _ in range(400):
|
||||
a = (rng.uniform(-50, 50), rng.uniform(-50, 50))
|
||||
b = (rng.uniform(-50, 50), rng.uniform(-50, 50))
|
||||
if math.dist(a, b) < 1.0:
|
||||
continue
|
||||
magnitude = rng.choice([0.2, 0.5, 1.0, 1.5, 2.5, rng.uniform(0.05, 3.0)])
|
||||
bulge = rng.choice([-1.0, 1.0]) * magnitude
|
||||
p1 = (rng.uniform(-80, 80), rng.uniform(-80, 80))
|
||||
p2 = (rng.uniform(-80, 80), rng.uniform(-80, 80))
|
||||
centre, s_rad, e_rad, radius = bulge_to_arc(Vec2(a), Vec2(b), bulge)
|
||||
arc = ConstructionArc(
|
||||
center=centre,
|
||||
radius=radius,
|
||||
start_angle=math.degrees(s_rad),
|
||||
end_angle=math.degrees(e_rad),
|
||||
)
|
||||
d = Vec2(p2) - Vec2(p1)
|
||||
hits = arc.intersect_line(ConstructionLine(Vec2(p1), Vec2(p2)), abs_tol=1e-9)
|
||||
expected = sum(1 for h in hits if 1e-6 < (h - Vec2(p1)).dot(d) / d.dot(d) < 1 - 1e-6)
|
||||
assert segment_arc_crossings(p1, p2, a, b, bulge) == expected, (p1, p2, a, b, bulge)
|
||||
checked += 1
|
||||
assert checked > 350
|
||||
|
||||
|
||||
def test_flatten_bulges_passes_straight_edges_through_and_pads_short_bulge_lists():
|
||||
from engineering.pid.lines import flatten_bulges
|
||||
|
||||
|
||||
Reference in New Issue
Block a user