draftwright 0.2.13__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 (130) hide show
  1. {draftwright-0.2.13 → draftwright-0.3.1}/CHANGELOG.md +110 -0
  2. {draftwright-0.2.13 → draftwright-0.3.1}/PKG-INFO +4 -2
  3. {draftwright-0.2.13 → draftwright-0.3.1}/pyproject.toml +18 -2
  4. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/__init__.py +8 -7
  5. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/_core.py +45 -45
  6. draftwright-0.3.1/src/draftwright/_geometry.py +50 -0
  7. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/analysis.py +226 -153
  8. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/annotations/_common.py +159 -7
  9. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/annotations/from_model.py +1074 -469
  10. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/annotations/holes.py +1033 -742
  11. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/annotations/orchestrator.py +112 -81
  12. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/annotations/sections.py +38 -37
  13. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/builder.py +15 -8
  14. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/cli.py +7 -13
  15. draftwright-0.2.13/src/draftwright/sheet.py → draftwright-0.3.1/src/draftwright/compose.py +84 -180
  16. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/drawing.py +603 -323
  17. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/export.py +21 -0
  18. draftwright-0.3.1/src/draftwright/layout.py +404 -0
  19. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/linting/coverage.py +47 -14
  20. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/linting/suggest.py +26 -0
  21. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/model/__init__.py +26 -0
  22. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/model/declare.py +532 -2
  23. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/model/detect.py +216 -24
  24. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/model/ir.py +182 -2
  25. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/model/planner.py +1 -1
  26. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/projection.py +4 -1
  27. draftwright-0.3.1/src/draftwright/recognition/__init__.py +125 -0
  28. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/recognition/_features.py +87 -32
  29. draftwright-0.3.1/src/draftwright/recognition/_record.py +28 -0
  30. draftwright-0.3.1/src/draftwright/recognition/chamfers.py +170 -0
  31. draftwright-0.3.1/src/draftwright/recognition/countersinks.py +145 -0
  32. draftwright-0.3.1/src/draftwright/recognition/fillets.py +161 -0
  33. draftwright-0.3.1/src/draftwright/recognition/flats.py +172 -0
  34. draftwright-0.3.1/src/draftwright/recognition/grooves.py +132 -0
  35. draftwright-0.3.1/src/draftwright/recognition/levels.py +175 -0
  36. draftwright-0.3.1/src/draftwright/recognition/plates.py +149 -0
  37. draftwright-0.3.1/src/draftwright/recognition/slots.py +675 -0
  38. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/recognition/turned.py +46 -18
  39. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/registry.py +5 -0
  40. draftwright-0.3.1/src/draftwright/score.py +70 -0
  41. draftwright-0.2.13/src/draftwright/sheet_dsl.py → draftwright-0.3.1/src/draftwright/sheet.py +94 -2
  42. draftwright-0.3.1/src/draftwright/sheet_dsl.py +35 -0
  43. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/sheet_emit.py +42 -8
  44. draftwright-0.3.1/tests/_kernel.py +32 -0
  45. draftwright-0.2.13/tests/test_layout_snapshot.py → draftwright-0.3.1/tests/_layout_sig.py +22 -72
  46. draftwright-0.2.13/tests/layout_snapshots/bracket.json → draftwright-0.3.1/tests/refactor_golden/bracket_section.json +1 -0
  47. draftwright-0.2.13/tests/layout_snapshots/slotted.json → draftwright-0.3.1/tests/refactor_golden/centered_rebate.json +86 -85
  48. draftwright-0.3.1/tests/refactor_golden/chamfered.json +135 -0
  49. draftwright-0.3.1/tests/refactor_golden/filleted.json +135 -0
  50. draftwright-0.2.13/tests/layout_snapshots/flange.json → draftwright-0.3.1/tests/refactor_golden/flange_dense.json +1 -0
  51. draftwright-0.3.1/tests/refactor_golden/grooved_shaft.json +149 -0
  52. draftwright-0.2.13/tests/layout_snapshots/box.json → draftwright-0.3.1/tests/refactor_golden/hex_bar.json +52 -51
  53. {draftwright-0.2.13/tests/layout_snapshots → draftwright-0.3.1/tests/refactor_golden}/holed_slot.json +1 -0
  54. draftwright-0.2.13/tests/layout_snapshots/dshape.json → draftwright-0.3.1/tests/refactor_golden/pocketed.json +113 -112
  55. draftwright-0.3.1/tests/refactor_golden/prismatic_ladder.json +153 -0
  56. {draftwright-0.2.13/tests/layout_snapshots → draftwright-0.3.1/tests/refactor_golden}/side_drilled.json +1 -0
  57. draftwright-0.2.13/tests/layout_snapshots/turned_shaft.json → draftwright-0.3.1/tests/refactor_golden/turned_stepped.json +1 -0
  58. draftwright-0.3.1/tests/test_carve_free_position_callers.py +161 -0
  59. draftwright-0.3.1/tests/test_declare.py +1414 -0
  60. draftwright-0.3.1/tests/test_drawing_encapsulation.py +193 -0
  61. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_e2e_standards.py +5 -5
  62. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_gdt_placement.py +1 -1
  63. draftwright-0.3.1/tests/test_import_boundaries.py +462 -0
  64. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_layout.py +3 -202
  65. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_layout_cleanliness.py +4 -3
  66. draftwright-0.3.1/tests/test_layout_hypothesis.py +205 -0
  67. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_layout_property.py +1 -1
  68. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_make_drawing.py +1585 -214
  69. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_part_model.py +15 -10
  70. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_pmi.py +33 -0
  71. draftwright-0.3.1/tests/test_private_test_imports.py +138 -0
  72. draftwright-0.3.1/tests/test_recogniser_contract.py +242 -0
  73. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_recognition.py +89 -89
  74. draftwright-0.3.1/tests/test_refactor_golden.py +219 -0
  75. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_render_seam.py +7 -2
  76. draftwright-0.3.1/tests/test_score.py +38 -0
  77. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_sheet_emit.py +235 -7
  78. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_sheet_gdt.py +1 -1
  79. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_sheet_notes.py +1 -1
  80. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_strip_layout.py +381 -85
  81. draftwright-0.3.1/tests/test_turned_steps.py +118 -0
  82. draftwright-0.2.13/src/draftwright/features.py +0 -23
  83. draftwright-0.2.13/src/draftwright/layout.py +0 -640
  84. draftwright-0.2.13/src/draftwright/recognition/__init__.py +0 -54
  85. draftwright-0.2.13/src/draftwright/recognition/levels.py +0 -51
  86. draftwright-0.2.13/src/draftwright/recognition/slots.py +0 -317
  87. draftwright-0.2.13/tests/_kernel.py +0 -19
  88. draftwright-0.2.13/tests/layout_snapshots/drive_screw_x.json +0 -155
  89. draftwright-0.2.13/tests/layout_snapshots/plate_holes.json +0 -258
  90. draftwright-0.2.13/tests/test_declare.py +0 -647
  91. draftwright-0.2.13/tests/test_turned_steps.py +0 -87
  92. {draftwright-0.2.13 → draftwright-0.3.1}/.gitignore +0 -0
  93. {draftwright-0.2.13 → draftwright-0.3.1}/LICENSE +0 -0
  94. {draftwright-0.2.13 → draftwright-0.3.1}/README.md +0 -0
  95. {draftwright-0.2.13 → draftwright-0.3.1}/skills/SKILL.md +0 -0
  96. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/annotate.py +0 -0
  97. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/annotations/__init__.py +0 -0
  98. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/fits.py +0 -0
  99. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  100. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  101. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  102. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  103. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/fonts/__init__.py +0 -0
  104. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/intents.py +0 -0
  105. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/linting/__init__.py +0 -0
  106. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/linting/issues.py +0 -0
  107. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/linting/structural.py +0 -0
  108. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/make_drawing.py +0 -0
  109. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/pmi.py +0 -0
  110. {draftwright-0.2.13 → draftwright-0.3.1}/src/draftwright/repair.py +0 -0
  111. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  112. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  113. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  114. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  115. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  116. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  117. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  118. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  119. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  120. {draftwright-0.2.13 → draftwright-0.3.1}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  121. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_e2e_slice.py +0 -0
  122. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_fits.py +0 -0
  123. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_lint_reconciliation.py +0 -0
  124. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_lint_structural.py +0 -0
  125. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_linting.py +0 -0
  126. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_object_aspects.py +0 -0
  127. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_registry.py +0 -0
  128. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_sheet_of.py +0 -0
  129. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_sheet_tables.py +0 -0
  130. {draftwright-0.2.13 → draftwright-0.3.1}/tests/test_tolerances.py +0 -0
