draftwright 0.3.7__tar.gz → 0.3.9__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 (147) hide show
  1. {draftwright-0.3.7 → draftwright-0.3.9}/.gitignore +3 -0
  2. {draftwright-0.3.7 → draftwright-0.3.9}/CHANGELOG.md +106 -0
  3. {draftwright-0.3.7 → draftwright-0.3.9}/PKG-INFO +12 -2
  4. {draftwright-0.3.7 → draftwright-0.3.9}/README.md +10 -1
  5. {draftwright-0.3.7 → draftwright-0.3.9}/pyproject.toml +12 -1
  6. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/_core.py +29 -12
  7. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/analysis.py +4 -0
  8. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/annotations/_common.py +25 -2
  9. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/annotations/balloons.py +1 -1
  10. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/annotations/from_model.py +182 -77
  11. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/annotations/holes.py +243 -17
  12. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/annotations/orchestrator.py +33 -11
  13. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/annotations/sections.py +61 -46
  14. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/builder.py +46 -31
  15. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/cli.py +9 -0
  16. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/drawing.py +383 -91
  17. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/linting/coverage.py +7 -1
  18. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/linting/suggest.py +9 -9
  19. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/make_drawing.py +5 -4
  20. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/model/__init__.py +8 -0
  21. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/model/declare.py +258 -5
  22. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/model/detect.py +391 -135
  23. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/model/ir.py +79 -0
  24. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/model/planner.py +18 -5
  25. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/projection.py +48 -7
  26. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/__init__.py +18 -1
  27. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/_features.py +42 -21
  28. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/slots.py +506 -11
  29. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/turned.py +8 -2
  30. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/sheet.py +191 -12
  31. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/sheet_emit.py +240 -4
  32. draftwright-0.3.9/tests/fixtures/tuner_jig_blind_obround_pockets.step +4109 -0
  33. draftwright-0.3.9/tests/test_agents_guide.py +113 -0
  34. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_declare.py +112 -1
  35. draftwright-0.3.9/tests/test_detect_registry.py +166 -0
  36. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_drawing_encapsulation.py +65 -9
  37. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_e2e_standards.py +3 -3
  38. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_export_dxf_zoom.py +2 -2
  39. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_gdt_placement.py +41 -10
  40. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_import_boundaries.py +4 -5
  41. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_make_drawing.py +354 -103
  42. draftwright-0.3.9/tests/test_note_verb.py +59 -0
  43. draftwright-0.3.9/tests/test_pocket_pattern.py +259 -0
  44. draftwright-0.3.9/tests/test_pocket_pattern_recognition.py +191 -0
  45. draftwright-0.3.9/tests/test_private_test_attr_reads.py +298 -0
  46. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_recogniser_contract.py +56 -0
  47. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_render_seam.py +17 -6
  48. draftwright-0.3.9/tests/test_script_detail_parity.py +400 -0
  49. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_sheet_emit.py +45 -0
  50. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_sheet_notes.py +35 -0
  51. draftwright-0.3.9/tests/test_sheet_section.py +103 -0
  52. draftwright-0.3.9/tests/test_slot_pattern.py +151 -0
  53. draftwright-0.3.9/tests/test_slot_pattern_recognition.py +181 -0
  54. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_strip_layout.py +24 -16
  55. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_tolerances.py +118 -7
  56. draftwright-0.3.7/tests/test_script_detail_parity.py +0 -185
  57. {draftwright-0.3.7 → draftwright-0.3.9}/LICENSE +0 -0
  58. {draftwright-0.3.7 → draftwright-0.3.9}/docs/adr/README.md +0 -0
  59. {draftwright-0.3.7 → draftwright-0.3.9}/skills/SKILL.md +0 -0
  60. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/__init__.py +0 -0
  61. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/_geometry.py +0 -0
  62. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/annotate.py +0 -0
  63. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/annotations/__init__.py +0 -0
  64. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/compose.py +0 -0
  65. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/export.py +0 -0
  66. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/fits.py +0 -0
  67. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  68. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  69. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  70. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  71. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/fonts/__init__.py +0 -0
  72. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/intents.py +0 -0
  73. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/layout.py +0 -0
  74. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/linting/__init__.py +0 -0
  75. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/linting/issues.py +0 -0
  76. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/linting/structural.py +0 -0
  77. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/pmi.py +0 -0
  78. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/_record.py +0 -0
  79. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/chamfers.py +0 -0
  80. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/countersinks.py +0 -0
  81. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/fillets.py +0 -0
  82. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/flats.py +0 -0
  83. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/grooves.py +0 -0
  84. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/levels.py +0 -0
  85. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/recognition/plates.py +0 -0
  86. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/registry.py +0 -0
  87. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/repair.py +0 -0
  88. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/score.py +0 -0
  89. {draftwright-0.3.7 → draftwright-0.3.9}/src/draftwright/sheet_dsl.py +0 -0
  90. {draftwright-0.3.7 → draftwright-0.3.9}/tests/_kernel.py +0 -0
  91. {draftwright-0.3.7 → draftwright-0.3.9}/tests/_layout_sig.py +0 -0
  92. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
  93. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  94. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  95. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  96. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  97. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  98. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  99. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  100. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  101. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  102. {draftwright-0.3.7 → draftwright-0.3.9}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  103. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/bracket_section.json +0 -0
  104. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/centered_rebate.json +0 -0
  105. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/chamfered.json +0 -0
  106. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/filleted.json +0 -0
  107. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/flange_dense.json +0 -0
  108. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/grid_plate.json +0 -0
  109. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/grooved_shaft.json +0 -0
  110. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/hex_bar.json +0 -0
  111. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/holed_slot.json +0 -0
  112. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/pocketed.json +0 -0
  113. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/prismatic_ladder.json +0 -0
  114. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/scattered_plate.json +0 -0
  115. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/side_drilled.json +0 -0
  116. {draftwright-0.3.7 → draftwright-0.3.9}/tests/refactor_golden/turned_stepped.json +0 -0
  117. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_architecture_docs.py +0 -0
  118. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_carve_free_position_callers.py +0 -0
  119. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_dense_sheet_canary.py +0 -0
  120. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_detect_once.py +0 -0
  121. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_e2e_slice.py +0 -0
  122. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_fillets_adjacency.py +0 -0
  123. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_fits.py +0 -0
  124. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_layout.py +0 -0
  125. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_layout_cleanliness.py +0 -0
  126. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_layout_hypothesis.py +0 -0
  127. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_layout_property.py +0 -0
  128. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_lint_box_cache.py +0 -0
  129. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_lint_reconciliation.py +0 -0
  130. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_lint_structural.py +0 -0
  131. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_linting.py +0 -0
  132. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_object_aspects.py +0 -0
  133. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_occupancy_boxes.py +0 -0
  134. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_part_model.py +0 -0
  135. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_pitch_dim_footprint.py +0 -0
  136. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_pmi.py +0 -0
  137. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_private_test_imports.py +0 -0
  138. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_recognition.py +0 -0
  139. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_refactor_golden.py +0 -0
  140. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_registry.py +0 -0
  141. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_score.py +0 -0
  142. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_sheet_gdt.py +0 -0
  143. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_sheet_of.py +0 -0
  144. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_sheet_tables.py +0 -0
  145. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_solve_trace.py +0 -0
  146. {draftwright-0.3.7 → draftwright-0.3.9}/tests/test_turned_steps.py +0 -0
  147. {draftwright-0.3.7 → draftwright-0.3.9}/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,111 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.9 — 2026-07-26
