draftwright 0.2.4__tar.gz → 0.2.6__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 (92) hide show
  1. {draftwright-0.2.4 → draftwright-0.2.6}/CHANGELOG.md +63 -1
  2. {draftwright-0.2.4 → draftwright-0.2.6}/PKG-INFO +1 -1
  3. {draftwright-0.2.4 → draftwright-0.2.6}/pyproject.toml +1 -1
  4. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/__init__.py +3 -0
  5. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/_core.py +41 -5
  6. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/analysis.py +21 -16
  7. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/annotate.py +1 -0
  8. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/annotations/_common.py +12 -4
  9. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/annotations/from_model.py +234 -80
  10. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/annotations/holes.py +506 -30
  11. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/annotations/orchestrator.py +94 -47
  12. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/annotations/sections.py +58 -0
  13. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/builder.py +236 -14
  14. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/cli.py +2 -5
  15. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/drawing.py +582 -9
  16. draftwright-0.2.6/src/draftwright/fits.py +154 -0
  17. draftwright-0.2.6/src/draftwright/intents.py +39 -0
  18. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/model/__init__.py +7 -0
  19. draftwright-0.2.6/src/draftwright/model/declare.py +476 -0
  20. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/model/detect.py +16 -0
  21. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/model/ir.py +13 -1
  22. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/model/planner.py +23 -10
  23. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/registry.py +36 -2
  24. draftwright-0.2.6/src/draftwright/sheet_dsl.py +244 -0
  25. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/box.json +47 -47
  26. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/bracket.json +86 -86
  27. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/drive_screw_x.json +36 -36
  28. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/dshape.json +54 -54
  29. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/flange.json +66 -66
  30. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/holed_slot.json +71 -71
  31. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/plate_holes.json +61 -61
  32. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/side_drilled.json +50 -50
  33. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/slotted.json +72 -72
  34. {draftwright-0.2.4 → draftwright-0.2.6}/tests/layout_snapshots/turned_shaft.json +79 -79
  35. draftwright-0.2.6/tests/test_declare.py +566 -0
  36. draftwright-0.2.6/tests/test_e2e_standards.py +343 -0
  37. draftwright-0.2.6/tests/test_fits.py +225 -0
  38. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_make_drawing.py +1398 -16
  39. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_render_seam.py +7 -2
  40. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_strip_layout.py +7 -8
  41. draftwright-0.2.6/tests/test_tolerances.py +218 -0
  42. draftwright-0.2.4/tests/test_e2e_standards.py +0 -175
  43. {draftwright-0.2.4 → draftwright-0.2.6}/.gitignore +0 -0
  44. {draftwright-0.2.4 → draftwright-0.2.6}/LICENSE +0 -0
  45. {draftwright-0.2.4 → draftwright-0.2.6}/README.md +0 -0
  46. {draftwright-0.2.4 → draftwright-0.2.6}/skills/SKILL.md +0 -0
  47. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/annotations/__init__.py +0 -0
  48. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/export.py +0 -0
  49. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/features.py +0 -0
  50. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  51. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  52. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  53. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  54. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/fonts/__init__.py +0 -0
  55. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/layout.py +0 -0
  56. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/linting/__init__.py +0 -0
  57. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/linting/coverage.py +0 -0
  58. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/linting/issues.py +0 -0
  59. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/linting/structural.py +0 -0
  60. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/linting/suggest.py +0 -0
  61. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/make_drawing.py +0 -0
  62. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/pmi.py +0 -0
  63. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/projection.py +0 -0
  64. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/recognition/__init__.py +0 -0
  65. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/recognition/_features.py +0 -0
  66. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/recognition/levels.py +0 -0
  67. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/recognition/slots.py +0 -0
  68. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/recognition/turned.py +0 -0
  69. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/repair.py +0 -0
  70. {draftwright-0.2.4 → draftwright-0.2.6}/src/draftwright/sheet.py +0 -0
  71. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  72. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  73. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  74. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  75. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  76. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  77. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  78. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  79. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  80. {draftwright-0.2.4 → draftwright-0.2.6}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  81. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_e2e_slice.py +0 -0
  82. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_layout.py +0 -0
  83. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_layout_cleanliness.py +0 -0
  84. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_layout_property.py +0 -0
  85. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_layout_snapshot.py +0 -0
  86. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_lint_structural.py +0 -0
  87. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_linting.py +0 -0
  88. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_part_model.py +0 -0
  89. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_pmi.py +0 -0
  90. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_recognition.py +0 -0
  91. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_registry.py +0 -0
  92. {draftwright-0.2.4 → draftwright-0.2.6}/tests/test_turned_steps.py +0 -0