@@ -1,5 +1,115 @@
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
+
33
+ ## v0.3.0 — 2026-07-14
34
+
35
+ **Breaking — feature-recogniser API renamed** (ADR 0013 / #568): *deliberate* breaks with
36
+ **no compatibility aliases**, hence the minor bump. Only import paths and callable
37
+ signatures change — but note this release also **changes drawing output**: new machined-
38
+ feature callouts, corrected leader anchors, and a reoriented isometric (see Added / Fixed).
39
+
40
+ ### Added
41
+
42
+ - **Machined-feature recognition — the #148 epic.** #135 deliberately handled only enclosed
43
+ rectangular through-slots; this broadens recognition, and each feature is recognised **and**
44
+ dimensioned **and** authorable (recognise + emit + declare, ADR 0011):
45
+ - **Blind slots + pockets** — floored recesses, `W × L × D DEEP` (#603).
46
+ - **Cross / intersecting slots** collapse into single continuous channels (#604).
47
+ - **Machined flats** on round stock — `{across} A/F` (#605).
48
+ - **Turned / circlip grooves** — `{width} WIDE × ø{floor}` (#606).
49
+ - **Slots / pockets in curved stock** — arc-bounded walls (#607).
50
+ - **Radiused-end (obround) slots** now report their **overall** length (#613).
51
+ - **Chamfer callouts** — `C{leg}` / `{leg}×{angle}°` off each bevel (#560).
52
+ - **Fillet radius callouts** — `R{radius}` off each rounded edge (#561).
53
+ - **Plate / wall thickness** dimensions on multi-plate prismatics (#559).
54
+ - **Prismatic step-position** dimensions locating each shoulder (#555).
55
+ - **Countersink callouts** — `ø6 THRU ⌵ ø14 × 90°` (major-ø + included angle), like the
56
+ counterbore `⊔ ø.. ↧..` (#558). The countersink rides on `HoleRecord`/`HoleFeature`, so
57
+ grouping and the callout-width layout estimate account for it.
58
+ - **Every recognised feature round-trips all three ADR-0011 surfaces (#574).** Beyond
59
+ detection, each feature now **emits** into the generated `--script` Sheet and is
60
+ **declarable** on the fluent `Sheet` surface — authored from scratch or read off a
61
+ build123d object: chamfer (#576), countersink (#575), plate (#577), step-position (#585),
62
+ fillet (#561), plus all the #148 features above.
63
+ - **`draftwright.score.feature_census(part)`** — a per-kind recognised-feature census, a
64
+ measurement tool for tracking recognition coverage (#608).
65
+
66
+ ### Changed
67
+
68
+ - **Feature recognisers renamed `find_`/`analyse_` → `recognise_`** and their
69
+ tuning/dependencies made keyword-only: `find_holes`→`recognise_holes`,
70
+ `find_bosses`→`recognise_bosses`, `find_hole_patterns`→`recognise_hole_patterns`,
71
+ `find_slots`→`recognise_slots`, `find_plates`→`recognise_plates`,
72
+ `find_chamfers`→`recognise_chamfers`, `find_turned_steps`→`recognise_turned_steps`,
73
+ `find_step_shoulders`→`recognise_step_shoulders`, `analyse_face_levels`→
74
+ `recognise_face_levels` (the last was also re-exported at top level as
75
+ `draftwright.analyse_face_levels` → now `draftwright.recognise_face_levels`).
76
+ Cylinder-analysis substrate (`analyse_cylinders`, `full_cylinders`,
77
+ `feature_diameters`) keeps its names.
78
+ - **`recognise_step_shoulders` now returns `list[StepShoulder]`** (a frozen dataclass)
79
+ instead of raw `(axis, position)` tuples; `levels` is keyword-only.
80
+ - **`recognise_turned_steps` now returns `list[TurnedStep]`** (empty for a non-turned
81
+ part) instead of `TurnedProfile | None`. `TurnedStep` gained an `axis` field (each step
82
+ is now a self-contained record); `TurnedProfile` remains only as a pipeline aggregate
83
+ (`TurnedProfile.from_steps(steps)`), no longer a recogniser return.
84
+ - **`recognise_face_levels` now returns `list[FaceLevel]`** (a frozen `FaceLevel(z)`
85
+ record) instead of `list[float]`.
86
+ - **Recognition record classes renamed to avoid clashing with the IR `Feature` types:**
87
+ `draftwright.recognition.HoleFeature` → `HoleRecord`, `BossFeature` → `BossRecord`
88
+ (the IR `draftwright.model.ir.HoleFeature`/`BossFeature` are unchanged).
89
+ - **Page/scale sizing now reads the IR, not recognition records (ADR 0008 / #588).** The
90
+ part model is built up front and drives sheet sizing, so detected and declared parts share
91
+ one path and no recogniser record reaches the sheet estimators. Byte-identical to the old
92
+ estimators except a hole pattern that shares a machining spec with loose holes now sizes
93
+ separately (no phantom merged `N×`), which can shift a tightly-packed such part's layout.
94
+
95
+ ### Fixed
96
+
97
+ - **Isometric view orientation (#620).** The iso camera viewed the *rear* (+Y) side against a
98
+ front-view orthographic set, mirroring asymmetric features; it now views from the same
99
+ front (−Y) / plan (+Z) / right (+X) combination.
100
+ - **Chamfer leaders (#621)** anchor on the bevel-face centroid (the diagonal middle), not the
101
+ supporting plane's parametric origin (which projected to a corner/endpoint).
102
+ - **Fillet leaders (#622)** anchor on the radius surface, not the face bounding-box centre
103
+ (which sat off the round, near the virtual sharp corner).
104
+ - **Authored `ref_pts`-only dimensions** now render (with Z-strip sizing) (#562).
105
+ - **Pitch-dim labels** are cleared off centrelines / bolt circles at creation time, not via
106
+ the repair loop (#129).
107
+
108
+ ### Removed
109
+
110
+ - **`draftwright.features` compatibility module** — deleted. Import `recognise_slots`/
111
+ `Slot` from `draftwright.recognition` instead.
112
+
3
113
  ## v0.2.13 — 2026-07-09
4
114
 
5
115
  **Compose-then-pack is now the layout authority.** This patch finishes the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.2.13
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
@@ -682,10 +682,12 @@ Classifier: Programming Language :: Python :: 3.13
682
682
  Classifier: Programming Language :: Python :: 3.14
683
683
  Classifier: Topic :: Scientific/Engineering
684
684
  Requires-Python: <3.15,>=3.10
685
- Requires-Dist: build123d-drafting-helpers>=0.13.0
685
+ 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.2.13"
7
+ version = "0.3.1"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -17,8 +17,13 @@ dependencies = [
17
17
  # 3.13+. Mirrors pzfreo/build123d-mcp's dependency handling.
18
18
  "build123d>=0.9.0,<0.11 ; python_full_version < '3.13'",
19
19
  "build123d>=0.11,<0.12 ; python_full_version >= '3.13'",
20
- "build123d-drafting-helpers>=0.13.0",
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",
@@ -111,6 +116,8 @@ ignore = ["E501"]
111
116
 
112
117
  [dependency-groups]
113
118
  dev = [
119
+ "codespell>=2.3.0",
120
+ "hypothesis>=6",
114
121
  "mypy>=2.1.0",
115
122
  "pytest>=8",
116
123
  "pytest-cov>=5",
@@ -119,6 +126,15 @@ dev = [
119
126
  "ruff>=0.4",
120
127
  ]
121
128
 
129
+ [tool.codespell]
130
+ # Spelling enforcement for the ADR 0013 contract (epic #584 WP3). Skips binary /
131
+ # generated / vendored assets: STEP fixtures, the lockfile, fonts, git internals.
132
+ skip = "*.stp,*.step,*.svg,*.dxf,*.pdf,uv.lock,.git,*.ttf,*.otf,src/draftwright/fonts/*"
133
+ # Valid words/identifiers codespell's dictionary misflags (calibrated from a clean run):
134
+ # `nd` a Dijkstra distance var, `tread` a real word (stair-tread faces), `accreting`
135
+ # a real word, `testof` the `TestOf` class name, and the valid hyphenated re-* forms.
136
+ ignore-words-list = "nd,tread,accreting,testof,re-using,re-declare,re-declared,re-declaring"
137
+
122
138
  [tool.uv]
123
139
  # On Python 3.13/3.14, build123d resolves to 0.11, whose OCP kernel is
124
140
  # cadquery-ocp-novtk (VTK dropped from the transitive tree — fine here, draftwright
@@ -23,19 +23,19 @@ from typing import TYPE_CHECKING
23
23
  # none of these names, so they stay sub-second instead of paying for the kernel
24
24
  # on every TAB press (#313). Each name maps to the submodule that provides it.
25
25
  _LAZY = {
26
- "analyse_face_levels": "draftwright.analysis",
26
+ "recognise_face_levels": "draftwright.recognition",
27
27
  "dedup_diams": "draftwright.analysis",
28
28
  "build_drawing": "draftwright.builder",
29
29
  "generate_script": "draftwright.builder",
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
 
@@ -72,14 +72,15 @@ _sys.modules[__name__].__class__ = _DraftwrightModule
72
72
 
73
73
 
74
74
  if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel cost
75
- from draftwright.analysis import analyse_face_levels, dedup_diams
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
- from draftwright.sheet import choose_scale
82
- from draftwright.sheet_dsl import Sheet
82
+ from draftwright.recognition import recognise_face_levels
83
+ from draftwright.sheet import Sheet
83
84
 
84
85
 
85
86
  def __dir__():
@@ -94,7 +95,7 @@ __all__ = [
94
95
  "FeatureInfo",
95
96
  "PmiRecord",
96
97
  "Sheet",
97
- "analyse_face_levels",
98
+ "recognise_face_levels",
98
99
  "build_drawing",
99
100
  "choose_scale",
100
101
  "dedup_diams",
@@ -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 (
@@ -31,6 +32,11 @@ from build123d_drafting.helpers import (
31
32
  format_drawing_scale,
32
33
  )
33
34
 
35
+ # The model-neutral geometry primitives (`_END_ON`, `_xyz`, `HoleRef`, `_axis_letter`)
36
+ # now live in the leaf `draftwright._geometry` so the IR waist (`model/`) can use them
37
+ # without importing this stage-level grab-bag (ADR 0008; #584 WP2). Re-exported here for
38
+ # the above-`_core` consumers (annotations/sheet/drawing/linting) that already import them.
39
+ from draftwright._geometry import _END_ON, HoleRef, _axis_letter, _xyz # noqa: F401
34
40
  from draftwright.fits import FitClass
35
41
  from draftwright.fonts import PLEX_MONO, PLEX_SANS_CONDENSED
36
42
  from draftwright.layout import _greedy_strip_1d, _solve_strip_1d
@@ -43,39 +49,6 @@ _MARGIN = 10.0
43
49
 
44
50
  _TB_CLEAR = _MARGIN + 1.0 # title-block inset: one extra mm over _MARGIN for clearance
45
51
 
46
- # The orthographic view a feature is dimensioned end-on in — the view normal to its
47
- # axis. Single source of truth shared by the planner (view choice) and the lint
48
- # coverage checks (where to look for a feature's dims). Orientation is data.
49
- _END_ON = {"x": "side", "y": "front", "z": "plan"}
50
-
51
-
52
- def _xyz(loc) -> tuple[float, float, float]:
53
- """A build123d ``Vector`` (has ``.X/.Y/.Z``) or an ``(x, y, z)`` sequence → an
54
- ``(x, y, z)`` float tuple. Shared by the detectors and the lint coverage checks
55
- so the Vector-unpacking idiom lives in one place."""
56
- if hasattr(loc, "X"):
57
- return (loc.X, loc.Y, loc.Z)
58
- x, y, z = loc
59
- return (float(x), float(y), float(z))
60
-
61
-
62
- @dataclass(frozen=True)
63
- class HoleRef:
64
- """A position-keyed reference to a hole — the IR-typed value the cover / hole-table
65
- bookkeeping matches on, so the shared escalation never needs a recogniser ``Hole``
66
- object (ADR 0008 Amendment 6). Built from any location via :meth:`of` (rounded, so
67
- two references at the same position compare equal)."""
68
-
69
- x: float
70
- y: float
71
- z: float
72
-
73
- @classmethod
74
- def of(cls, loc) -> HoleRef:
75
- x, y, z = _xyz(loc)
76
- return cls(round(x, 3), round(y, 3), round(z, 3))
77
-
78
-
79
52
  _FONT_SIZE = 3.0 # annotation text height (page-mm); the draft preset is built with this
80
53
 
81
54
 
@@ -161,17 +134,40 @@ def _text_width(text: str, font_size: float, font_path: str = PLEX_MONO) -> floa
161
134
  )
162
135
 
163
136
 
164
- def _axis_letter(obj) -> str:
165
- """Letter (``"x"``/``"y"``/``"z"``) of ``obj.axis``'s dominant component.
166
-
167
- ``obj`` is anything carrying an ``.axis`` 3-vector (a hole or a boss).
168
- """
169
- return max(zip("xyz", obj.axis, strict=True), key=lambda t: abs(t[1]))[0]
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
170
144
 
171
145
 
172
146
  _CONCENTRIC_TOL_MM = 0.5
173
147
 
174
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
+
175
171
  def _dim(p1, p2, side, distance, draft, **kwargs):
176
172
  """Build a :class:`Dimension`, tagged with its placement spec.
177
173
 
@@ -190,11 +186,14 @@ def _dim(p1, p2, side, distance, draft, **kwargs):
190
186
 
191
187
  # Dimension-line spacing (page-mm, scale-independent), the single source of truth for
192
188
  # BOTH the ADR 0009 strip carve (via the `Strip` dataclass defaults below) and the
193
- # 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
194
190
  # Y14.5, the FIRST dimension line sits furthest from the outline (clears the outline +
195
191
  # extension-line origins) and subsequent parallel lines stack tighter and uniform (#347).
196
192
  _STRIP_GAP = 10.0 # clearance between the view outline and the first dimension line
197
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
198
197
 
199
198
 
200
199
  @dataclass
@@ -389,7 +388,8 @@ def _largest_empty_rect(drawable, obstacles, *, warn: bool = True):
389
388
  @dataclass
390
389
  class DetailRequest:
391
390
  """A renderer's request for an enlarged detail of a region it could not draw
392
- 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``
393
393
  instead of building bespoke detail views; ``_resolve_details`` resolves them all
394
394
  through one generic detailer (crop → project → place → caption → marker), then
395
395
  calls ``redraw`` to draw the feature's own dims inside the placed detail view.
@@ -506,12 +506,12 @@ class Analysis:
506
506
  bbox_max: float
507
507
  holes: list
508
508
  patterns: list
509
- bosses: list # external bosses (find_bosses), detected once — the one inventory (#244)
509
+ bosses: list # external bosses (recognise_bosses), detected once — the one inventory (#244)
510
510
  slots: list
511
511
  z_diams: list[float]
512
512
  cross_diams: list[float]
513
513
  cyls: tuple[list, list]
514
- prof: TurnedProfile | None # turned step profile (find_turned_steps), detected once
514
+ prof: TurnedProfile | None # turned step profile (recognise_turned_steps), detected once
515
515
  od_diam: float | None
516
516
  is_rotational: bool
517
517
  od_axis: str # rotation/turning axis of a rotational part ("z" default; "x"/"y" #222)
@@ -636,7 +636,7 @@ def _iso_bbox(dwg):
636
636
 
637
637
 
638
638
  # --- page/scale selection + sheet-layout constants and helpers --------------
639
- # 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
640
640
  # (choose_scale/_layout_geometry) and make_drawing's repack pass, so they live
641
641
  # here in the shared base to keep the DAG acyclic.
642
642
  # The base inter-view corridor: one first-line gap + one dimension tier. Tracks
@@ -0,0 +1,50 @@
1
+ """Model-neutral geometry primitives — the leaf below both ``_core`` and ``model``.
2
+
3
+ These four helpers read an axis / coordinate / position off a build123d object
4
+ (or the IR) and carry no drawing, layout or page knowledge. They live here, not
5
+ in :mod:`draftwright._core`, so the IR waist (:mod:`draftwright.model`) can use
6
+ them without importing the stage-level drawing grab-bag (ADR 0008; #584 WP2).
7
+ This module imports nothing from ``draftwright`` — it is the bottom of the DAG.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+
14
+ # Axis letter -> the orthographic view a feature on that axis reads end-on in.
15
+ _END_ON = {"x": "side", "y": "front", "z": "plan"}
16
+
17
+
18
+ def _xyz(loc) -> tuple[float, float, float]:
19
+ """A build123d ``Vector`` (has ``.X/.Y/.Z``) or an ``(x, y, z)`` sequence → an
20
+ ``(x, y, z)`` float tuple. Shared by the detectors and the lint coverage checks
21
+ so the Vector-unpacking idiom lives in one place."""
22
+ if hasattr(loc, "X"):
23
+ return (loc.X, loc.Y, loc.Z)
24
+ x, y, z = loc
25
+ return (float(x), float(y), float(z))
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class HoleRef:
30
+ """A position-keyed reference to a hole — the IR-typed value the cover / hole-table
31
+ bookkeeping matches on, so the shared escalation never needs a recogniser ``Hole``
32
+ object (ADR 0008 Amendment 6). Built from any location via :meth:`of` (rounded, so
33
+ two references at the same position compare equal)."""
34
+
35
+ x: float
36
+ y: float
37
+ z: float
38
+
39
+ @classmethod
40
+ def of(cls, loc) -> HoleRef:
41
+ x, y, z = _xyz(loc)
42
+ return cls(round(x, 3), round(y, 3), round(z, 3))
43
+
44
+
45
+ def _axis_letter(obj) -> str:
46
+ """Letter (``"x"``/``"y"``/``"z"``) of ``obj.axis``'s dominant component.
47
+
48
+ ``obj`` is anything carrying an ``.axis`` 3-vector (a hole or a boss).
49
+ """
50
+ return max(zip("xyz", obj.axis, strict=True), key=lambda t: abs(t[1]))[0]