draftwright 0.3.6__tar.gz → 0.3.8__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 (141) hide show
  1. {draftwright-0.3.6 → draftwright-0.3.8}/.gitignore +3 -0
  2. {draftwright-0.3.6 → draftwright-0.3.8}/CHANGELOG.md +60 -0
  3. {draftwright-0.3.6 → draftwright-0.3.8}/PKG-INFO +3 -1
  4. {draftwright-0.3.6 → draftwright-0.3.8}/README.md +1 -0
  5. {draftwright-0.3.6 → draftwright-0.3.8}/pyproject.toml +12 -1
  6. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/_core.py +21 -5
  7. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/annotations/_common.py +25 -2
  8. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/annotations/balloons.py +1 -1
  9. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/annotations/from_model.py +252 -44
  10. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/annotations/holes.py +25 -13
  11. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/annotations/orchestrator.py +7 -6
  12. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/annotations/sections.py +33 -29
  13. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/builder.py +30 -30
  14. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/cli.py +9 -0
  15. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/drawing.py +287 -56
  16. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/linting/coverage.py +7 -1
  17. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/linting/structural.py +128 -0
  18. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/linting/suggest.py +9 -9
  19. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/make_drawing.py +5 -4
  20. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/model/detect.py +240 -133
  21. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/model/planner.py +10 -4
  22. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/projection.py +9 -5
  23. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/slots.py +126 -1
  24. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/turned.py +8 -2
  25. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/sheet.py +78 -12
  26. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/sheet_emit.py +169 -2
  27. draftwright-0.3.8/tests/test_agents_guide.py +113 -0
  28. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_declare.py +1 -1
  29. draftwright-0.3.8/tests/test_detect_registry.py +166 -0
  30. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_drawing_encapsulation.py +26 -1
  31. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_e2e_standards.py +3 -3
  32. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_export_dxf_zoom.py +2 -2
  33. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_gdt_placement.py +3 -1
  34. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_import_boundaries.py +4 -5
  35. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_lint_structural.py +79 -0
  36. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_make_drawing.py +560 -100
  37. draftwright-0.3.8/tests/test_note_verb.py +59 -0
  38. draftwright-0.3.8/tests/test_private_test_attr_reads.py +298 -0
  39. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_private_test_imports.py +8 -0
  40. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_render_seam.py +13 -2
  41. draftwright-0.3.8/tests/test_script_detail_parity.py +400 -0
  42. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_sheet_emit.py +45 -0
  43. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_strip_layout.py +24 -16
  44. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_tolerances.py +118 -7
  45. draftwright-0.3.6/tests/test_script_detail_parity.py +0 -185
  46. {draftwright-0.3.6 → draftwright-0.3.8}/LICENSE +0 -0
  47. {draftwright-0.3.6 → draftwright-0.3.8}/docs/adr/README.md +0 -0
  48. {draftwright-0.3.6 → draftwright-0.3.8}/skills/SKILL.md +0 -0
  49. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/__init__.py +0 -0
  50. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/_geometry.py +0 -0
  51. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/analysis.py +0 -0
  52. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/annotate.py +0 -0
  53. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/annotations/__init__.py +0 -0
  54. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/compose.py +0 -0
  55. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/export.py +0 -0
  56. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/fits.py +0 -0
  57. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  58. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  59. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  60. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  61. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/fonts/__init__.py +0 -0
  62. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/intents.py +0 -0
  63. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/layout.py +0 -0
  64. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/linting/__init__.py +0 -0
  65. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/linting/issues.py +0 -0
  66. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/model/__init__.py +0 -0
  67. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/model/declare.py +0 -0
  68. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/model/ir.py +0 -0
  69. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/pmi.py +0 -0
  70. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/__init__.py +0 -0
  71. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/_features.py +0 -0
  72. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/_record.py +0 -0
  73. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/chamfers.py +0 -0
  74. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/countersinks.py +0 -0
  75. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/fillets.py +0 -0
  76. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/flats.py +0 -0
  77. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/grooves.py +0 -0
  78. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/levels.py +0 -0
  79. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/recognition/plates.py +0 -0
  80. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/registry.py +0 -0
  81. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/repair.py +0 -0
  82. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/score.py +0 -0
  83. {draftwright-0.3.6 → draftwright-0.3.8}/src/draftwright/sheet_dsl.py +0 -0
  84. {draftwright-0.3.6 → draftwright-0.3.8}/tests/_kernel.py +0 -0
  85. {draftwright-0.3.6 → draftwright-0.3.8}/tests/_layout_sig.py +0 -0
  86. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  87. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  88. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  89. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  90. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  91. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  92. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  93. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  94. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  95. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  96. {draftwright-0.3.6 → draftwright-0.3.8}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  97. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/bracket_section.json +0 -0
  98. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/centered_rebate.json +0 -0
  99. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/chamfered.json +0 -0
  100. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/filleted.json +0 -0
  101. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/flange_dense.json +0 -0
  102. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/grid_plate.json +0 -0
  103. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/grooved_shaft.json +0 -0
  104. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/hex_bar.json +0 -0
  105. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/holed_slot.json +0 -0
  106. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/pocketed.json +0 -0
  107. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/prismatic_ladder.json +0 -0
  108. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/scattered_plate.json +0 -0
  109. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/side_drilled.json +0 -0
  110. {draftwright-0.3.6 → draftwright-0.3.8}/tests/refactor_golden/turned_stepped.json +0 -0
  111. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_architecture_docs.py +0 -0
  112. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_carve_free_position_callers.py +0 -0
  113. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_dense_sheet_canary.py +0 -0
  114. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_detect_once.py +0 -0
  115. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_e2e_slice.py +0 -0
  116. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_fillets_adjacency.py +0 -0
  117. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_fits.py +0 -0
  118. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_layout.py +0 -0
  119. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_layout_cleanliness.py +0 -0
  120. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_layout_hypothesis.py +0 -0
  121. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_layout_property.py +0 -0
  122. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_lint_box_cache.py +0 -0
  123. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_lint_reconciliation.py +0 -0
  124. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_linting.py +0 -0
  125. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_object_aspects.py +0 -0
  126. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_occupancy_boxes.py +0 -0
  127. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_part_model.py +0 -0
  128. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_pitch_dim_footprint.py +0 -0
  129. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_pmi.py +0 -0
  130. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_recogniser_contract.py +0 -0
  131. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_recognition.py +0 -0
  132. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_refactor_golden.py +0 -0
  133. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_registry.py +0 -0
  134. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_score.py +0 -0
  135. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_sheet_gdt.py +0 -0
  136. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_sheet_notes.py +0 -0
  137. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_sheet_of.py +0 -0
  138. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_sheet_tables.py +0 -0
  139. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_solve_trace.py +0 -0
  140. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_turned_steps.py +0 -0
  141. {draftwright-0.3.6 → draftwright-0.3.8}/tests/test_witness_label_reconciliation.py +0 -0