4
+
5
+ ### Added
6
+
7
+ - **Repeated recesses are dimensioned as one pattern, not N competing callouts**
8
+ (#841): identical blind pockets and identical through-slots now collapse to a single
9
+ grouped `5× 7.9 × 13.6 × 19 DEEP` / `4× SLOT 8 × 30` leader plus `(n-1)× pitch`
10
+ dim(s). Previously each member competed for its own size dims and some silently
11
+ dropped for want of strip room — five declared slots rendered only three length dims.
12
+ Both new kinds (`PocketPatternFeature`, `SlotPatternFeature`) round-trip every
13
+ surface: declare (`sheet.pocket_pattern(...)` / `sheet.slot_pattern(...)`),
14
+ recognition from a real solid (coplanar, same-facing members only), the emitted
15
+ Sheet script, and the editable surface (`dwg.callout(feature)` + `finalize()`).
16
+ - **`Sheet.section()` and `Sheet.detail()` — ask for the view you need** (#841/#847):
17
+ a section A–A auto-fires only for a Z-axis hole with a counterbore, spotface, or
18
+ blind bottom, so a blind pocket's floor and depth stayed hidden-line-only with no
19
+ supported way to request a cut. `sheet.section(feature)` / `section(at=y)` /
20
+ bare `section()` force one (the cut plane is validated to lie strictly inside the
21
+ part), and `sheet.detail()` exposes the existing `detail_view=True` opt-in as a
22
+ chainable verb.
23
+ - **External threads are a first-class declared aspect** (#859): the turned analog of
24
+ the #764 internal thread — `sheet.step(shaft).thread("M3x0.5")`,
25
+ `sheet.diameter(...).thread(...)`, and `sheet.boss(...).thread(...)` append the
26
+ spec to the ⌀ callout (`ø3 M3x0.5`) instead of needing a free-text `.note()`. It
27
+ composes with `.finish()` for Ra-on-thread, keys the callout bucket on `(⌀, thread)`
28
+ so a threaded and a plain ø6 stay distinct, and now round-trips through the Sheet
29
+ emitter — including pattern-member holes, closing the symmetric latent #764 gap.
30
+ Declaration-only: a plain cylinder is geometrically indistinguishable from a
31
+ threaded one, so there is no recogniser. *(Known limitation: the incremental
32
+ `callout`/`finalize(only=…)` paths still dedup thread-blind — #863.)*
33
+ - **Blind obround pocket recognition, including imported STEP** (#837): the blind
34
+ counterpart of the #816 through-slot work. A stubby floored obround has side walls
35
+ too short to pair, so it is recovered from its semicircular end caps — clustering
36
+ faces by axis proximity so the quarter-cylinder split a STEP importer commonly
37
+ produces recognises as well as build123d's half-cylinder. A sealed internal void
38
+ (capped at both ends) is correctly *not* a pocket. A real tuner-jig STEP with five
39
+ blind pockets ships as a fixture.
40
+ - **`sheet.slot(...).note(...)` / `sheet.pocket(...).note(...)`** (#841/#845): the
41
+ slot/pocket handle now anchors a note to its own feature, with no `ref=` needed
42
+ (an explicit `ref` still forwards, as before).
43
+
44
+ ### Changed
45
+
46
+ - **An anchored note, GD&T frame, or finish relaxes its side instead of silently
47
+ vanishing** (#841/#855): an annotation declared with an explicit `view=`/`side=`
48
+ used to drop with only a warning when that strip was full, while the export still
49
+ reported success. The requested side is now a *preference* — when the strip has no
50
+ room the placer tries the opposite side, then the two perpendicular sides, taking
51
+ the first with room (via the same bounds and title-block checks, so it never
52
+ overshoots). A relaxed placement records an INFO `gdt_side_relaxed` issue naming
53
+ requested versus actual. Only when no strip fits does it drop.
54
+
55
+ ### Internal
56
+
57
+ - **The state-bus endgame is closed to its permanent seam** (#830/#840): the
58
+ solve trace is filled into `BuildState` at the one construction site, the redundant
59
+ part-model re-attach is gone, and the detail view is now *transactional* — geometry
60
+ is projected at the detail scale and committed only if dimensions land, so
61
+ `_drop_view_coordinates` has zero engine callers. With those removed, the #817
62
+ decision-D method-call exemption narrows from a blanket pass to a named
63
+ `_LAYOUT_SEAM` allowlist (`_add_view`, `_set_view_coordinates`), accepted as the
64
+ permanent interactive-layout seam; every other private-method call on the drawing
65
+ is now flagged.
66
+ - **A draftwright logo set** (#843) — drafting-idiom mark, all text outlined so the
67
+ SVGs render font-independently; the README now leads with the lockup.
68
+ - **A worked multi-feature object-reference example** (#853) in
69
+ `docs/multi-feature-object-reference-workflow.md`.
70
+
71
+ ## v0.3.8 — 2026-07-23
72
+
73
+ ### Added
74
+
75
+ - **`Drawing.note()` — a public free-text note verb** (#817/#820): place a free-form
76
+ `Note` at a page point without reaching for the low-level placement primitive —
77
+ `dwg.note("SEE NOTE 1", at=(x, y))`. This is the safe door for free text that the
78
+ privatisation below needs.
79
+ - **Obround (racetrack) through-slot recognition** (#816): a part whose only features
80
+ are obround through-slots is now recognised, dimensioned, and referenceable for GD&T.
81
+ Previously such slots were silently undetected — and lint falsely reported the drawing
82
+ complete. Stubby obrounds, whose straight side walls are too short to pair, are recovered
83
+ from their two semicircular end caps (a round hole is never mistaken for a slot end).
84
+
85
+ ### Changed
86
+
87
+ - **The generated Sheet script is now self-describing** (#833): `--script --style sheet`
88
+ emits a feature-census header, section sub-headers with repeat tallies (`8× R50`), and a
89
+ plain-language comment on each verb line (`# ⌀8 THRU ×4`, `# slot 40 × 120`, `# R50`).
90
+ All additions are comment-only, so re-run fidelity is unchanged.
91
+
92
+ ### Deprecated
93
+
94
+ - **The low-level `Drawing` placement API is now private — the public surface is
95
+ safe-by-default** (#817): `add`, `place_dim`, and the view-coordinate / build-state
96
+ plumbing (`set_view_coordinates`, `drop_view_coordinates`, `attach_part_model`,
97
+ `attach_solve_trace`, `add_view`, `clear_annotations`) are deprecated in favour of the
98
+ feature-backed verbs (`callout` / `dimension` / `locate` / `note` / …), which route
99
+ through the layout solve. Each keeps a `@deprecated` (PEP 702) shim for one release —
100
+ so type-checkers and IDEs flag call sites, not just a runtime warning — and is slated
101
+ for removal in ~0.5.0.
102
+
103
+ ### Internal
104
+
105
+ - **`AGENTS.md` — an AI-agent usage guide** (#818) with an executable check, steering
106
+ agents toward the safe declarative surface rather than the low-level primitives.
107
+ - **Coverage reporting is governed and a Codecov badge added** (#825/#832).
108
+
3
109
  ## v0.3.7 — 2026-07-21
4
110
 
5
111
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.3.7
3
+ Version: 0.3.9
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,15 @@ 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
- # draftwright
697
+ <h1 align="center">
698
+ <img src="docs/images/draftwright-logo.png" alt="draftwright" width="460">
699
+ </h1>
697
700
 
698
701
  [![CI](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml/badge.svg)](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml)
702
+ [![codecov](https://codecov.io/gh/pzfreo/draftwright/branch/main/graph/badge.svg)](https://codecov.io/gh/pzfreo/draftwright)
699
703
  [![PyPI](https://img.shields.io/pypi/v/draftwright.svg)](https://pypi.org/project/draftwright/)
700
704
  [![Python](https://img.shields.io/pypi/pyversions/draftwright.svg)](https://pypi.org/project/draftwright/)
701
705
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)
@@ -830,6 +834,12 @@ sheet.hole(features.m3_bore).thread("M3x0.5").finish("1.6") # tapped + Ra on t
830
834
  An object-sourced script carries an inline tip pointing at exactly this edit; a STEP-sourced
831
835
  script keeps the detected numbers (there's no object to reference).
832
836
 
837
+ See [`docs/multi-feature-object-reference-workflow.md`](docs/multi-feature-object-reference-workflow.md)
838
+ for a complete, runnable walkthrough of this pattern on a multi-feature turned part —
839
+ including the backward-compatible refactor, external threads (no `.thread()` on a step —
840
+ use `.note(...)`), and why an object-sourced script should point at the cheap geometry
841
+ variant of your source.
842
+
833
843
  ## What it produces
834
844
 
835
845
  - **Three orthographic views** (front, plan, side) sized and scaled automatically to the
@@ -1,6 +1,9 @@
1
- # draftwright
1
+ <h1 align="center">
2
+ <img src="docs/images/draftwright-logo.png" alt="draftwright" width="460">
3
+ </h1>
2
4
 
3
5
  [![CI](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml/badge.svg)](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml)
6
+ [![codecov](https://codecov.io/gh/pzfreo/draftwright/branch/main/graph/badge.svg)](https://codecov.io/gh/pzfreo/draftwright)
4
7
  [![PyPI](https://img.shields.io/pypi/v/draftwright.svg)](https://pypi.org/project/draftwright/)
5
8
  [![Python](https://img.shields.io/pypi/pyversions/draftwright.svg)](https://pypi.org/project/draftwright/)
6
9
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)
@@ -135,6 +138,12 @@ sheet.hole(features.m3_bore).thread("M3x0.5").finish("1.6") # tapped + Ra on t
135
138
  An object-sourced script carries an inline tip pointing at exactly this edit; a STEP-sourced
136
139
  script keeps the detected numbers (there's no object to reference).
137
140
 
141
+ See [`docs/multi-feature-object-reference-workflow.md`](docs/multi-feature-object-reference-workflow.md)
142
+ for a complete, runnable walkthrough of this pattern on a multi-feature turned part —
143
+ including the backward-compatible refactor, external threads (no `.thread()` on a step —
144
+ use `.note(...)`), and why an object-sourced script should point at the cheap geometry
145
+ variant of your source.
146
+
138
147
  ## What it produces
139
148
 
140
149
  - **Three orthographic views** (front, plan, side) sized and scaled automatically to the
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.3.7"
7
+ version = "0.3.9"
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
@@ -588,13 +604,14 @@ class DetailRequest:
588
604
  axis: part axis the band spans / is cropped along ("x"/"y"/"z").
589
605
  lo, hi: band bounds along ``axis`` (world mm).
590
606
  scale_needed: detail world→page scale that makes the region legible.
591
- redraw: ``redraw(dwg, view_name, detail_scale) -> int`` — draws the
592
- detail's dimensions in the placed detail view's coordinate system
593
- and returns the count placed (0 the detailer rolls the view
594
- back rather than leave an empty box). Called once the detail is
595
- placed; the main view always carries the located head/block
596
- inline regardless, so a placement failure loses no coverage (lint
597
- reports the un-located interior instead).
607
+ redraw: ``redraw(dwg, view_name, coords, detail_scale) -> int`` — draws the
608
+ detail's dimensions against *coords* (the detail's
609
+ :class:`ViewCoordinates`, mapped via ``coords.pp``) and returns the count
610
+ placed (0 the detailer drops the detail without committing a view, #840).
611
+ Called on the SCRATCH-projected coords before the view is committed, so a
612
+ zero result needs no rollback; the main view always carries the located
613
+ head/block inline regardless, so a placement failure loses no coverage
614
+ (lint reports the un-located interior instead).
598
615
  pad_top: page-mm band reserved above the detail view (a horizontal
599
616
  chain); reserved in the fit + placement.
600
617
  pads: optional ``pads(detail_scale) -> (pad_right, pad_top)`` for a
@@ -841,7 +858,7 @@ def _add_title_block(dwg, a: Analysis):
841
858
  bx + cell["max_x"],
842
859
  _TB_CLEAR + cell["max_y"],
843
860
  )
844
- dwg.add(tb, "title_block")
861
+ place_annotation(dwg.registry, dwg.items, tb, "title_block")
845
862
 
846
863
 
847
864
  def _make_sheet_frame(a: Analysis) -> Compound:
@@ -866,7 +883,7 @@ def _make_sheet_frame(a: Analysis) -> Compound:
866
883
  def _add_sheet_frame(dwg, a: Analysis):
867
884
  """Add the sheet border (#767), drawn last like the title block. No-op is the caller's
868
885
  (gated on ``a.frame``)."""
869
- dwg.add(_make_sheet_frame(a), "sheet_frame")
886
+ place_annotation(dwg.registry, dwg.items, _make_sheet_frame(a), "sheet_frame")
870
887
 
871
888
 
872
889
  def _add_projection_symbol(dwg, a: Analysis):
@@ -893,7 +910,7 @@ def _add_projection_symbol(dwg, a: Analysis):
893
910
  cy = _TB_CLEAR + _TB_H - h / 2 - 2
894
911
  sym = sym.locate(Location((cx - bx, cy - by, 0)))
895
912
  sym.is_projection_symbol = True
896
- dwg.add(sym, "projection_symbol")
913
+ place_annotation(dwg.registry, dwg.items, sym, "projection_symbol")
897
914
 
898
915
 
899
916
  def _add_zone_grid(dwg, a: Analysis):
@@ -926,12 +943,12 @@ def _add_zone_grid(dwg, a: Analysis):
926
943
  ticks.append(Edge.make_line(Vector(x1, yb, 0), Vector(x1 + tick, yb, 0)))
927
944
  grid = Compound(children=ticks)
928
945
  grid.is_zone_grid = True
929
- dwg.add(grid, "zone_grid")
946
+ place_annotation(dwg.registry, dwg.items, grid, "zone_grid")
930
947
 
931
948
  def _label(text, cx, cy, name):
932
949
  note = Note(text, (cx, cy), draft, align=(Align.CENTER, Align.CENTER))
933
950
  note.is_zone_label = True
934
- dwg.add(note, name)
951
+ place_annotation(dwg.registry, dwg.items, note, name)
935
952
 
936
953
  for i in range(cols): # numbers 1.. left→right, in the bottom + top bands
937
954
  cx = x0 + (i + 0.5) * cw
@@ -93,6 +93,10 @@ def _will_section(model, *, is_rotational=False, cx=0.0, cy=0.0) -> bool:
93
93
 
94
94
  if model is None:
95
95
  return False
96
+ # An explicit Sheet.section() request (ADR 0011, #841) reserves the row even when no
97
+ # hole gate qualifies — a blind pocket's floor/depth section has no driving Z hole.
98
+ if getattr(model, "decorations", {}).get("section") is not None:
99
+ return True
96
100
  features = getattr(model, "features", model)
97
101
 
98
102
  def feature_member(pt) -> bool:
@@ -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,