@@ -1,6 +1,68 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## v0.2.6 — 2026-07-05
4
+
5
+ **ADR 0011 — the IR as a public input: declare features, don't only detect them.**
6
+ Detection stays the default, but the caller can now *supply* the feature model, so a
7
+ part you built parametrically reads as its own drawing and misdetection becomes
8
+ recoverable by construction. Plus the first Phase-2 aspect layer (tolerances + fits).
9
+
10
+ ### Added
11
+
12
+ - **`build_drawing(part, model=…)`** accepts a caller-supplied `PartModel` (or a
13
+ `Sequence[Feature]`); when given, **detection is skipped** and the auto-pass dimensions
14
+ the declared features. Detection and declaration are two producers of the same IR (#447,
15
+ ADR 0011 Phase 0).
16
+ - **Object → feature constructors** `draftwright.model.hole` / `boss` / `step` / `slot` /
17
+ `pattern` / `envelope` — read a feature's geometry off the build123d object you built (⌀
18
+ from the cylindrical face, axis/location from the bbox), or supply explicit values (#447).
19
+ - **The fluent `Sheet` façade** (`draftwright.Sheet`) — reference the objects you built,
20
+ declare their drawing aspects, `.build()` / `.export()`. `Sheet.from_part()` seeds the
21
+ hybrid override mode from detection (#447, Phase 1).
22
+ - **Toleranced dimensions** — a `±` / limit tolerance on a diameter, step, or hole bore,
23
+ rendered on both the linear and ⌀-callout paths (`Sheet.tolerance(...)`, or a
24
+ `decorations=` side-layer) (#28, Phase 2 P2a).
25
+ - **Fit-class deviation** — `Sheet.fit("H7")` resolves an ISO 286 fit class to its limit
26
+ deviation for the feature's nominal ⌀, rendered as the class code (`ø20 H7`, default) or
27
+ the signed deviations (`show="deviation"` → `ø20 +0.021/0`). Common classes; fails loud
28
+ outside its table (#29, Phase 2 P2a.2).
29
+
30
+ ### Changed
31
+
32
+ - **Object constructors honour explicit overrides** — a passed object supplies *defaults*;
33
+ each explicit keyword overrides that field independently, and invalid public input fails
34
+ at declaration with a clear `ValueError` (#451, #452).
35
+ - **A declared hole/pattern renders at its declared position** even where detection missed
36
+ it — the callout membership is sourced from the declared model, not only detection (#448).
37
+ - **`Sheet.model()` / `Sheet.from_part()`** no longer build a full drawing just to return
38
+ the IR — feature inspection/seeding is now a cheap, no-render path (#453).
39
+
40
+ ### Fixed
41
+
42
+ - A narrow diameter band hidden under a larger OD silhouette is no longer silently
43
+ undimensioned (the two feature inventories agreed) (#298).
44
+
45
+ ## v0.2.5 — 2026-07-04
46
+
47
+ **The editable write API and record-then-finalize.** A detected drawing became an editable
48
+ object, and a generated `--script` became a runnable reconstruction that reaches auto-pass
49
+ quality.
50
+
51
+ ### Added
52
+
53
+ - **`dwg.model()`** exposes the detected `PartModel` as a read surface, plus feature-
54
+ referenced add verbs `dimension()` / `callout()` / `locate()` / `furniture()` /
55
+ `section()` and `drop(feature)`, with a machine-checked completeness audit (#400).
56
+ - **Record-then-finalize** (#426): the verbs record intents in deferred mode and
57
+ `dwg.finalize()` (auto-run by export) drains them through the auto-pass's own solvers, so
58
+ a reconstruction reaches auto-pass quality; `--script` now emits a runnable detect-only
59
+ reconstruction.
60
+
61
+ ### Changed
62
+
63
+ - **Dimension-line spacing now follows ISO 129-1 / ASME Y14.5 convention** (#347): a wider
64
+ first-line gap (8 → 10 mm) and tighter, uniform parallel stacking (between-line clear gap
65
+ 4 → 2.5 mm), with the inter-view corridor widened in step. Re-drifts every drawing.
4
66
 
5
67
  ## v0.2.4 — 2026-07-03
6
68
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -30,6 +30,7 @@ _LAZY = {
30
30
  "make_drawing": "draftwright.builder",
31
31
  "Drawing": "draftwright.drawing",
32
32
  "FeatureInfo": "draftwright.drawing",
33
+ "Sheet": "draftwright.sheet_dsl",
33
34
  "fix_svg_page_size": "draftwright.export",
34
35
  "lint_feature_coverage": "draftwright.linting",
35
36
  "PmiRecord": "draftwright.pmi",
@@ -78,6 +79,7 @@ if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel co
78
79
  from draftwright.linting import lint_feature_coverage
79
80
  from draftwright.pmi import PmiRecord, extract_pmi
80
81
  from draftwright.sheet import choose_scale
82
+ from draftwright.sheet_dsl import Sheet
81
83
 
82
84
 
83
85
  def __dir__():
@@ -91,6 +93,7 @@ __all__ = [
91
93
  "Drawing",
92
94
  "FeatureInfo",
93
95
  "PmiRecord",
96
+ "Sheet",
94
97
  "analyse_face_levels",
95
98
  "build_drawing",
96
99
  "choose_scale",
@@ -30,6 +30,7 @@ from build123d_drafting.helpers import (
30
30
  format_drawing_scale,
31
31
  )
32
32
 
33
+ from draftwright.fits import FitClass
33
34
  from draftwright.fonts import PLEX_MONO, PLEX_SANS_CONDENSED
34
35
  from draftwright.layout import _greedy_strip_1d, _solve_strip_1d
35
36
 
@@ -86,6 +87,30 @@ def _fmt(v: float) -> str:
86
87
  return str(r) if abs(v - r) < 1e-6 else f"{v:.1f}"
87
88
 
88
89
 
90
+ def _tol_suffix(tolerance, draft) -> str:
91
+ """The ``±`` / limit tolerance suffix to append to a **callout** label (a ø leader
92
+ or a hole callout), matching byte-for-byte what ``Dimension(tolerance=…)`` renders
93
+ on a linear dim (helpers ``_format_label``): a symmetric ``float`` → ``" ±t"``; an
94
+ ``(lower, upper)`` pair → ``" +upper -lower"`` — all rounded to the draft precision.
95
+
96
+ draftwright owns this suffix ONLY because the pinned helpers' ``Leader`` /
97
+ ``HoleCallout`` take no ``tolerance=`` yet, so we bake it into the label string
98
+ ourselves. Delete this once helpers grows a first-class tolerance parameter for
99
+ those two (extraction tracked as #449) — ``Dimension`` formats its own (#28 / P2a).
100
+
101
+ A resolved fit (:class:`~draftwright.fits.FitClass`, P2a.2) renders its own class-code
102
+ or deviation suffix — it rides the same ``tolerance`` field as an aspect marker."""
103
+ if tolerance is None:
104
+ return ""
105
+ if isinstance(tolerance, FitClass):
106
+ return tolerance.suffix()
107
+ prec = draft.decimal_precision
108
+ if isinstance(tolerance, (int, float)):
109
+ return f" ±{round(tolerance, prec):.{prec}f}"
110
+ lo, hi = tolerance
111
+ return f" +{round(hi, prec):.{prec}f} -{round(lo, prec):.{prec}f}"
112
+
113
+
89
114
  def _tag_sequence(n):
90
115
  """``A, B, …, Z, AA, AB, …`` — deterministic hole-table tags for *n* rows."""
91
116
  tags = []
@@ -162,6 +187,15 @@ def _dim(p1, p2, side, distance, draft, **kwargs):
162
187
  return d
163
188
 
164
189
 
190
+ # Dimension-line spacing (page-mm, scale-independent), the single source of truth for
191
+ # BOTH the ADR 0009 strip carve (via the `Strip` dataclass defaults below) and the
192
+ # sheet.py halo/depth estimates that must reserve the same space. Per ISO 129-1 / ASME
193
+ # Y14.5, the FIRST dimension line sits furthest from the outline (clears the outline +
194
+ # extension-line origins) and subsequent parallel lines stack tighter and uniform (#347).
195
+ _STRIP_GAP = 10.0 # clearance between the view outline and the first dimension line
196
+ _STRIP_SPACING = 2.5 # clear gap between successive parallel dimension lines (beyond the label)
197
+
198
+
165
199
  @dataclass
166
200
  class Strip:
167
201
  """A one-dimensional annotation band adjacent to an orthographic view.
@@ -184,8 +218,8 @@ class Strip:
184
218
  anchor: float
185
219
  outer_limit: float
186
220
  direction: float = 1.0
187
- gap: float = 8.0
188
- spacing: float = 4.0
221
+ gap: float = _STRIP_GAP
222
+ spacing: float = _STRIP_SPACING
189
223
 
190
224
  @property
191
225
  def available(self) -> float:
@@ -220,7 +254,6 @@ _SLOT_DIM_DEPTH = 2 * _FONT_SIZE + _PAD # sv_zones.below: overall depth dimensi
220
254
  _SLOT_DIM_HEIGHT = 2 * _FONT_SIZE + 2 * _PAD # fv_zones.right: overall height dim
221
255
 
222
256
 
223
- _STRIP_SPACING = 4.0 # page-mm between successive annotations in a strip
224
257
  _MIN_VIEW_MM = (
225
258
  10.0 # min projected view dimension; below it annotation geometry degenerates (#129)
226
259
  )
@@ -572,8 +605,11 @@ def _iso_bbox(dwg):
572
605
  # Relocated from make_drawing for the sheet.py split (#162). Shared by sheet.py
573
606
  # (choose_scale/_layout_geometry) and make_drawing's repack pass, so they live
574
607
  # here in the shared base to keep the DAG acyclic.
575
- _DIM_PAD = 18.0
576
- _STRIP_GAP = 8.0
608
+ # The base inter-view corridor: one first-line gap + one dimension tier. Tracks
609
+ # _STRIP_GAP so widening the first-line gap (#347) keeps the below-plan / between-view
610
+ # corridors from razor-fitting the first dim line (the #130 slack guarantee): 10 + 10.
611
+ _DIM_PAD = _STRIP_GAP + _SLOT_DIM_HEIGHT # 20.0
612
+ # _STRIP_GAP / _STRIP_SPACING are defined above (beside the `Strip` dataclass they seed).
577
613
 
578
614
  _PAGE_SIZES = {
579
615
  "A4": (297.0, 210.0),
@@ -179,6 +179,26 @@ _STEP_MIN_AREA_FRAC = 0.01
179
179
  # ---------------------------------------------------------------------------
180
180
 
181
181
 
182
+ def _solids_body(part, src: str = "part"):
183
+ """The part reduced to just its solids — the geometry the drawing is *of*.
184
+
185
+ AP242 STEP files (and hand-built Compounds) can carry non-solid geometry beside
186
+ the solid — PMI presentation wires, leader curves, construction edges/sketches —
187
+ which, left in, draw as phantom rectangles in every view and inflate the bounding
188
+ box, corrupting the scale choice and the envelope dimensions. Shared by
189
+ :func:`_analyse` and :meth:`draftwright.Sheet.model` (#453) so the model a caller
190
+ *inspects* is wrapped from the exact same body the engine *draws*."""
191
+ solids = part.solids()
192
+ if not solids:
193
+ return part
194
+ body = solids[0] if len(solids) == 1 else Compound(children=list(solids))
195
+ if body.bounding_box().size != part.bounding_box().size or len(part.edges()) != len(
196
+ body.edges()
197
+ ):
198
+ _log.info("Dropping non-solid geometry from %s (PMI presentation data)", src)
199
+ return body
200
+
201
+
182
202
  def _analyse(
183
203
  step_file, title, number, tolerance, drawn_by, out, scale=None, page=None, pmi="off"
184
204
  ) -> Analysis:
@@ -192,22 +212,7 @@ def _analyse(
192
212
  else:
193
213
  part = _import_step(step_file)
194
214
  src = str(step_file)
195
- # AP242 STEP files carry PMI presentation geometry (annotation-plane
196
- # border wires, leader curves) beside the solid; left in, it draws as
197
- # phantom rectangles in every view and inflates the bounding box —
198
- # corrupting the scale choice and the envelope dimensions. The drawing
199
- # is of the solids.
200
- solids = part.solids()
201
- if solids:
202
- body = solids[0] if len(solids) == 1 else Compound(children=list(solids))
203
- if body.bounding_box().size != part.bounding_box().size or len(part.edges()) != len(
204
- body.edges()
205
- ):
206
- _log.info(
207
- "Dropping non-solid geometry from %s (PMI presentation data)",
208
- src,
209
- )
210
- part = body
215
+ part = _solids_body(part, src)
211
216
 
212
217
  # Semantic PMI extraction (AP242 only; separate read-only pass).
213
218
  pmi_records: list = []
@@ -10,4 +10,5 @@ from draftwright.annotations.from_model import _detect_step_repeat # noqa: F401
10
10
  from draftwright.annotations.orchestrator import ( # noqa: F401
11
11
  _auto_annotate,
12
12
  _wrap_rows,
13
+ build_model,
13
14
  )
@@ -302,6 +302,9 @@ class CorridorCandidate:
302
302
  dedup: tuple | None = None
303
303
  precedence: int = 0
304
304
  force: bool = False
305
+ # The source IR feature this dim was rendered for — recorded as provenance when the
306
+ # dim is placed at drain (ADR 0010). ``None`` leaves the annotation feature-less.
307
+ feature: object | None = None
305
308
 
306
309
 
307
310
  def solve_corridor(dwg, strip, view, axis, cands, tier):
@@ -353,13 +356,16 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
353
356
  _promote_losers(c)
354
357
  return
355
358
  pairs = [(c.name, c.build) for c in kept]
356
- left = {n for n, _ in place_strip_candidates(dwg, strip, view, axis, pairs, tier)}
359
+ feats = {c.name: c.feature for c in kept if c.feature is not None} # provenance (ADR 0010)
360
+ left = {
361
+ n for n, _ in place_strip_candidates(dwg, strip, view, axis, pairs, tier, features=feats)
362
+ }
357
363
  force_pairs = [(c.name, c.build) for c in kept if c.name in left and c.force]
358
364
  still = (
359
365
  {
360
366
  n
361
367
  for n, _ in place_strip_candidates(
362
- dwg, strip, view, axis, force_pairs, tier, force=True
368
+ dwg, strip, view, axis, force_pairs, tier, force=True, features=feats
363
369
  )
364
370
  }
365
371
  if force_pairs
@@ -393,7 +399,7 @@ def drain_corridors(dwg):
393
399
  dwg._corridor_batch = {}
394
400
 
395
401
 
396
- def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False):
402
+ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False, features=None):
397
403
  """Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
398
404
  The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
399
405
  P3): each candidate in *cands* — an ``(name, build(pos)->dim)`` pair — is spaced by
@@ -481,7 +487,9 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False):
481
487
  if not force and _box_hits(_geom_box(dim), blockers): # corridor crosses a leader
482
488
  todo.append((name, build))
483
489
  continue
484
- dwg.add(dim, name, view=view)
490
+ # Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
491
+ # dims — `features` maps this batch's names to their source IR feature.
492
+ dwg.add(dim, name, view=view, feature=(features or {}).get(name))
485
493
  return todo
486
494
 
487
495