draftwright 0.3.0__tar.gz → 0.3.2__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 (132) hide show
  1. {draftwright-0.3.0 → draftwright-0.3.2}/CHANGELOG.md +75 -0
  2. {draftwright-0.3.0 → draftwright-0.3.2}/PKG-INFO +4 -2
  3. {draftwright-0.3.0 → draftwright-0.3.2}/pyproject.toml +8 -2
  4. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/__init__.py +4 -4
  5. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/_core.py +69 -20
  6. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/analysis.py +18 -13
  7. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/annotations/_common.py +300 -22
  8. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/annotations/from_model.py +945 -694
  9. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/annotations/holes.py +971 -803
  10. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/annotations/orchestrator.py +61 -55
  11. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/annotations/sections.py +11 -14
  12. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/builder.py +15 -10
  13. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/cli.py +7 -13
  14. draftwright-0.3.0/src/draftwright/sheet.py → draftwright-0.3.2/src/draftwright/compose.py +5 -2
  15. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/drawing.py +418 -234
  16. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/export.py +48 -0
  17. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/linting/coverage.py +32 -3
  18. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/linting/structural.py +102 -35
  19. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/fillets.py +23 -11
  20. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/registry.py +5 -0
  21. draftwright-0.3.0/src/draftwright/sheet_dsl.py → draftwright-0.3.2/src/draftwright/sheet.py +6 -1
  22. draftwright-0.3.2/src/draftwright/sheet_dsl.py +35 -0
  23. draftwright-0.3.2/tests/_kernel.py +32 -0
  24. draftwright-0.3.0/tests/test_layout_snapshot.py → draftwright-0.3.2/tests/_layout_sig.py +22 -72
  25. draftwright-0.3.0/tests/layout_snapshots/bracket.json → draftwright-0.3.2/tests/refactor_golden/bracket_section.json +1 -0
  26. draftwright-0.3.0/tests/layout_snapshots/slotted.json → draftwright-0.3.2/tests/refactor_golden/centered_rebate.json +86 -85
  27. draftwright-0.3.2/tests/refactor_golden/chamfered.json +135 -0
  28. draftwright-0.3.2/tests/refactor_golden/filleted.json +135 -0
  29. draftwright-0.3.0/tests/layout_snapshots/flange.json → draftwright-0.3.2/tests/refactor_golden/flange_dense.json +1 -0
  30. draftwright-0.3.2/tests/refactor_golden/grid_plate.json +474 -0
  31. draftwright-0.3.2/tests/refactor_golden/grooved_shaft.json +149 -0
  32. draftwright-0.3.0/tests/layout_snapshots/box.json → draftwright-0.3.2/tests/refactor_golden/hex_bar.json +52 -51
  33. {draftwright-0.3.0/tests/layout_snapshots → draftwright-0.3.2/tests/refactor_golden}/holed_slot.json +3 -2
  34. draftwright-0.3.2/tests/refactor_golden/pocketed.json +202 -0
  35. draftwright-0.3.2/tests/refactor_golden/prismatic_ladder.json +153 -0
  36. draftwright-0.3.2/tests/refactor_golden/scattered_plate.json +649 -0
  37. {draftwright-0.3.0/tests/layout_snapshots → draftwright-0.3.2/tests/refactor_golden}/side_drilled.json +1 -0
  38. draftwright-0.3.0/tests/layout_snapshots/turned_shaft.json → draftwright-0.3.2/tests/refactor_golden/turned_stepped.json +1 -0
  39. draftwright-0.3.2/tests/test_carve_free_position_callers.py +167 -0
  40. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_declare.py +32 -3
  41. draftwright-0.3.2/tests/test_detect_once.py +81 -0
  42. draftwright-0.3.2/tests/test_drawing_encapsulation.py +193 -0
  43. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_e2e_standards.py +5 -5
  44. draftwright-0.3.2/tests/test_export_dxf_zoom.py +65 -0
  45. draftwright-0.3.2/tests/test_fillets_adjacency.py +53 -0
  46. draftwright-0.3.2/tests/test_import_boundaries.py +462 -0
  47. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_layout_cleanliness.py +12 -4
  48. draftwright-0.3.2/tests/test_layout_hypothesis.py +205 -0
  49. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_layout_property.py +1 -1
  50. draftwright-0.3.2/tests/test_lint_box_cache.py +117 -0
  51. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_make_drawing.py +439 -114
  52. draftwright-0.3.2/tests/test_pitch_dim_footprint.py +157 -0
  53. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_pmi.py +33 -0
  54. draftwright-0.3.2/tests/test_private_test_imports.py +138 -0
  55. draftwright-0.3.2/tests/test_refactor_golden.py +258 -0
  56. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_render_seam.py +7 -2
  57. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_sheet_gdt.py +1 -1
  58. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_sheet_notes.py +1 -1
  59. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_strip_layout.py +54 -69
  60. draftwright-0.3.0/tests/_kernel.py +0 -19
  61. draftwright-0.3.0/tests/layout_snapshots/drive_screw_x.json +0 -155
  62. draftwright-0.3.0/tests/layout_snapshots/dshape.json +0 -219
  63. draftwright-0.3.0/tests/layout_snapshots/plate_holes.json +0 -258
  64. draftwright-0.3.0/tests/test_import_boundaries.py +0 -114
  65. {draftwright-0.3.0 → draftwright-0.3.2}/.gitignore +0 -0
  66. {draftwright-0.3.0 → draftwright-0.3.2}/LICENSE +0 -0
  67. {draftwright-0.3.0 → draftwright-0.3.2}/README.md +0 -0
  68. {draftwright-0.3.0 → draftwright-0.3.2}/skills/SKILL.md +0 -0
  69. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/_geometry.py +0 -0
  70. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/annotate.py +0 -0
  71. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/annotations/__init__.py +0 -0
  72. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/fits.py +0 -0
  73. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  74. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  75. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  76. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  77. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/fonts/__init__.py +0 -0
  78. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/intents.py +0 -0
  79. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/layout.py +0 -0
  80. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/linting/__init__.py +0 -0
  81. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/linting/issues.py +0 -0
  82. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/linting/suggest.py +0 -0
  83. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/make_drawing.py +0 -0
  84. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/model/__init__.py +0 -0
  85. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/model/declare.py +0 -0
  86. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/model/detect.py +0 -0
  87. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/model/ir.py +0 -0
  88. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/model/planner.py +0 -0
  89. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/pmi.py +0 -0
  90. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/projection.py +0 -0
  91. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/__init__.py +0 -0
  92. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/_features.py +0 -0
  93. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/_record.py +0 -0
  94. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/chamfers.py +0 -0
  95. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/countersinks.py +0 -0
  96. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/flats.py +0 -0
  97. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/grooves.py +0 -0
  98. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/levels.py +0 -0
  99. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/plates.py +0 -0
  100. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/slots.py +0 -0
  101. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/recognition/turned.py +0 -0
  102. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/repair.py +0 -0
  103. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/score.py +0 -0
  104. {draftwright-0.3.0 → draftwright-0.3.2}/src/draftwright/sheet_emit.py +0 -0
  105. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  106. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  107. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  108. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  109. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  110. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  111. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  112. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  113. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  114. {draftwright-0.3.0 → draftwright-0.3.2}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  115. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_e2e_slice.py +0 -0
  116. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_fits.py +0 -0
  117. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_gdt_placement.py +0 -0
  118. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_layout.py +0 -0
  119. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_lint_reconciliation.py +0 -0
  120. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_lint_structural.py +0 -0
  121. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_linting.py +0 -0
  122. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_object_aspects.py +0 -0
  123. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_part_model.py +0 -0
  124. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_recogniser_contract.py +0 -0
  125. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_recognition.py +0 -0
  126. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_registry.py +0 -0
  127. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_score.py +0 -0
  128. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_sheet_emit.py +0 -0
  129. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_sheet_of.py +0 -0
  130. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_sheet_tables.py +0 -0
  131. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_tolerances.py +0 -0
  132. {draftwright-0.3.0 → draftwright-0.3.2}/tests/test_turned_steps.py +0 -0
