draftwright 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. {draftwright-0.3.0 → draftwright-0.3.1}/CHANGELOG.md +30 -0
  2. {draftwright-0.3.0 → draftwright-0.3.1}/PKG-INFO +3 -1
  3. {draftwright-0.3.0 → draftwright-0.3.1}/pyproject.toml +7 -1
  4. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/__init__.py +4 -4
  5. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/_core.py +40 -4
  6. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/analysis.py +13 -13
  7. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/annotations/_common.py +74 -7
  8. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/annotations/from_model.py +778 -688
  9. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/annotations/holes.py +948 -798
  10. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/annotations/orchestrator.py +61 -55
  11. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/annotations/sections.py +11 -14
  12. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/builder.py +7 -7
  13. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/cli.py +7 -13
  14. draftwright-0.3.0/src/draftwright/sheet.py → draftwright-0.3.1/src/draftwright/compose.py +5 -2
  15. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/drawing.py +400 -234
  16. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/export.py +21 -0
  17. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/linting/coverage.py +32 -3
  18. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/registry.py +5 -0
  19. draftwright-0.3.0/src/draftwright/sheet_dsl.py → draftwright-0.3.1/src/draftwright/sheet.py +6 -1
  20. draftwright-0.3.1/src/draftwright/sheet_dsl.py +35 -0
  21. draftwright-0.3.1/tests/_kernel.py +32 -0
  22. draftwright-0.3.0/tests/test_layout_snapshot.py → draftwright-0.3.1/tests/_layout_sig.py +22 -72
  23. draftwright-0.3.0/tests/layout_snapshots/bracket.json → draftwright-0.3.1/tests/refactor_golden/bracket_section.json +1 -0
  24. draftwright-0.3.0/tests/layout_snapshots/slotted.json → draftwright-0.3.1/tests/refactor_golden/centered_rebate.json +86 -85
  25. draftwright-0.3.1/tests/refactor_golden/chamfered.json +135 -0
  26. draftwright-0.3.1/tests/refactor_golden/filleted.json +135 -0
  27. draftwright-0.3.0/tests/layout_snapshots/flange.json → draftwright-0.3.1/tests/refactor_golden/flange_dense.json +1 -0
  28. draftwright-0.3.1/tests/refactor_golden/grooved_shaft.json +149 -0
  29. draftwright-0.3.0/tests/layout_snapshots/box.json → draftwright-0.3.1/tests/refactor_golden/hex_bar.json +52 -51
  30. {draftwright-0.3.0/tests/layout_snapshots → draftwright-0.3.1/tests/refactor_golden}/holed_slot.json +1 -0
  31. draftwright-0.3.1/tests/refactor_golden/pocketed.json +202 -0
  32. draftwright-0.3.1/tests/refactor_golden/prismatic_ladder.json +153 -0
  33. {draftwright-0.3.0/tests/layout_snapshots → draftwright-0.3.1/tests/refactor_golden}/side_drilled.json +1 -0
  34. draftwright-0.3.0/tests/layout_snapshots/turned_shaft.json → draftwright-0.3.1/tests/refactor_golden/turned_stepped.json +1 -0
  35. draftwright-0.3.1/tests/test_carve_free_position_callers.py +161 -0
  36. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_declare.py +32 -3
  37. draftwright-0.3.1/tests/test_drawing_encapsulation.py +193 -0
  38. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_e2e_standards.py +5 -5
  39. draftwright-0.3.1/tests/test_import_boundaries.py +462 -0
  40. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_layout_cleanliness.py +4 -3
  41. draftwright-0.3.1/tests/test_layout_hypothesis.py +205 -0
  42. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_layout_property.py +1 -1
  43. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_make_drawing.py +439 -114
  44. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_pmi.py +33 -0
  45. draftwright-0.3.1/tests/test_private_test_imports.py +138 -0
  46. draftwright-0.3.1/tests/test_refactor_golden.py +219 -0
  47. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_render_seam.py +7 -2
  48. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_sheet_gdt.py +1 -1
  49. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_sheet_notes.py +1 -1
  50. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_strip_layout.py +54 -69
  51. draftwright-0.3.0/tests/_kernel.py +0 -19
  52. draftwright-0.3.0/tests/layout_snapshots/drive_screw_x.json +0 -155
  53. draftwright-0.3.0/tests/layout_snapshots/dshape.json +0 -219
  54. draftwright-0.3.0/tests/layout_snapshots/plate_holes.json +0 -258
  55. draftwright-0.3.0/tests/test_import_boundaries.py +0 -114
  56. {draftwright-0.3.0 → draftwright-0.3.1}/.gitignore +0 -0
  57. {draftwright-0.3.0 → draftwright-0.3.1}/LICENSE +0 -0
  58. {draftwright-0.3.0 → draftwright-0.3.1}/README.md +0 -0
  59. {draftwright-0.3.0 → draftwright-0.3.1}/skills/SKILL.md +0 -0
  60. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/_geometry.py +0 -0
  61. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/annotate.py +0 -0
  62. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/annotations/__init__.py +0 -0
  63. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/fits.py +0 -0
  64. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  65. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  66. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  67. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  68. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/fonts/__init__.py +0 -0
  69. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/intents.py +0 -0
  70. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/layout.py +0 -0
  71. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/linting/__init__.py +0 -0
  72. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/linting/issues.py +0 -0
  73. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/linting/structural.py +0 -0
  74. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/linting/suggest.py +0 -0
  75. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/make_drawing.py +0 -0
  76. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/model/__init__.py +0 -0
  77. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/model/declare.py +0 -0
  78. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/model/detect.py +0 -0
  79. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/model/ir.py +0 -0
  80. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/model/planner.py +0 -0
  81. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/pmi.py +0 -0
  82. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/projection.py +0 -0
  83. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/__init__.py +0 -0
  84. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/_features.py +0 -0
  85. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/_record.py +0 -0
  86. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/chamfers.py +0 -0
  87. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/countersinks.py +0 -0
  88. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/fillets.py +0 -0
  89. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/flats.py +0 -0
  90. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/grooves.py +0 -0
  91. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/levels.py +0 -0
  92. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/plates.py +0 -0
  93. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/slots.py +0 -0
  94. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/recognition/turned.py +0 -0
  95. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/repair.py +0 -0
  96. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/score.py +0 -0
  97. {draftwright-0.3.0 → draftwright-0.3.1}/src/draftwright/sheet_emit.py +0 -0
  98. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  99. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  100. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  101. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  102. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  103. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  104. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  105. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  106. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  107. {draftwright-0.3.0 → draftwright-0.3.1}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  108. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_e2e_slice.py +0 -0
  109. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_fits.py +0 -0
  110. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_gdt_placement.py +0 -0
  111. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_layout.py +0 -0
  112. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_lint_reconciliation.py +0 -0
  113. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_lint_structural.py +0 -0
  114. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_linting.py +0 -0
  115. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_object_aspects.py +0 -0
  116. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_part_model.py +0 -0
  117. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_recogniser_contract.py +0 -0
  118. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_recognition.py +0 -0
  119. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_registry.py +0 -0
  120. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_score.py +0 -0
  121. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_sheet_emit.py +0 -0
  122. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_sheet_of.py +0 -0
  123. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_sheet_tables.py +0 -0
  124. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_tolerances.py +0 -0
  125. {draftwright-0.3.0 → draftwright-0.3.1}/tests/test_turned_steps.py +0 -0
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.1 — 2026-07-17
4
+
5
+ Also lands the **#635 consolidation epic** (P1–P6, all six children closed): the render
6
+ layer's state bus is retired (`annotations/` reads zero `Drawing` privates,
7
+ machine-enforced), the three complexity hotspots are split (nesting ≤2), the whole-package
8
+ import DAG + a test-import ratchet are enforced, and the test suite gains a Hypothesis fuzz
9
+ tier + relational cross-kernel placement gates. All internal / behaviour-preserving
10
+ (byte-identical golden) — no output change.
11
+
12
+ ### Added
13
+
14
+ - **PNG raster export** — `--format png` on the CLI, or `Drawing.export(formats=("png",))`.
15
+ Rendered SVG → PDF → PNG via **pypdfium2** (Apache-2.0, Google PDFium BSD-3) + **Pillow**
16
+ (HPND) — both permissively licensed and pure-wheel, so PNG works cross-platform with **no
17
+ native cairo** (ADR 0006) and keeps the render path dual-license-friendly. `dpi=` sets the
18
+ raster resolution.
19
+
20
+ ### Changed
21
+
22
+ - **Unified export API** — `Drawing.export(out, *, formats=("pdf",)) → {format: path}` is now
23
+ the single entry point over `svg`/`dxf`/`pdf`/`png`; it returns a dict and internally handles
24
+ the SVG→PDF→PNG intermediate chain (writing/cleaning up intermediates it wasn't asked to keep).
25
+ The CLI `--format` gains `png`.
26
+
27
+ ### Deprecated
28
+
29
+ - `Drawing.export_pdf()` — use `export(formats=("pdf",))["pdf"]`; the method now warns.
30
+ - The legacy `export(svg=, dxf=)` boolean keywords and their `(svg_path, dxf_path)` tuple return
31
+ — kept for back-compat; prefer `formats=[...]` (the dict API).
32
+
3
33
  ## v0.3.0 — 2026-07-14
4
34
 
5
35
  **Breaking — feature-recogniser API renamed** (ADR 0013 / #568): *deliberate* breaks with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Automated technical-drawing generation for build123d
5
5
  Project-URL: Homepage, https://github.com/pzfreo/draftwright
6
6
  Project-URL: Repository, https://github.com/pzfreo/draftwright
@@ -686,6 +686,8 @@ Requires-Dist: build123d-drafting-helpers>=0.13.1
686
686
  Requires-Dist: build123d<0.11,>=0.9.0; python_full_version < '3.13'
687
687
  Requires-Dist: build123d<0.12,>=0.11; python_full_version >= '3.13'
688
688
  Requires-Dist: numpy>=1.24
689
+ Requires-Dist: pillow>=10
690
+ Requires-Dist: pypdfium2>=4
689
691
  Requires-Dist: reportlab>=4.0
690
692
  Requires-Dist: svglib>=1.5
691
693
  Requires-Dist: typer>=0.12
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -19,6 +19,11 @@ dependencies = [
19
19
  "build123d>=0.11,<0.12 ; python_full_version >= '3.13'",
20
20
  "build123d-drafting-helpers>=0.13.1",
21
21
  "numpy>=1.24",
22
+ # PNG raster export (SVG→PDF→PNG). Both permissively licensed (Pillow HPND; pypdfium2
23
+ # Apache-2.0 wrapping Google PDFium BSD-3) and ship pre-built wheels with NO native system
24
+ # deps — so PNG works cross-platform without cairo (ADR 0006) and stays dual-license-clean.
25
+ "pillow>=10",
26
+ "pypdfium2>=4",
22
27
  "reportlab>=4.0",
23
28
  "svglib>=1.5",
24
29
  "typer>=0.12",
@@ -112,6 +117,7 @@ ignore = ["E501"]
112
117
  [dependency-groups]
113
118
  dev = [
114
119
  "codespell>=2.3.0",
120
+ "hypothesis>=6",
115
121
  "mypy>=2.1.0",
116
122
  "pytest>=8",
117
123
  "pytest-cov>=5",
@@ -30,12 +30,12 @@ _LAZY = {
30
30
  "make_drawing": "draftwright.builder",
31
31
  "Drawing": "draftwright.drawing",
32
32
  "FeatureInfo": "draftwright.drawing",
33
- "Sheet": "draftwright.sheet_dsl",
33
+ "Sheet": "draftwright.sheet",
34
34
  "fix_svg_page_size": "draftwright.export",
35
35
  "lint_feature_coverage": "draftwright.linting",
36
36
  "PmiRecord": "draftwright.pmi",
37
37
  "extract_pmi": "draftwright.pmi",
38
- "choose_scale": "draftwright.sheet",
38
+ "choose_scale": "draftwright.compose",
39
39
  }
40
40
 
41
41
 
@@ -74,13 +74,13 @@ _sys.modules[__name__].__class__ = _DraftwrightModule
74
74
  if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel cost
75
75
  from draftwright.analysis import dedup_diams
76
76
  from draftwright.builder import build_drawing, generate_script, make_drawing
77
+ from draftwright.compose import choose_scale
77
78
  from draftwright.drawing import Drawing, FeatureInfo
78
79
  from draftwright.export import fix_svg_page_size
79
80
  from draftwright.linting import lint_feature_coverage
80
81
  from draftwright.pmi import PmiRecord, extract_pmi
81
82
  from draftwright.recognition import recognise_face_levels
82
- from draftwright.sheet import choose_scale
83
- from draftwright.sheet_dsl import Sheet
83
+ from draftwright.sheet import Sheet
84
84
 
85
85
 
86
86
  def __dir__():
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import functools
14
14
  import logging
15
+ import math
15
16
  import re
16
17
  from collections.abc import Callable
17
18
  from dataclasses import dataclass
@@ -20,8 +21,8 @@ from types import SimpleNamespace
20
21
  from typing import TYPE_CHECKING
21
22
 
22
23
  if TYPE_CHECKING:
24
+ from draftwright.compose import StripDepths
23
25
  from draftwright.recognition import TurnedProfile
24
- from draftwright.sheet import StripDepths
25
26
 
26
27
  from build123d import Align, BoundBox, Location, Mode, Shape, Text
27
28
  from build123d_drafting.helpers import (
@@ -133,9 +134,40 @@ def _text_width(text: str, font_size: float, font_path: str = PLEX_MONO) -> floa
133
134
  )
134
135
 
135
136
 
137
+ # Cheap mean glyph advance as a fraction of the em (font size), for label-width ESTIMATES in
138
+ # the diameter row/column capacity checks. Plex Mono is monospaced at ~0.6 em, padded a touch.
139
+ # The exact metric is `_text_width()` above (measured per string), which the corridor/label
140
+ # placers use; these capacity gates only need a close bound, not the metric, so they trade
141
+ # exactness for not rasterising a text layout per candidate. The two may diverge for unusually
142
+ # wide/narrow glyph runs.
143
+ _EST_CHAR_WIDTH_EM = 0.62
144
+
145
+
136
146
  _CONCENTRIC_TOL_MM = 0.5
137
147
 
138
148
 
149
+ def _first_free_index(prefix: str, taken) -> int:
150
+ """The lowest ``j`` for which ``f"{prefix}{j}"`` is not in *taken* (a set or any container
151
+ supporting ``in``). The shared kernel of the first-free annotation-name allocators
152
+ (``_loc_name``/``_uniq``/``_hc_name``) — used where reusing a freed gap is fine. NOTE: the
153
+ step-length / diameter runs deliberately use ``max+1`` instead (``_next_start`` /
154
+ ``_next_steplen_start``), because a contiguous run started at a gap below an occupied index
155
+ would wrap onto it (#432); don't fold those onto this."""
156
+ j = 0
157
+ while f"{prefix}{j}" in taken:
158
+ j += 1
159
+ return j
160
+
161
+
162
+ def _concentric_with_axis(a, x: float, y: float) -> bool:
163
+ """True when the page/world point ``(x, y)`` lies on the rotational part's turned axis
164
+ (within :data:`_CONCENTRIC_TOL_MM`). A bore/pattern centred on the axis needs no location
165
+ dim — its position is the axis — so several passes filter such refs; this is the single
166
+ radial test they share (the perpendicular-plane distance to the axis centre ``(a.cx,
167
+ a.cy)``). Callers still gate on ``a.is_rotational`` / role / axis as their context needs."""
168
+ return math.hypot(x - a.cx, y - a.cy) <= _CONCENTRIC_TOL_MM
169
+
170
+
139
171
  def _dim(p1, p2, side, distance, draft, **kwargs):
140
172
  """Build a :class:`Dimension`, tagged with its placement spec.
141
173
 
@@ -154,11 +186,14 @@ def _dim(p1, p2, side, distance, draft, **kwargs):
154
186
 
155
187
  # Dimension-line spacing (page-mm, scale-independent), the single source of truth for
156
188
  # BOTH the ADR 0009 strip carve (via the `Strip` dataclass defaults below) and the
157
- # sheet.py halo/depth estimates that must reserve the same space. Per ISO 129-1 / ASME
189
+ # compose.py halo/depth estimates that must reserve the same space. Per ISO 129-1 / ASME
158
190
  # Y14.5, the FIRST dimension line sits furthest from the outline (clears the outline +
159
191
  # extension-line origins) and subsequent parallel lines stack tighter and uniform (#347).
160
192
  _STRIP_GAP = 10.0 # clearance between the view outline and the first dimension line
161
193
  _STRIP_SPACING = 2.5 # clear gap between successive parallel dimension lines (beyond the label)
194
+ # Small lift (page-mm) of an overall/envelope witness line off the projected silhouette edge,
195
+ # so its extension line reads as distinct from the outline rather than sitting on top of it.
196
+ _WITNESS_LIFT_MM = 2.0
162
197
 
163
198
 
164
199
  @dataclass
@@ -353,7 +388,8 @@ def _largest_empty_rect(drawable, obstacles, *, warn: bool = True):
353
388
  @dataclass
354
389
  class DetailRequest:
355
390
  """A renderer's request for an enlarged detail of a region it could not draw
356
- legibly at sheet scale (#307). Renderers append these to ``dwg._detail_requests``
391
+ legibly at sheet scale (#307). Renderers append these to the run's
392
+ ``PlacementContext.detail_requests``
357
393
  instead of building bespoke detail views; ``_resolve_details`` resolves them all
358
394
  through one generic detailer (crop → project → place → caption → marker), then
359
395
  calls ``redraw`` to draw the feature's own dims inside the placed detail view.
@@ -600,7 +636,7 @@ def _iso_bbox(dwg):
600
636
 
601
637
 
602
638
  # --- page/scale selection + sheet-layout constants and helpers --------------
603
- # Relocated from make_drawing for the sheet.py split (#162). Shared by sheet.py
639
+ # Relocated from make_drawing for the compose.py (née sheet.py) split (#162). Shared by compose.py
604
640
  # (choose_scale/_layout_geometry) and make_drawing's repack pass, so they live
605
641
  # here in the shared base to keep the DAG acyclic.
606
642
  # The base inter-view corridor: one first-line gap + one dimension tier. Tracks
@@ -2,10 +2,10 @@
2
2
 
3
3
  `_analyse` imports the part (STEP or Shape), runs feature detection (holes,
4
4
  patterns, cylinders, face levels), classifies it (rotational vs prismatic),
5
- chooses the sheet (scale/page via `sheet.choose_scale`) and lays out the view
6
- zones (`sheet._layout_geometry`/`_build_zones`) — returning the `Analysis`
7
- namespace the rest of the pipeline reads. Sits above `sheet` and below
8
- `make_drawing` in the DAG.
5
+ chooses the sheet (scale/page via `compose.choose_scale`) and lays out the view
6
+ zones (`compose._layout_geometry`/`_build_zones`) — returning the `Analysis`
7
+ namespace the rest of the pipeline reads. Sits above `compose` (née `sheet`,
8
+ #640) and below `builder` in the DAG.
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -32,6 +32,15 @@ from draftwright._core import (
32
32
  _legible_steps,
33
33
  _Projector,
34
34
  )
35
+ from draftwright.compose import (
36
+ StripDepths,
37
+ _build_zones,
38
+ _est_hole_table_sizes,
39
+ _est_planned_bore_callout_width,
40
+ _layout_geometry,
41
+ _measure_strips,
42
+ choose_scale,
43
+ )
35
44
  from draftwright.model import build_part_model
36
45
  from draftwright.model.ir import Datum, PartModel, StepFeature
37
46
  from draftwright.model.planner import plan_dimensions
@@ -47,15 +56,6 @@ from draftwright.recognition import (
47
56
  recognise_turned_steps,
48
57
  step_level_zs,
49
58
  )
50
- from draftwright.sheet import (
51
- StripDepths,
52
- _build_zones,
53
- _est_hole_table_sizes,
54
- _est_planned_bore_callout_width,
55
- _layout_geometry,
56
- _measure_strips,
57
- choose_scale,
58
- )
59
59
 
60
60
  _log = logging.getLogger(__name__)
61
61
 
@@ -9,10 +9,12 @@ from __future__ import annotations
9
9
 
10
10
  import logging
11
11
  from dataclasses import dataclass, field
12
+ from typing import Any
12
13
 
13
14
  from build123d_drafting.helpers import Dimension, SafeDimension
14
15
 
15
16
  from draftwright.layout import StripCandidate, plan_strip
17
+ from draftwright.linting.issues import LintIssue
16
18
  from draftwright.linting.structural import _centerline_extent
17
19
 
18
20
  _log = logging.getLogger(__name__)
@@ -22,7 +24,8 @@ _log = logging.getLogger(__name__)
22
24
  class Escalation:
23
25
  """A first-class "could not place this here" signal (ADR 0009 Amendment 1, P5-strand-2).
24
26
 
25
- Placers *collect* one of these into ``dwg._escalations`` at the point of failure —
27
+ Placers *collect* one of these into the run's ``PlacementContext.escalations`` at the point
28
+ of failure —
26
29
  instead of recording a stringly-typed ``*_dropped`` lint code and letting the escalators
27
30
  grep for it — and one later resolver pass groups them by ``(view, feature-or-pattern)``,
28
31
  picks a remedy per group (ISO pattern-grouped balloon / table / detail / drop), and emits
@@ -519,25 +522,89 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
519
522
  _promote_losers(c)
520
523
 
521
524
 
522
- def register_corridor(dwg, key, strip, view, axis, tier, cand):
525
+ @dataclass
526
+ class PlacementContext:
527
+ """The per-run placement scratch a build's passes share — plus references to the drawing's
528
+ build-state stores (the ``registry`` build-issue sink + ``coverage`` bookkeeping) — threaded
529
+ to the passes explicitly instead of hung on the ``Drawing`` result object (ADR 0005 §2, #639):
530
+ the corridor batch
531
+ (:func:`register_corridor`/:func:`drain_corridors`), the escalation list (ADR 0009 Amdt 1,
532
+ #351), and the enlarged-detail request list (#307).
533
+
534
+ All three are per-run — both entry paths (:func:`_auto_annotate` and ``Drawing.finalize``)
535
+ make a fresh one each build and discard it after draining/consuming. The corridor batch is a
536
+ pure function of the still-present intents; escalations/detail-requests need no cross-retry
537
+ persistence either, because finalize is transactional (#647): a raised drain rolls the drawing
538
+ back and the retry re-runs from a clean slate, re-generating them."""
539
+
540
+ corridor_batch: dict = field(default_factory=dict)
541
+ escalations: list = field(default_factory=list)
542
+ detail_requests: list = field(default_factory=list)
543
+ # The drawing's build-state stores, referenced (not owned) by the run's passes (#639).
544
+ # Duck-typed as ``Any`` — matching the untyped ``Drawing._record_build_issue`` they replace —
545
+ # so mypy does not reject the delegating calls below.
546
+ registry: Any = None # the drawing's AnnotationRegistry: build-issue sink + names
547
+ coverage: Any = None # the drawing's CoverageState
548
+ # The ensured PartModel (ADR 0008 IR) the run's passes read, threaded off the drawing so
549
+ # they no longer reach into ``dwg._part_model`` (#639). Both entry paths set it from the
550
+ # PUBLIC ``dwg.model()`` after the model is ensured/attached.
551
+ part_model: Any = None
552
+ # Whether the model was DECLARED (vs detected) — the ADR 0011 gate the orchestrator reads,
553
+ # threaded off ``getattr(dwg, "_model_declared")`` (#639).
554
+ model_declared: bool = False
555
+ # Per-run cache for :meth:`feature_of_hole_at` — the model is fixed after build, so a
556
+ # per-ctx (per-run) index is correct (mirrors the old ``Drawing._hole_feature_index``).
557
+ _hole_feature_index: Any = field(default=None, repr=False)
558
+
559
+ def feature_of_hole_at(self, location):
560
+ """The IR hole/pattern feature whose member sits at model-space *location*, or ``None``
561
+ (#408/#639). Attributes a balloon (which carries a recognition hole, not the IR feature)
562
+ to its feature so :meth:`Drawing.drop` clears it. Cached on the run's ctx — the model is
563
+ fixed after build."""
564
+ m = self.part_model
565
+ if m is None:
566
+ return None
567
+ if self._hole_feature_index is None:
568
+ idx: dict = {}
569
+ for f in getattr(m, "features", []):
570
+ if getattr(f, "kind", None) in ("hole", "pattern"):
571
+ for loc in getattr(f, "members", None) or (f.frame.origin,):
572
+ idx[tuple(round(c, 3) for c in loc)] = f
573
+ self._hole_feature_index = idx
574
+ return self._hole_feature_index.get(tuple(round(c, 3) for c in location))
575
+
576
+ def record_issue(self, severity, code, message) -> None:
577
+ """Record a build-time lint issue on the run's registry (#639). Replaces the passes'
578
+ old `dwg._record_build_issue`."""
579
+ self.registry.record_issue(LintIssue(severity=severity, code=code, message=message))
580
+
581
+ def reset_issues(self) -> None:
582
+ self.registry.reset_issues()
583
+
584
+ def drop_issues(self, *codes) -> None:
585
+ self.registry.drop_issues(codes)
586
+
587
+
588
+ def register_corridor(ctx, key, strip, view, axis, tier, cand):
523
589
  """Queue a :class:`CorridorCandidate` under a shared corridor *key* so one
524
590
  :func:`drain_corridors` places the whole cross-pass set together (ADR 0009 end state).
525
591
  The first registration for a key fixes its ``(strip, view, axis)``; mixed producers on
526
592
  the same corridor use the largest requested tier so spacing is not registration-order
527
593
  dependent."""
528
- b = dwg._corridor_batch.setdefault(
594
+ b = ctx.corridor_batch.setdefault(
529
595
  key, {"strip": strip, "view": view, "axis": axis, "tier": tier, "cands": []}
530
596
  )
531
597
  b["tier"] = max(b["tier"], tier)
532
598
  b["cands"].append(cand)
533
599
 
534
600
 
535
- def drain_corridors(dwg):
601
+ def drain_corridors(ctx, dwg):
536
602
  """Solve every registered corridor (one :func:`solve_corridor` per strip), then clear
537
- the batch. Called once, after all corridor-feeding passes have registered."""
538
- for b in dwg._corridor_batch.values():
603
+ the batch. Called once, after all corridor-feeding passes have registered. Takes both the
604
+ scratch *ctx* (the batch) and *dwg* (the drawing :func:`solve_corridor` places onto)."""
605
+ for b in ctx.corridor_batch.values():
539
606
  solve_corridor(dwg, b["strip"], b["view"], b["axis"], b["cands"], b["tier"])
540
- dwg._corridor_batch = {}
607
+ ctx.corridor_batch = {}
541
608
 
542
609
 
543
610
  def place_strip_candidates(