@@ -8,5 +8,8 @@ build/
8
8
  .pytest_cache/
9
9
  testenv/
10
10
  .coverage
11
+ .coverage.*
11
12
  coverage.xml
13
+ coverage*.json
14
+ htmlcov/
12
15
  .DS_Store
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.8 — 2026-07-23
4
+
5
+ ### Added
6
+
7
+ - **`Drawing.note()` — a public free-text note verb** (#817/#820): place a free-form
8
+ `Note` at a page point without reaching for the low-level placement primitive —
9
+ `dwg.note("SEE NOTE 1", at=(x, y))`. This is the safe door for free text that the
10
+ privatisation below needs.
11
+ - **Obround (racetrack) through-slot recognition** (#816): a part whose only features
12
+ are obround through-slots is now recognised, dimensioned, and referenceable for GD&T.
13
+ Previously such slots were silently undetected — and lint falsely reported the drawing
14
+ complete. Stubby obrounds, whose straight side walls are too short to pair, are recovered
15
+ from their two semicircular end caps (a round hole is never mistaken for a slot end).
16
+
17
+ ### Changed
18
+
19
+ - **The generated Sheet script is now self-describing** (#833): `--script --style sheet`
20
+ emits a feature-census header, section sub-headers with repeat tallies (`8× R50`), and a
21
+ plain-language comment on each verb line (`# ⌀8 THRU ×4`, `# slot 40 × 120`, `# R50`).
22
+ All additions are comment-only, so re-run fidelity is unchanged.
23
+
24
+ ### Deprecated
25
+
26
+ - **The low-level `Drawing` placement API is now private — the public surface is
27
+ safe-by-default** (#817): `add`, `place_dim`, and the view-coordinate / build-state
28
+ plumbing (`set_view_coordinates`, `drop_view_coordinates`, `attach_part_model`,
29
+ `attach_solve_trace`, `add_view`, `clear_annotations`) are deprecated in favour of the
30
+ feature-backed verbs (`callout` / `dimension` / `locate` / `note` / …), which route
31
+ through the layout solve. Each keeps a `@deprecated` (PEP 702) shim for one release —
32
+ so type-checkers and IDEs flag call sites, not just a runtime warning — and is slated
33
+ for removal in ~0.5.0.
34
+
35
+ ### Internal
36
+
37
+ - **`AGENTS.md` — an AI-agent usage guide** (#818) with an executable check, steering
38
+ agents toward the safe declarative surface rather than the low-level primitives.
39
+ - **Coverage reporting is governed and a Codecov badge added** (#825/#832).
40
+
41
+ ## v0.3.7 — 2026-07-21
42
+
43
+ ### Fixed
44
+
45
+ - **Turned ⌀ leaders point at the feature, not a corner or across the body**
46
+ (#794/#798): the diameter callouts on a turned part now centre on the step's
47
+ mid-length (a boss keeps its frame origin; a ⌀ shared by disjoint steps centres
48
+ on the longest run), so the arrow lands on the middle of the feature's edge
49
+ rather than a step corner. A leader that would otherwise cut diagonally through
50
+ the body — a nested or end feature such as a ⌀6 boss stub — is re-routed out to
51
+ the nearest clear margin (axis-aware: X-turned → left/right, Z-turned → top/
52
+ bottom), pointing straight at the feature instead of clipping the silhouette. A
53
+ re-route that can't be placed clear-and-safe is restored and flagged, and a
54
+ pinned leader is never moved.
55
+
56
+ ### Added
57
+
58
+ - **`leader_crosses_silhouette` lint notice** (#796): an info-level structural
59
+ notice when a leader shaft passes *through* the projected part body (as opposed
60
+ to a hole callout legitimately exiting an internal hole), so the gap is
61
+ observable even where routing cannot clear it.
62
+
3
63
  ## v0.3.6 — 2026-07-21
4
64
 
5
65
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.3.6
3
+ Version: 0.3.8
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
@@ -691,11 +691,13 @@ Requires-Dist: pypdfium2>=4
691
691
  Requires-Dist: reportlab>=4.0
692
692
  Requires-Dist: svglib>=1.5
693
693
  Requires-Dist: typer>=0.12
694
+ Requires-Dist: typing-extensions>=4.5; python_full_version < '3.13'
694
695
  Description-Content-Type: text/markdown
695
696
 
696
697
  # draftwright
697
698
 
698
699
  [![CI](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml/badge.svg)](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml)
700
+ [![codecov](https://codecov.io/gh/pzfreo/draftwright/branch/main/graph/badge.svg)](https://codecov.io/gh/pzfreo/draftwright)
699
701
  [![PyPI](https://img.shields.io/pypi/v/draftwright.svg)](https://pypi.org/project/draftwright/)
700
702
  [![Python](https://img.shields.io/pypi/pyversions/draftwright.svg)](https://pypi.org/project/draftwright/)
701
703
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)
@@ -1,6 +1,7 @@
1
1
  # draftwright
2
2
 
3
3
  [![CI](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml/badge.svg)](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/pzfreo/draftwright/branch/main/graph/badge.svg)](https://codecov.io/gh/pzfreo/draftwright)
4
5
  [![PyPI](https://img.shields.io/pypi/v/draftwright.svg)](https://pypi.org/project/draftwright/)
5
6
  [![Python](https://img.shields.io/pypi/pyversions/draftwright.svg)](https://pypi.org/project/draftwright/)
6
7
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.3.6"
7
+ version = "0.3.8"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -27,6 +27,8 @@ dependencies = [
27
27
  "reportlab>=4.0",
28
28
  "svglib>=1.5",
29
29
  "typer>=0.12",
30
+ # PEP 702 @deprecated: stdlib `warnings.deprecated` lands in 3.13; backport for 3.10–3.12.
31
+ "typing_extensions>=4.5 ; python_full_version < '3.13'",
30
32
  ]
31
33
  keywords = ["build123d", "cad", "drafting", "engineering-drawing", "technical-drawing", "automation"]
32
34
  classifiers = [
@@ -85,8 +87,17 @@ addopts = "--tb=short -m 'not slow'"
85
87
  [tool.coverage.run]
86
88
  source = ["src/draftwright"]
87
89
  omit = ["src/draftwright/__init__.py"]
90
+ # Branch coverage makes the regression gate sensitive to untested decision paths,
91
+ # which matter more than raw statement execution in the recognition/layout code.
92
+ branch = true
88
93
 
89
94
  [tool.coverage.report]
95
+ # Baseline measured on the full fast tier on Linux/Python 3.13 (#825):
96
+ # 92.05% combined line+branch coverage (93.90% statements, 86.89% branches).
97
+ # Keep two points of cross-platform/kernel headroom; ratchet upward only after the
98
+ # lowest CI matrix result has stayed above the proposed floor.
99
+ fail_under = 90
100
+ precision = 1
90
101
  exclude_lines = [
91
102
  "pragma: no cover",
92
103
  "if TYPE_CHECKING:",
@@ -32,6 +32,7 @@ from build123d import Align, BoundBox, Compound, Edge, Location, Mode, Shape, Te
32
32
  from build123d_drafting.helpers import (
33
33
  Dimension,
34
34
  TitleBlock,
35
+ annotate,
35
36
  draft_preset,
36
37
  format_drawing_scale,
37
38
  )
@@ -48,6 +49,21 @@ from draftwright.layout import _greedy_strip_1d, _solve_strip_1d
48
49
  _log = logging.getLogger(__name__)
49
50
 
50
51
 
52
+ def place_annotation(registry, items, obj, name=None, view=None, feature=None):
53
+ """The annotation-placement primitive (#817): register *obj* under *name* — replacing any
54
+ prior object of that name (dropped from the render list *items*) so a name maps to one
55
+ object — append it to *items*, and record its owning *view* + source *feature* in *registry*.
56
+ Shared by :meth:`Drawing._add` and :meth:`PlacementContext.place` so the render passes place
57
+ annotations without reaching into the ``Drawing``. Returns *obj*."""
58
+ displaced = registry.named(name) if name is not None else None
59
+ if displaced is not None:
60
+ items.remove(displaced)
61
+ annotate(obj, name)
62
+ items.append(obj)
63
+ registry.add(obj, name, view, feature)
64
+ return obj
65
+
66
+
51
67
  _MARGIN = 10.0
52
68
 
53
69
  # When a sheet frame is drawn (#767), content reserves this extra band inside the border so
@@ -841,7 +857,7 @@ def _add_title_block(dwg, a: Analysis):
841
857
  bx + cell["max_x"],
842
858
  _TB_CLEAR + cell["max_y"],
843
859
  )
844
- dwg.add(tb, "title_block")
860
+ place_annotation(dwg.registry, dwg.items, tb, "title_block")
845
861
 
846
862
 
847
863
  def _make_sheet_frame(a: Analysis) -> Compound:
@@ -866,7 +882,7 @@ def _make_sheet_frame(a: Analysis) -> Compound:
866
882
  def _add_sheet_frame(dwg, a: Analysis):
867
883
  """Add the sheet border (#767), drawn last like the title block. No-op is the caller's
868
884
  (gated on ``a.frame``)."""
869
- dwg.add(_make_sheet_frame(a), "sheet_frame")
885
+ place_annotation(dwg.registry, dwg.items, _make_sheet_frame(a), "sheet_frame")
870
886
 
871
887
 
872
888
  def _add_projection_symbol(dwg, a: Analysis):
@@ -893,7 +909,7 @@ def _add_projection_symbol(dwg, a: Analysis):
893
909
  cy = _TB_CLEAR + _TB_H - h / 2 - 2
894
910
  sym = sym.locate(Location((cx - bx, cy - by, 0)))
895
911
  sym.is_projection_symbol = True
896
- dwg.add(sym, "projection_symbol")
912
+ place_annotation(dwg.registry, dwg.items, sym, "projection_symbol")
897
913
 
898
914
 
899
915
  def _add_zone_grid(dwg, a: Analysis):
@@ -926,12 +942,12 @@ def _add_zone_grid(dwg, a: Analysis):
926
942
  ticks.append(Edge.make_line(Vector(x1, yb, 0), Vector(x1 + tick, yb, 0)))
927
943
  grid = Compound(children=ticks)
928
944
  grid.is_zone_grid = True
929
- dwg.add(grid, "zone_grid")
945
+ place_annotation(dwg.registry, dwg.items, grid, "zone_grid")
930
946
 
931
947
  def _label(text, cx, cy, name):
932
948
  note = Note(text, (cx, cy), draft, align=(Align.CENTER, Align.CENTER))
933
949
  note.is_zone_label = True
934
- dwg.add(note, name)
950
+ place_annotation(dwg.registry, dwg.items, note, name)
935
951
 
936
952
  for i in range(cols): # numbers 1.. left→right, in the bottom + top bands
937
953
  cx = x0 + (i + 0.5) * cw
@@ -18,7 +18,11 @@ from typing import Any
18
18
 
19
19
  from build123d_drafting.helpers import DEFAULT_FONT_PATH, Dimension, SafeDimension
20
20
 
21
- from draftwright._core import _anno_box, _text_size # noqa: F401 — _anno_box re-exported (#700)
21
+ from draftwright._core import ( # noqa: F401 — _anno_box re-exported (#700)
22
+ _anno_box,
23
+ _text_size,
24
+ place_annotation,
25
+ )
22
26
  from draftwright._geometry import _boxes_overlap, _segment_crosses_box # noqa: F401
23
27
  from draftwright.layout import StripCandidate, plan_strip
24
28
  from draftwright.linting.issues import LintIssue
@@ -908,6 +912,7 @@ def solve_corridor(dwg, strip, view, axis, cands, tier, corner_reserves=(), *, k
908
912
  axis,
909
913
  pairs,
910
914
  tier,
915
+ ctx=ctx,
911
916
  features=feats,
912
917
  sizes=sizes,
913
918
  forbid=forbid,
@@ -930,6 +935,7 @@ def solve_corridor(dwg, strip, view, axis, cands, tier, corner_reserves=(), *, k
930
935
  axis,
931
936
  force_pairs,
932
937
  tier,
938
+ ctx=ctx,
933
939
  force=True,
934
940
  footprints=foots,
935
941
  corner_reserves=corner_reserves,
@@ -996,6 +1002,9 @@ class PlacementContext:
996
1002
  # so mypy does not reject the delegating calls below.
997
1003
  registry: Any = None # the drawing's AnnotationRegistry: build-issue sink + names
998
1004
  coverage: Any = None # the drawing's CoverageState
1005
+ # The drawing's render list (#817): :meth:`place` appends here, so a render pass places an
1006
+ # annotation through the ctx seam (``ctx.place(...)``) instead of reaching into the drawing.
1007
+ items: Any = None
999
1008
  # The ensured PartModel (ADR 0008 IR) the run's passes read, threaded off the drawing so
1000
1009
  # they no longer reach into ``dwg._part_model`` (#639). Both entry paths set it from the
1001
1010
  # PUBLIC ``dwg.model()`` after the model is ensured/attached.
@@ -1007,6 +1016,19 @@ class PlacementContext:
1007
1016
  # per-ctx (per-run) index is correct (mirrors the old ``Drawing._hole_feature_index``).
1008
1017
  _hole_feature_index: Any = field(default=None, repr=False)
1009
1018
 
1019
+ def place(self, obj, name=None, view=None, feature=None):
1020
+ """Place an annotation onto the drawing through this context (#817) — the render passes'
1021
+ door to the placement primitive, so a pass never reaches into the ``Drawing`` (ADR 0005
1022
+ §2). Registers *obj* under *name* (owning *view* + source *feature*) and appends it to the
1023
+ render list. Returns *obj*."""
1024
+ if self.items is None:
1025
+ raise ValueError(
1026
+ "PlacementContext.place() needs the drawing's render list — construct the context "
1027
+ "with items=dwg.items (the orchestrator and Drawing verbs already do; a unit test "
1028
+ "calling a render helper must pass it too)."
1029
+ )
1030
+ return place_annotation(self.registry, self.items, obj, name, view, feature)
1031
+
1010
1032
  def feature_of_hole_at(self, location):
1011
1033
  """The IR hole/pattern feature whose member sits at model-space *location*, or ``None``
1012
1034
  (#408/#639). Attributes a balloon (which carries a recognition hole, not the IR feature)
@@ -1107,6 +1129,7 @@ def place_strip_candidates(
1107
1129
  cands,
1108
1130
  tier,
1109
1131
  *,
1132
+ ctx,
1110
1133
  force=False,
1111
1134
  features=None,
1112
1135
  sizes=None,
@@ -1416,7 +1439,7 @@ def place_strip_candidates(
1416
1439
  for name, dim in placed:
1417
1440
  # Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
1418
1441
  # dims — `features` maps this batch's names to their source IR feature.
1419
- dwg.add(dim, name, view=view, feature=(features or {}).get(name))
1442
+ ctx.place(dim, name, view=view, feature=(features or {}).get(name))
1420
1443
  if tp is not None:
1421
1444
  tp["unplaced"] = [n for n, _ in todo]
1422
1445
  trace.end_pass(tp) # folds a standalone pass's items; no-op when corridor-nested
@@ -234,7 +234,7 @@ def _render_balloon(dwg, view, tag, j, hole, cx, cy, bx, by, fs, r, ctx):
234
234
  # Furniture that legitimately sits on the view geometry — exempt from the
235
235
  # annotation-overlap / centreline lint, as the section arrows do.
236
236
  balloon.is_centerline = True
237
- dwg.add(
237
+ ctx.place(
238
238
  balloon,
239
239
  f"balloon_{view}_{tag}_{j}",
240
240
  view=view,