@@ -1,5 +1,80 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.2 — 2026-07-18
4
+
5
+ The **performance release**: the #602 perf loop (six PRs) plus the
6
+ build123d-drafting-helpers 0.14 upgrade. A dimension-dense drawing now builds
7
+ roughly **14× faster** than v0.3.1 (grid-plate benchmark 40 s → 2.8 s; NIST
8
+ CTC-02 ~107 s → well under half); the full test tier dropped ~14 min → ~3.5 min.
9
+
10
+ ### Performance
11
+
12
+ - **Placement measures, then builds** (#678, #679): corridor/strip candidates are
13
+ evaluated on analytical footprints (`dim_footprint`) or a single probe box; an
14
+ accepted candidate builds its OCC geometry exactly once and re-validates the
15
+ real box. Dimension constructions on the scattered-plate benchmark: 59 → 21
16
+ (= the placed count).
17
+ - **Detect once** (#682): the sizing model `_analyse` builds pre-scale is stored
18
+ on `Analysis.model` and reused as the render model — restoring ADR 0008
19
+ Amendment 5 ("one inventory, detected once"). ~13 s off CTC-02.
20
+ - **Fillet recognition de-quadratised** (#683): edge→faces adjacency map replaces
21
+ the O(faces²×edges²) `IsSame` sweep (3.7 M calls on CTC-02); 10.8 s → 4.6 s,
22
+ byte-identical records.
23
+ - **Lint bounding boxes memoised** (#681): `Drawing.lint()` 0.88 s → 0.07 s per
24
+ call (identity+location-token-checked cache beside the #143 view-edge cache).
25
+ - **DXF viewport from the known page window** (#680): skips `ezdxf.zoom.extents`'
26
+ per-entity walk; ~2.4 s per DXF export.
27
+ - **helpers 0.14** (#684): boolean-free dimension ink (upstream #177, filed and
28
+ fixed same-day) — ~25 ms per `Dimension`, was ~234 ms.
29
+
30
+ ### Changed
31
+
32
+ - **Tight-span dimensions now render honestly** (helpers 0.14): ISO outside
33
+ arrows where 0.13 silently degraded to a bare line. Ink extents grow on such
34
+ dims; placement models the flip analytically. Two placement-golden fixtures
35
+ re-blessed (ink extent only).
36
+ - Diameter-row leader labels space by **measured text width** and repair
37
+ helpers' shelf-flip collisions (crowded rows only; ordinary rows unchanged).
38
+ - A plate-thickness dim whose home strip corner is structurally contested falls
39
+ through to the opposite strip / side view instead of dropping (#684; the
40
+ underlying one-box-occupancy artifact is tracked as #685).
41
+
42
+ ### Fixed
43
+
44
+ - `recognise_fillets` neighbour tie-break is order-independent (#683 review).
45
+ - Lint's annotation-box cache prunes departed objects and detects in-place
46
+ relocation (#681 review).
47
+
48
+ ## v0.3.1 — 2026-07-17
49
+
50
+ Also lands the **#635 consolidation epic** (P1–P6, all six children closed): the render
51
+ layer's state bus is retired (`annotations/` reads zero `Drawing` privates,
52
+ machine-enforced), the three complexity hotspots are split (nesting ≤2), the whole-package
53
+ import DAG + a test-import ratchet are enforced, and the test suite gains a Hypothesis fuzz
54
+ tier + relational cross-kernel placement gates. All internal / behaviour-preserving
55
+ (byte-identical golden) — no output change.
56
+
57
+ ### Added
58
+
59
+ - **PNG raster export** — `--format png` on the CLI, or `Drawing.export(formats=("png",))`.
60
+ Rendered SVG → PDF → PNG via **pypdfium2** (Apache-2.0, Google PDFium BSD-3) + **Pillow**
61
+ (HPND) — both permissively licensed and pure-wheel, so PNG works cross-platform with **no
62
+ native cairo** (ADR 0006) and keeps the render path dual-license-friendly. `dpi=` sets the
63
+ raster resolution.
64
+
65
+ ### Changed
66
+
67
+ - **Unified export API** — `Drawing.export(out, *, formats=("pdf",)) → {format: path}` is now
68
+ the single entry point over `svg`/`dxf`/`pdf`/`png`; it returns a dict and internally handles
69
+ the SVG→PDF→PNG intermediate chain (writing/cleaning up intermediates it wasn't asked to keep).
70
+ The CLI `--format` gains `png`.
71
+
72
+ ### Deprecated
73
+
74
+ - `Drawing.export_pdf()` — use `export(formats=("pdf",))["pdf"]`; the method now warns.
75
+ - The legacy `export(svg=, dxf=)` boolean keywords and their `(svg_path, dxf_path)` tuple return
76
+ — kept for back-compat; prefer `formats=[...]` (the dict API).
77
+
3
78
  ## v0.3.0 — 2026-07-14
4
79
 
5
80
  **Breaking — feature-recogniser API renamed** (ADR 0013 / #568): *deliberate* breaks with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.3.0
3
+ Version: 0.3.2
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.1
685
+ Requires-Dist: build123d-drafting-helpers>=0.14.0
686
686
  Requires-Dist: build123d<0.11,>=0.9.0; python_full_version < '3.13'
687
687
  Requires-Dist: build123d<0.12,>=0.11; python_full_version >= '3.13'
688
688
  Requires-Dist: numpy>=1.24
689
+ Requires-Dist: pillow>=10
690
+ Requires-Dist: pypdfium2>=4
689
691
  Requires-Dist: reportlab>=4.0
690
692
  Requires-Dist: svglib>=1.5
691
693
  Requires-Dist: typer>=0.12
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.3.0"
7
+ version = "0.3.2"
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.1",
20
+ "build123d-drafting-helpers>=0.14.0",
21
21
  "numpy>=1.24",
22
+ # PNG raster export (SVG→PDF→PNG). Both permissively licensed (Pillow HPND; pypdfium2
23
+ # Apache-2.0 wrapping Google PDFium BSD-3) and ship pre-built wheels with NO native system
24
+ # deps — so PNG works cross-platform without cairo (ADR 0006) and stays dual-license-clean.
25
+ "pillow>=10",
26
+ "pypdfium2>=4",
22
27
  "reportlab>=4.0",
23
28
  "svglib>=1.5",
24
29
  "typer>=0.12",
@@ -112,6 +117,7 @@ ignore = ["E501"]
112
117
  [dependency-groups]
113
118
  dev = [
114
119
  "codespell>=2.3.0",
120
+ "hypothesis>=6",
115
121
  "mypy>=2.1.0",
116
122
  "pytest>=8",
117
123
  "pytest-cov>=5",
@@ -30,12 +30,12 @@ _LAZY = {
30
30
  "make_drawing": "draftwright.builder",
31
31
  "Drawing": "draftwright.drawing",
32
32
  "FeatureInfo": "draftwright.drawing",
33
- "Sheet": "draftwright.sheet_dsl",
33
+ "Sheet": "draftwright.sheet",
34
34
  "fix_svg_page_size": "draftwright.export",
35
35
  "lint_feature_coverage": "draftwright.linting",
36
36
  "PmiRecord": "draftwright.pmi",
37
37
  "extract_pmi": "draftwright.pmi",
38
- "choose_scale": "draftwright.sheet",
38
+ "choose_scale": "draftwright.compose",
39
39
  }
40
40
 
41
41
 
@@ -74,13 +74,13 @@ _sys.modules[__name__].__class__ = _DraftwrightModule
74
74
  if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel cost
75
75
  from draftwright.analysis import dedup_diams
76
76
  from draftwright.builder import build_drawing, generate_script, make_drawing
77
+ from draftwright.compose import choose_scale
77
78
  from draftwright.drawing import Drawing, FeatureInfo
78
79
  from draftwright.export import fix_svg_page_size
79
80
  from draftwright.linting import lint_feature_coverage
80
81
  from draftwright.pmi import PmiRecord, extract_pmi
81
82
  from draftwright.recognition import recognise_face_levels
82
- from draftwright.sheet import choose_scale
83
- from draftwright.sheet_dsl import Sheet
83
+ from draftwright.sheet import Sheet
84
84
 
85
85
 
86
86
  def __dir__():
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import functools
14
14
  import logging
15
+ import math
15
16
  import re
16
17
  from collections.abc import Callable
17
18
  from dataclasses import dataclass
@@ -20,8 +21,8 @@ from types import SimpleNamespace
20
21
  from typing import TYPE_CHECKING
21
22
 
22
23
  if TYPE_CHECKING:
24
+ from draftwright.compose import StripDepths
23
25
  from draftwright.recognition import TurnedProfile
24
- from draftwright.sheet import StripDepths
25
26
 
26
27
  from build123d import Align, BoundBox, Location, Mode, Shape, Text
27
28
  from build123d_drafting.helpers import (
@@ -106,8 +107,10 @@ _QUOTED_RE = re.compile(r"'([^']*)'")
106
107
 
107
108
 
108
109
  @functools.lru_cache(maxsize=512)
109
- def _text_width(text: str, font_size: float, font_path: str = PLEX_MONO) -> float:
110
- """Measured rendered width (page-mm) of *text* at *font_size*.
110
+ def _text_size(
111
+ text: str, font_size: float, font_path: str | None = PLEX_MONO, font: str = "Arial"
112
+ ) -> tuple[float, float]:
113
+ """Measured rendered (width, height) (page-mm) of *text* at *font_size*.
111
114
 
112
115
  Uses build123d's ``Text`` — the same primitive ``Dimension``/``HoleCallout``
113
116
  stroke their labels with — so callout-width estimates use real glyph metrics
@@ -115,27 +118,63 @@ def _text_width(text: str, font_size: float, font_path: str = PLEX_MONO) -> floa
115
118
  (``font_path``), not a system font *name*: name resolution substitutes a
116
119
  different font on Linux, which makes this estimate — and the layout it feeds —
117
120
  platform-variant (#149). The default is the same face the annotations render
118
- with, so estimate and render agree. Cached because the same numeric labels
119
- recur across holes and the rasterisation is the costly part.
121
+ with, so estimate and render agree. *font* only matters when a caller opts out
122
+ of path-pinning (``font_path=None``) then the *name* resolves through the OS
123
+ font stack, exactly as the renderer would. Cached because the same numeric
124
+ labels recur across holes and the rasterisation is the costly part.
120
125
  """
121
126
  if not text:
122
- return 0.0
123
- return (
124
- Text(
125
- txt=text,
126
- font_size=font_size,
127
- font_path=font_path,
128
- align=(Align.CENTER, Align.CENTER),
129
- mode=Mode.PRIVATE,
130
- )
131
- .bounding_box()
132
- .size.X
133
- )
127
+ return (0.0, 0.0)
128
+ bb = Text(
129
+ txt=text,
130
+ font_size=font_size,
131
+ font=font,
132
+ font_path=font_path,
133
+ align=(Align.CENTER, Align.CENTER),
134
+ mode=Mode.PRIVATE,
135
+ ).bounding_box()
136
+ return (bb.size.X, bb.size.Y)
137
+
138
+
139
+ def _text_width(text: str, font_size: float, font_path: str = PLEX_MONO) -> float:
140
+ """Measured rendered width (page-mm) of *text* — see :func:`_text_size`."""
141
+ return _text_size(text, font_size, font_path)[0]
142
+
143
+
144
+ # Cheap mean glyph advance as a fraction of the em (font size), for label-width ESTIMATES in
145
+ # the diameter row/column capacity checks. Plex Mono is monospaced at ~0.6 em, padded a touch.
146
+ # The exact metric is `_text_width()` above (measured per string), which the corridor/label
147
+ # placers use; these capacity gates only need a close bound, not the metric, so they trade
148
+ # exactness for not rasterising a text layout per candidate. The two may diverge for unusually
149
+ # wide/narrow glyph runs.
150
+ _EST_CHAR_WIDTH_EM = 0.62
134
151
 
135
152
 
136
153
  _CONCENTRIC_TOL_MM = 0.5
137
154
 
138
155
 
156
+ def _first_free_index(prefix: str, taken) -> int:
157
+ """The lowest ``j`` for which ``f"{prefix}{j}"`` is not in *taken* (a set or any container
158
+ supporting ``in``). The shared kernel of the first-free annotation-name allocators
159
+ (``_loc_name``/``_uniq``/``_hc_name``) — used where reusing a freed gap is fine. NOTE: the
160
+ step-length / diameter runs deliberately use ``max+1`` instead (``_next_start`` /
161
+ ``_next_steplen_start``), because a contiguous run started at a gap below an occupied index
162
+ would wrap onto it (#432); don't fold those onto this."""
163
+ j = 0
164
+ while f"{prefix}{j}" in taken:
165
+ j += 1
166
+ return j
167
+
168
+
169
+ def _concentric_with_axis(a, x: float, y: float) -> bool:
170
+ """True when the page/world point ``(x, y)`` lies on the rotational part's turned axis
171
+ (within :data:`_CONCENTRIC_TOL_MM`). A bore/pattern centred on the axis needs no location
172
+ dim — its position is the axis — so several passes filter such refs; this is the single
173
+ radial test they share (the perpendicular-plane distance to the axis centre ``(a.cx,
174
+ a.cy)``). Callers still gate on ``a.is_rotational`` / role / axis as their context needs."""
175
+ return math.hypot(x - a.cx, y - a.cy) <= _CONCENTRIC_TOL_MM
176
+
177
+
139
178
  def _dim(p1, p2, side, distance, draft, **kwargs):
140
179
  """Build a :class:`Dimension`, tagged with its placement spec.
141
180
 
@@ -154,11 +193,14 @@ def _dim(p1, p2, side, distance, draft, **kwargs):
154
193
 
155
194
  # Dimension-line spacing (page-mm, scale-independent), the single source of truth for
156
195
  # BOTH the ADR 0009 strip carve (via the `Strip` dataclass defaults below) and the
157
- # sheet.py halo/depth estimates that must reserve the same space. Per ISO 129-1 / ASME
196
+ # compose.py halo/depth estimates that must reserve the same space. Per ISO 129-1 / ASME
158
197
  # Y14.5, the FIRST dimension line sits furthest from the outline (clears the outline +
159
198
  # extension-line origins) and subsequent parallel lines stack tighter and uniform (#347).
160
199
  _STRIP_GAP = 10.0 # clearance between the view outline and the first dimension line
161
200
  _STRIP_SPACING = 2.5 # clear gap between successive parallel dimension lines (beyond the label)
201
+ # Small lift (page-mm) of an overall/envelope witness line off the projected silhouette edge,
202
+ # so its extension line reads as distinct from the outline rather than sitting on top of it.
203
+ _WITNESS_LIFT_MM = 2.0
162
204
 
163
205
 
164
206
  @dataclass
@@ -353,7 +395,8 @@ def _largest_empty_rect(drawable, obstacles, *, warn: bool = True):
353
395
  @dataclass
354
396
  class DetailRequest:
355
397
  """A renderer's request for an enlarged detail of a region it could not draw
356
- legibly at sheet scale (#307). Renderers append these to ``dwg._detail_requests``
398
+ legibly at sheet scale (#307). Renderers append these to the run's
399
+ ``PlacementContext.detail_requests``
357
400
  instead of building bespoke detail views; ``_resolve_details`` resolves them all
358
401
  through one generic detailer (crop → project → place → caption → marker), then
359
402
  calls ``redraw`` to draw the feature's own dims inside the placed detail view.
@@ -520,6 +563,12 @@ class Analysis:
520
563
  out: str
521
564
  pmi: list
522
565
  pmi_mode: str
566
+ # The PartModel built by _analyse's pre-scale sizing pass (#584 WP1 A) — stored so
567
+ # the render path reuses it instead of re-running the detectors (ADR 0008 Amdt 5:
568
+ # one inventory, detected once; #602). Typed `object` because model/ sits above
569
+ # _core in the DAG. None when the caller declared a model (ADR 0011) or on a
570
+ # manually-built Analysis — consumers fall back to build_model(a).
571
+ model: object | None = None
523
572
 
524
573
 
525
574
  _greedy_strip_ys = _greedy_strip_1d
@@ -600,7 +649,7 @@ def _iso_bbox(dwg):
600
649
 
601
650
 
602
651
  # --- page/scale selection + sheet-layout constants and helpers --------------
603
- # Relocated from make_drawing for the sheet.py split (#162). Shared by sheet.py
652
+ # Relocated from make_drawing for the compose.py (née sheet.py) split (#162). Shared by compose.py
604
653
  # (choose_scale/_layout_geometry) and make_drawing's repack pass, so they live
605
654
  # here in the shared base to keep the DAG acyclic.
606
655
  # The base inter-view corridor: one first-line gap + one dimension tier. Tracks
@@ -2,10 +2,10 @@
2
2
 
3
3
  `_analyse` imports the part (STEP or Shape), runs feature detection (holes,
4
4
  patterns, cylinders, face levels), classifies it (rotational vs prismatic),
5
- chooses the sheet (scale/page via `sheet.choose_scale`) and lays out the view
6
- zones (`sheet._layout_geometry`/`_build_zones`) — returning the `Analysis`
7
- namespace the rest of the pipeline reads. Sits above `sheet` and below
8
- `make_drawing` in the DAG.
5
+ chooses the sheet (scale/page via `compose.choose_scale`) and lays out the view
6
+ zones (`compose._layout_geometry`/`_build_zones`) — returning the `Analysis`
7
+ namespace the rest of the pipeline reads. Sits above `compose` (née `sheet`,
8
+ #640) and below `builder` in the DAG.
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -32,6 +32,15 @@ from draftwright._core import (
32
32
  _legible_steps,
33
33
  _Projector,
34
34
  )
35
+ from draftwright.compose import (
36
+ StripDepths,
37
+ _build_zones,
38
+ _est_hole_table_sizes,
39
+ _est_planned_bore_callout_width,
40
+ _layout_geometry,
41
+ _measure_strips,
42
+ choose_scale,
43
+ )
35
44
  from draftwright.model import build_part_model
36
45
  from draftwright.model.ir import Datum, PartModel, StepFeature
37
46
  from draftwright.model.planner import plan_dimensions
@@ -47,15 +56,6 @@ from draftwright.recognition import (
47
56
  recognise_turned_steps,
48
57
  step_level_zs,
49
58
  )
50
- from draftwright.sheet import (
51
- StripDepths,
52
- _build_zones,
53
- _est_hole_table_sizes,
54
- _est_planned_bore_callout_width,
55
- _layout_geometry,
56
- _measure_strips,
57
- choose_scale,
58
- )
59
59
 
60
60
  _log = logging.getLogger(__name__)
61
61
 
@@ -772,4 +772,9 @@ def _analyse(
772
772
  out=out,
773
773
  pmi=pmi_records,
774
774
  pmi_mode=pmi,
775
+ # The sizing model IS the render model when detection ran (identical inputs by
776
+ # construction — #584 WP1 A); store it so the pipeline never detects twice
777
+ # (ADR 0008 Amdt 5, #602). A declared model (layout_model) is NOT stored: the
778
+ # builder coerces + decorates the caller's model itself.
779
+ model=sizing_model if layout_model is None else None,
775
780
  )