From b1a031f40e860f425594aa2384ad93a474875945 Mon Sep 17 00:00:00 2001 From: Paul Fremantle Date: Sat, 3 Oct 2026 19:39:42 +0100 Subject: [PATCH 1/7] Render single pattern pitch without redundant multiplier --- src/draftwright/annotations/holes.py | 7 ++++--- src/draftwright/evaluation/_pocket_evidence.py | 3 ++- tests/test_hole_pattern_callouts.py | 14 +++++++------- tests/test_pocket_pattern.py | 2 +- tests/test_pocket_pattern_completeness_evidence.py | 2 +- tests/test_slot_completeness.py | 8 ++++---- tests/test_slot_pattern.py | 2 +- 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/draftwright/annotations/holes.py b/src/draftwright/annotations/holes.py index c7346a2ab..22c919315 100644 --- a/src/draftwright/annotations/holes.py +++ b/src/draftwright/annotations/holes.py @@ -824,8 +824,9 @@ def _place_pitch_dim( ctx, ): """Pitch dimension between two hole-centre *locations* ``loc1``→``loc2``, labelled - ``(n-1)× pitch``, placed just outside the view on the side of the row's - outward perpendicular (#92). *feature* attributes it to the source pattern (#408).""" + ``(n-1)× pitch`` for repeated gaps or just ``pitch`` for one gap. Place it outside + the view on the row's outward perpendicular (#92). *feature* attributes it to the + source pattern (#408).""" p1 = to_page(loc1) p2 = to_page(loc2) ux, uy = p2[0] - p1[0], p2[1] - p1[1] @@ -860,7 +861,7 @@ def _place_pitch_dim( # A uniform array's ± applies to each identical gap, so `4× 20 ±0.05` is coherent; that is # unlike the STEP representative, whose levels merely fall within 10% of each other, where # a ± would claim the tolerance of values that differ. - label = f"{n - 1}× {pitch_text}" + label = f"{n - 1}× {pitch_text}" if n > 2 else pitch_text def _make(off, side_vec=side, label_offset_x=0.0): return _dim( diff --git a/src/draftwright/evaluation/_pocket_evidence.py b/src/draftwright/evaluation/_pocket_evidence.py index 1cb5d8d53..0f0638f63 100644 --- a/src/draftwright/evaluation/_pocket_evidence.py +++ b/src/draftwright/evaluation/_pocket_evidence.py @@ -506,7 +506,8 @@ def _pocket_pattern_drawing_outcomes(patterns, drawing) -> list[Outcome]: round(gap, drawing.draft.decimal_precision) == nominal for gap in gaps ): suffix = _tol_suffix(approved.tolerance, drawing.draft) - pitch_labels[(feature, parameter)] = f"{intervals}× {approved.value_text}{suffix}" + prefix = f"{intervals}× " if intervals > 1 else "" + pitch_labels[(feature, parameter)] = f"{prefix}{approved.value_text}{suffix}" location_labels: dict[tuple[object, str], str] = {} for approved in plan.locations: diff --git a/tests/test_hole_pattern_callouts.py b/tests/test_hole_pattern_callouts.py index 37ce2ebc7..fda1a005a 100644 --- a/tests/test_hole_pattern_callouts.py +++ b/tests/test_hole_pattern_callouts.py @@ -299,7 +299,7 @@ def test_four_corner_rectangle_states_two_pitches_without_a_bolt_circle(self): annotation.label for name, annotation in drawing.iter_annotations() if name.startswith("dim_pitch_") - } == {"1× 20", "1× 30"} + } == {"20", "30"} assert drawing.lint() == [] @pytest.mark.parametrize("variant", ("a", "b")) @@ -315,7 +315,7 @@ def test_diagonal_square_grid_pitches_survive_the_exact_ink_gate(self, variant): grid_pitches = [ annotation for name, annotation in drawing.iter_annotations() - if name.startswith("dim_pitch_plan") and annotation.label == "1× 22.6" + if name.startswith("dim_pitch_plan") and annotation.label == "22.6" ] assert len(grid_pitches) == 2 assert not any(issue.code == "hole_pattern_dim_dropped" for issue in drawing.lint()) @@ -336,9 +336,9 @@ def test_rect_grid_one_callout_and_two_pitch_dims(self): assert len(hc) == 1, f"expected one grouped callout, got {hc}" assert named[hc[0]].covers_count == 8 assert named[hc[0]].covers_diameters == (8.0,) - # both grid pitch dimensions, labelled (n-1)× pitch + # Both grid pitches survive; one gap prints its value without a multiplier. assert len(pitch) == 2, f"expected two pitch dims, got {pitch}" - assert {named[n].label for n in pitch} == {"1× 20", "3× 25"} + assert {named[n].label for n in pitch} == {"20", "3× 25"} # each dim runs ALONG one lattice axis — its endpoints share a coordinate # — not diagonally across the grid; and the two are perpendicular. axes = set() @@ -373,8 +373,8 @@ def test_rect_grid_pitch_dims_not_diagonal_when_rotated(self): dim = dwg.get_annotation(n) sp = dim.placement_spec span = math.hypot(sp.p2[0] - sp.p1[0], sp.p2[1] - sp.p1[1]) / scale - k, p = dim.label.split("× ") - expected = int(k) * float(p) + parts = dim.label.split("× ") + expected = float(parts[0]) if len(parts) == 1 else int(parts[0]) * float(parts[1]) assert abs(span - expected) < 1.0, ( f"{n} ({dim.label!r}) endpoint span {span:.1f} ≠ {expected:.1f} — drawn diagonally" ) @@ -398,7 +398,7 @@ def test_x_axis_rect_grid_keeps_both_pitches_in_the_side_view(self): assert len(pitch) == 2, f"expected two side-grid pitch dims, got {pitch}" assert {dwg.view_of(name) for name in pitch} == {"side"} - assert {dwg.get_annotation(name).label for name in pitch} == {"1× 10", "4× 45"} + assert {dwg.get_annotation(name).label for name in pitch} == {"10", "4× 45"} assert "hole_pattern_dim_dropped" not in {issue.code for issue in dwg.lint()} @pytest.mark.timeout(120) diff --git a/tests/test_pocket_pattern.py b/tests/test_pocket_pattern.py index 3fc6eb51f..8cf588572 100644 --- a/tests/test_pocket_pattern.py +++ b/tests/test_pocket_pattern.py @@ -72,7 +72,7 @@ def test_grid_pattern_renders_both_pitch_dims(): "6× 8 × 8 × 4 DEEP" ] pitch_labels = sorted(dwg.get_annotation(n).label for n in names if "pitch" in n) - assert pitch_labels == ["1× 30", "2× 40"] # (rows-1)× row_pitch, (cols-1)× col_pitch + assert pitch_labels == ["2× 40", "30"] # one gap reads directly; repeated gaps retain count assert not [x for x in dwg.lint() if x.code == "annotation_out_of_bounds"] diff --git a/tests/test_pocket_pattern_completeness_evidence.py b/tests/test_pocket_pattern_completeness_evidence.py index f4b74ff22..acfdb59f5 100644 --- a/tests/test_pocket_pattern_completeness_evidence.py +++ b/tests/test_pocket_pattern_completeness_evidence.py @@ -552,7 +552,7 @@ def test_legitimate_pitch_tolerance_survives_exact_drawing_observation() -> None for name in drawing.registry.names() if name.startswith("dim_pocketpat_pitch") } - assert labels == {"1× 32 ±0.2", "2× 24 ±0.2"} + assert labels == {"32 ±0.2", "2× 24 ±0.2"} assert _pocket_pattern_drawing_outcomes( tuple(drawing.recognition().section_recess_patterns), drawing ) == ["supported"] diff --git a/tests/test_slot_completeness.py b/tests/test_slot_completeness.py index 8c75585fe..7b7ca9645 100644 --- a/tests/test_slot_completeness.py +++ b/tests/test_slot_completeness.py @@ -35,10 +35,10 @@ def test_ctc_left_slot_position_survives_grid_pitch_carve(monkeypatch): for row in (0, 1) for col in (0, 1) } == { - "1× 90", - "1× 320", - "1× 350", - "1× 650", + "90", + "320", + "350", + "650", } assert { drawing.get_annotation(name).label diff --git a/tests/test_slot_pattern.py b/tests/test_slot_pattern.py index 3eeeb9650..5c072c65f 100644 --- a/tests/test_slot_pattern.py +++ b/tests/test_slot_pattern.py @@ -105,7 +105,7 @@ def test_grid_pattern_accounts_for_both_pitch_dimensions(): "6× SLOT 6 × 12" ] pitch_labels = sorted(dwg.get_annotation(n).label for n in names if "slotpat_pitch" in n) - assert pitch_labels == ["1× 30"] + assert pitch_labels == ["30"] pattern = next(feature for feature in dwg.model().features if feature.kind == "slot_pattern") assert [ parameter.value for parameter in pattern.parameters() if parameter.role == "grid_pitch" From bf7e2a26fb746cb760d918b6903aea2ce9cb695e Mon Sep 17 00:00:00 2001 From: Paul Fremantle Date: Sat, 3 Oct 2026 19:41:20 +0100 Subject: [PATCH 2/7] Keep invalid gap counts explicit in pitch labels --- src/draftwright/annotations/holes.py | 2 +- src/draftwright/evaluation/_pocket_evidence.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/draftwright/annotations/holes.py b/src/draftwright/annotations/holes.py index 22c919315..24c2368d4 100644 --- a/src/draftwright/annotations/holes.py +++ b/src/draftwright/annotations/holes.py @@ -861,7 +861,7 @@ def _place_pitch_dim( # A uniform array's ± applies to each identical gap, so `4× 20 ±0.05` is coherent; that is # unlike the STEP representative, whose levels merely fall within 10% of each other, where # a ± would claim the tolerance of values that differ. - label = f"{n - 1}× {pitch_text}" if n > 2 else pitch_text + label = pitch_text if n == 2 else f"{n - 1}× {pitch_text}" def _make(off, side_vec=side, label_offset_x=0.0): return _dim( diff --git a/src/draftwright/evaluation/_pocket_evidence.py b/src/draftwright/evaluation/_pocket_evidence.py index 0f0638f63..b399a4715 100644 --- a/src/draftwright/evaluation/_pocket_evidence.py +++ b/src/draftwright/evaluation/_pocket_evidence.py @@ -506,7 +506,7 @@ def _pocket_pattern_drawing_outcomes(patterns, drawing) -> list[Outcome]: round(gap, drawing.draft.decimal_precision) == nominal for gap in gaps ): suffix = _tol_suffix(approved.tolerance, drawing.draft) - prefix = f"{intervals}× " if intervals > 1 else "" + prefix = "" if intervals == 1 else f"{intervals}× " pitch_labels[(feature, parameter)] = f"{prefix}{approved.value_text}{suffix}" location_labels: dict[tuple[object, str], str] = {} From c454616b5af541f2fff06ba1bfecad40bada5ab0 Mon Sep 17 00:00:00 2001 From: Paul Fremantle Date: Sat, 3 Oct 2026 19:45:31 +0100 Subject: [PATCH 3/7] Keep pitch formatter within function length ratchet --- src/draftwright/annotations/holes.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/draftwright/annotations/holes.py b/src/draftwright/annotations/holes.py index 24c2368d4..d584ad204 100644 --- a/src/draftwright/annotations/holes.py +++ b/src/draftwright/annotations/holes.py @@ -823,10 +823,9 @@ def _place_pitch_dim( *, ctx, ): - """Pitch dimension between two hole-centre *locations* ``loc1``→``loc2``, labelled - ``(n-1)× pitch`` for repeated gaps or just ``pitch`` for one gap. Place it outside - the view on the row's outward perpendicular (#92). *feature* attributes it to the - source pattern (#408).""" + """Pitch between hole centres: ``pitch`` for one gap, else ``(n-1)× pitch``. + Place it outside the view on the row's outward perpendicular (#92); *feature* + attributes it to the source pattern (#408).""" p1 = to_page(loc1) p2 = to_page(loc2) ux, uy = p2[0] - p1[0], p2[1] - p1[1] From df897b3d1fc02e08144fd7ce4a4a8f6bcfa9148a Mon Sep 17 00:00:00 2001 From: Paul Fremantle Date: Sat, 3 Oct 2026 19:55:17 +0100 Subject: [PATCH 4/7] Keep numeric pitch helper inputs as text --- src/draftwright/annotations/holes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/draftwright/annotations/holes.py b/src/draftwright/annotations/holes.py index d584ad204..e15f559c9 100644 --- a/src/draftwright/annotations/holes.py +++ b/src/draftwright/annotations/holes.py @@ -860,7 +860,7 @@ def _place_pitch_dim( # A uniform array's ± applies to each identical gap, so `4× 20 ±0.05` is coherent; that is # unlike the STEP representative, whose levels merely fall within 10% of each other, where # a ± would claim the tolerance of values that differ. - label = pitch_text if n == 2 else f"{n - 1}× {pitch_text}" + label = str(pitch_text) if n == 2 else f"{n - 1}× {pitch_text}" def _make(off, side_vec=side, label_offset_x=0.0): return _dim( From 3887cc99c53dc251cf1c7e25e0d35d50d364ef31 Mon Sep 17 00:00:00 2001 From: Paul Fremantle Date: Sat, 3 Oct 2026 23:01:46 +0100 Subject: [PATCH 5/7] Refresh single-gap grid golden label and bounds --- tests/refactor_golden/grid_plate.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/refactor_golden/grid_plate.json b/tests/refactor_golden/grid_plate.json index d0115f35c..1a1a5e149 100644 --- a/tests/refactor_golden/grid_plate.json +++ b/tests/refactor_golden/grid_plate.json @@ -43,11 +43,11 @@ 149.4, 215.5 ], - "label": "1\u00d7 20", + "label": "20", "label_bbox": [ - 130.4, + 142.0, 212.4, - 139.1, + 145.2, 214.6 ], "name": "dim_pitch_plan0_1", From 1a4c84c012ccba0841992e3ca3e748d062856412 Mon Sep 17 00:00:00 2001 From: Paul Fremantle Date: Sat, 3 Oct 2026 23:06:02 +0100 Subject: [PATCH 6/7] Recognize bare single-gap pitch in hole pattern evidence --- .../evaluation/_hole_family_evidence.py | 4 +++- ...1370_hole_pattern_completeness_evidence.py | 21 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/draftwright/evaluation/_hole_family_evidence.py b/src/draftwright/evaluation/_hole_family_evidence.py index 5d379d49d..501850e34 100644 --- a/src/draftwright/evaluation/_hole_family_evidence.py +++ b/src/draftwright/evaluation/_hole_family_evidence.py @@ -443,7 +443,7 @@ def rendered_group_count(outcome) -> bool: return False def rendered_interval_count(outcome, expected: int) -> bool: - """Whether the exact pitch dimension renders its required interval multiplier.""" + """Whether the exact pitch dimension renders its required interval count.""" for name, annotation in drawing.registry.iter_named(): owns_pitch = any( getattr(measurement, "feature", None) in outcome.features @@ -455,6 +455,8 @@ def rendered_interval_count(outcome, expected: int) -> bool: label = getattr(annotation, "label", None) or getattr( annotation, "_annotate_label", None ) + if expected == 1 and re.match(r"^\s*\d+(?:\.\d+)?(?:\s|$)", str(label or "")): + return True match = re.match(r"^\s*(\d+)\s*[×x]\s", str(label or "")) if match is not None and int(match.group(1)) == expected: return True diff --git a/tests/test_issue_1370_hole_pattern_completeness_evidence.py b/tests/test_issue_1370_hole_pattern_completeness_evidence.py index 94b096414..4f0bd8c60 100644 --- a/tests/test_issue_1370_hole_pattern_completeness_evidence.py +++ b/tests/test_issue_1370_hole_pattern_completeness_evidence.py @@ -169,9 +169,8 @@ def with_wrong_pitch_ink(*args, **kwargs): assert names, "fixture must place pitch dimensions" for name in names: dimension = drawing.registry.named(name) - prefix, _nominal = dimension.label.split(" ", 1) - assert prefix.endswith("×") - dimension.label = f"{prefix} 9999 WRONG" + assert dimension.label in {"20", "2× 15"} + dimension.label = "9999 WRONG" return drawing monkeypatch.setattr(builder, "build_drawing", with_wrong_pitch_ink) @@ -218,20 +217,20 @@ def with_wrong_compound_nominals(*args, **kwargs): def test_wrong_placed_grid_interval_count_loses_drawing_credit(monkeypatch) -> None: - import re - import draftwright.builder as builder original = builder.build_drawing def with_wrong_interval_count(*args, **kwargs): drawing = original(*args, **kwargs) - names = [name for name in drawing.annotations() if name.startswith("dim_pitch_")] - assert names, "fixture must place pitch dimensions" - for name in names: - dimension = drawing.registry.named(name) - assert re.match(r"^\d+× ", dimension.label) - dimension.label = re.sub(r"^\d+× ", "9× ", dimension.label) + pitches = [ + drawing.registry.named(name) + for name in drawing.annotations() + if name.startswith("dim_pitch_") + ] + assert {pitch.label for pitch in pitches} == {"20", "2× 15"} + single_gap = next(pitch for pitch in pitches if pitch.label == "20") + single_gap.label = "9× 20" return drawing monkeypatch.setattr(builder, "build_drawing", with_wrong_interval_count) From 785968c7c9314a8547396d4a0b133ea833dd4121 Mon Sep 17 00:00:00 2001 From: Paul Fremantle Date: Sat, 3 Oct 2026 23:12:22 +0100 Subject: [PATCH 7/7] Keep interval grammar outside the pattern outcome loop --- .../evaluation/_hole_family_evidence.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/draftwright/evaluation/_hole_family_evidence.py b/src/draftwright/evaluation/_hole_family_evidence.py index 501850e34..4507d5b6a 100644 --- a/src/draftwright/evaluation/_hole_family_evidence.py +++ b/src/draftwright/evaluation/_hole_family_evidence.py @@ -399,6 +399,15 @@ def _pattern_model_outcomes(patterns, recognition, features) -> list[Outcome]: return result +def _pitch_interval_label_matches(label: object, expected: int) -> bool: + """A single gap has a bare value; longer spans state their interval count.""" + text = str(label or "") + if expected == 1: + return re.match(r"^\s*\d+(?:\.\d+)?(?:\s|$)", text) is not None + match = re.match(r"^\s*(\d+)\s*[×x]\s", text) + return match is not None and int(match.group(1)) == expected + + def _pattern_drawing_outcomes(patterns, drawing) -> list[Outcome]: """Per recognised pattern: did its grouping grammar reach the placed drawing?""" from draftwright.linting.evidence import verify_measurement_claims @@ -455,10 +464,7 @@ def rendered_interval_count(outcome, expected: int) -> bool: label = getattr(annotation, "label", None) or getattr( annotation, "_annotate_label", None ) - if expected == 1 and re.match(r"^\s*\d+(?:\.\d+)?(?:\s|$)", str(label or "")): - return True - match = re.match(r"^\s*(\d+)\s*[×x]\s", str(label or "")) - if match is not None and int(match.group(1)) == expected: + if _pitch_interval_label_matches(label, expected): return True return False