draftwright 0.2.3__tar.gz → 0.2.5__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 (85) hide show
  1. {draftwright-0.2.3 → draftwright-0.2.5}/CHANGELOG.md +40 -0
  2. {draftwright-0.2.3 → draftwright-0.2.5}/PKG-INFO +63 -49
  3. {draftwright-0.2.3 → draftwright-0.2.5}/README.md +62 -48
  4. {draftwright-0.2.3 → draftwright-0.2.5}/pyproject.toml +1 -1
  5. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/_core.py +34 -6
  6. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotate.py +1 -0
  7. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/_common.py +151 -4
  8. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/from_model.py +296 -112
  9. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/holes.py +502 -29
  10. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/orchestrator.py +83 -65
  11. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/sections.py +65 -7
  12. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/builder.py +188 -18
  13. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/cli.py +2 -5
  14. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/drawing.py +592 -15
  15. draftwright-0.2.5/src/draftwright/intents.py +39 -0
  16. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/model/planner.py +15 -9
  17. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/registry.py +50 -5
  18. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/repair.py +2 -2
  19. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/sheet.py +47 -0
  20. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/box.json +47 -47
  21. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/bracket.json +86 -86
  22. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/drive_screw_x.json +36 -36
  23. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/dshape.json +54 -54
  24. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/flange.json +66 -66
  25. draftwright-0.2.5/tests/layout_snapshots/holed_slot.json +299 -0
  26. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/plate_holes.json +61 -61
  27. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/side_drilled.json +50 -50
  28. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/slotted.json +72 -72
  29. {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/turned_shaft.json +79 -79
  30. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_e2e_standards.py +62 -0
  31. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_layout_cleanliness.py +13 -0
  32. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_layout_snapshot.py +18 -0
  33. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_make_drawing.py +1429 -20
  34. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_pmi.py +19 -17
  35. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_registry.py +28 -0
  36. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_render_seam.py +6 -2
  37. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_strip_layout.py +102 -8
  38. {draftwright-0.2.3 → draftwright-0.2.5}/.gitignore +0 -0
  39. {draftwright-0.2.3 → draftwright-0.2.5}/LICENSE +0 -0
  40. {draftwright-0.2.3 → draftwright-0.2.5}/skills/SKILL.md +0 -0
  41. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/__init__.py +0 -0
  42. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/analysis.py +0 -0
  43. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/__init__.py +0 -0
  44. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/export.py +0 -0
  45. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/features.py +0 -0
  46. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  47. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  48. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  49. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  50. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/__init__.py +0 -0
  51. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/layout.py +0 -0
  52. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/__init__.py +0 -0
  53. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/coverage.py +0 -0
  54. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/issues.py +0 -0
  55. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/structural.py +0 -0
  56. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/suggest.py +0 -0
  57. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/make_drawing.py +0 -0
  58. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/model/__init__.py +0 -0
  59. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/model/detect.py +0 -0
  60. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/model/ir.py +0 -0
  61. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/pmi.py +0 -0
  62. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/projection.py +0 -0
  63. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/__init__.py +0 -0
  64. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/_features.py +0 -0
  65. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/levels.py +0 -0
  66. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/slots.py +0 -0
  67. {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/turned.py +0 -0
  68. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  69. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  70. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  71. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  72. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  73. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  74. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  75. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  76. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  77. {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  78. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_e2e_slice.py +0 -0
  79. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_layout.py +0 -0
  80. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_layout_property.py +0 -0
  81. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_lint_structural.py +0 -0
  82. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_linting.py +0 -0
  83. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_part_model.py +0 -0
  84. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_recognition.py +0 -0
  85. {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_turned_steps.py +0 -0
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### Changed
6
+
7
+ - **Dimension-line spacing now follows ISO 129-1 / ASME Y14.5 convention** (#347).
8
+ The first dimension line sits further from the view outline (first-line gap
9
+ 8 → 10 mm) and successive parallel lines stack tighter and uniform (between-line
10
+ clear gap 4 → 2.5 mm). The inter-view corridor widens in step (`_DIM_PAD`
11
+ 18 → 20 mm) so the wider first-line gap does not crowd the between-view
12
+ dimensions. Re-drifts the layout of every drawing.
13
+
14
+ ## v0.2.4 — 2026-07-03
15
+
16
+ A follow-up patch on the ADR 0009 placement rebuild in 0.2.3: it finishes unifying
17
+ the shared "above-view" dimension corridor, adds a layout-overflow safety net, and
18
+ makes two more drop paths non-silent.
19
+
20
+ ### Changed
21
+
22
+ - **Plan-view X location dimensions, side-view Y location dimensions, and a
23
+ coincident slot-position dimension now share one collect-then-solve pass** (ADR
24
+ 0009 Amendment 6, #345/#346). Previously each pass carved the strip independently,
25
+ so a hole location and a slot position measuring the same datum span could both be
26
+ drawn, and the location ladder could come out non-monotonic. One solve now dedups
27
+ the coincident span (keeping the higher-priority location dimension) and orders the
28
+ whole ladder as segregated, monotonic runs — feature-size dimensions nearest the
29
+ view, datum locations nesting outward by distance.
30
+
31
+ ### Fixed
32
+
33
+ - **`choose_scale` never returns an overflowing layout** (#350). Scale selection
34
+ could pick a scale whose composed block layout exceeded the drawable area; it now
35
+ rejects any overflowing candidate.
36
+ - **A hole location and a coincident slot position are no longer drawn twice** (#345),
37
+ including at fractional datum distances where a display-value snap gap previously let
38
+ the duplicate escape deduplication.
39
+ - **The plan-view location ladder is monotonic** (#346) — running dimensions off a
40
+ shared datum stack outward in ascending order instead of interleaving.
41
+ - **A dropped balloon is non-silent** (#387). A balloon that cannot be placed now
42
+ reports the drop and clears its `callout_dropped` state precisely, instead of
43
+ vanishing with no on-sheet signal.
44
+
5
45
  ## v0.2.3 — 2026-07-03
6
46
 
7
47
  A large patch release: the **annotation-placement engine was rebuilt** as a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.2.3
3
+ Version: 0.2.5
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
@@ -700,24 +700,70 @@ Description-Content-Type: text/markdown
700
700
  Automated technical-drawing generation for [build123d](https://github.com/gumyr/build123d).
701
701
  Point it at a solid (or a STEP file) and get a fully-annotated multi-view engineering
702
702
  drawing — orthographic views, dimensions, section A–A, ISO hatching, title block — ready
703
- to export as SVG and DXF.
703
+ to export as PDF, SVG, and DXF.
704
+
705
+ ![Example engineering drawing generated by draftwright](docs/images/example.png)
706
+
707
+ *A mounting plate, generated automatically: three dimensioned orthographic views, a
708
+ counterbored bore callout (`⌀18 THRU ⊔ ⌀30 ↓14`), a `4× ⌀8 THRU` hole-pattern callout,
709
+ Section A–A with ISO hatching, an isometric, and an ISO 7200 title block — every
710
+ annotation placed by the engine.*
711
+
712
+ ## Quick start
713
+
714
+ ```
715
+ pip install draftwright
716
+ ```
717
+
718
+ ### Command line
719
+
720
+ Point it at a STEP file — that's the whole workflow:
721
+
722
+ ```
723
+ draftwright my_part.step --title "Mounting Plate" --number DWG-001
724
+ # writes my_part.pdf (the default)
725
+ ```
726
+
727
+ Choose formats, scale, and page; or emit an editable drawing script:
728
+
729
+ ```
730
+ draftwright my_part.step --format pdf,dxf # also: svg, all
731
+ draftwright my_part.step --scale 2 --page A3 # override the auto scale / page
732
+ draftwright my_part.step --script # write an editable .py drawing script
733
+ ```
734
+
735
+ `draftwright --help` lists every flag; `--version` prints the version.
736
+
737
+ ### Python
738
+
739
+ One call turns a build123d solid (or a STEP file) into a drawing. This is the exact
740
+ part in the image above:
704
741
 
705
742
  ```python
706
743
  from build123d import Box, Cylinder, Pos
707
744
  from draftwright import make_drawing
708
745
 
709
- part = Box(80, 60, 20) - Pos(0, 0, 5) * Cylinder(8, 20)
710
- make_drawing(part, out="my_part", title="Mounting Block", number="DWG-001")
711
- # writes my_part.svg and my_part.dxf
746
+ part = (
747
+ Box(100, 70, 24)
748
+ - Pos(0, 0, 0) * Cylinder(9, 40) # central bore, counterbored below
749
+ - Pos(0, 0, 8) * Cylinder(15, 20) # counterbore → triggers Section A–A
750
+ - Pos(-38, 24, 0) * Cylinder(4, 40) # 4× corner holes (recognised as a pattern)
751
+ - Pos(38, 24, 0) * Cylinder(4, 40)
752
+ - Pos(-38, -24, 0) * Cylinder(4, 40)
753
+ - Pos(38, -24, 0) * Cylinder(4, 40)
754
+ )
755
+ make_drawing(part, out="my_part", title="Mounting Plate", number="DWG-001")
756
+ # writes my_part.svg and my_part.dxf (or from a STEP file: make_drawing(step_file="p.step", out="my_part"))
712
757
  ```
713
758
 
714
- Or from the command line:
759
+ For inspection and editing, get a composable `Drawing`:
715
760
 
716
- ```
717
- draftwright my_part.step --title "Mounting Block" --number DWG-001
718
- # writes my_part.pdf (the default); add --format to choose:
719
- draftwright my_part.step --format pdf,dxf # PDF + DXF
720
- draftwright my_part.step --format all # PDF + SVG + DXF
761
+ ```python
762
+ from draftwright import build_drawing
763
+
764
+ dwg = build_drawing(part, title="Mounting Plate")
765
+ issues = dwg.lint() # list[LintIssue] coverage, page bounds, ISO
766
+ svg_path, dxf_path = dwg.export("my_part")
721
767
  ```
722
768
 
723
769
  ## What it produces
@@ -734,48 +780,14 @@ draftwright my_part.step --format all # PDF + SVG + DXF
734
780
  - **Lint** — `Drawing.lint()` checks annotation coverage, page bounds, and ISO compliance
735
781
  and returns structured `LintIssue` objects
736
782
 
737
- All output is real build123d geometry, so SVG and DXF export come from the same source
738
- and dimensions are live on the DXF layer.
739
-
740
- ## Installation
741
-
742
- ```
743
- pip install draftwright
744
- ```
783
+ All output is real build123d geometry, so PDF, SVG, and DXF all come from the same
784
+ source and dimensions are live on the DXF layer.
745
785
 
746
786
  Requires Python ≥ 3.10 and build123d ≥ 0.9.0. Annotation primitives are provided by
747
787
  [`build123d-drafting-helpers`](https://github.com/pzfreo/build123d-drafting-helpers),
748
- which is installed automatically as a dependency.
749
-
750
- ## Usage
788
+ installed automatically as a dependency.
751
789
 
752
- ### From a build123d solid
753
-
754
- ```python
755
- from draftwright import make_drawing, build_drawing, Drawing
756
-
757
- # One-shot: write SVG + DXF
758
- make_drawing(part, out="drawing", title="My Part", number="DWG-001")
759
-
760
- # Composable: get a Drawing object to inspect or extend
761
- dwg = build_drawing(part, title="My Part")
762
- issues = dwg.lint() # list[LintIssue]
763
- svg_path, dxf_path = dwg.export("drawing")
764
- ```
765
-
766
- ### From a STEP file
767
-
768
- ```python
769
- from draftwright import make_drawing
770
- make_drawing(step_file="part.step", out="drawing")
771
- ```
772
-
773
- Or via the CLI:
774
-
775
- ```
776
- draftwright part.step --out drawing --scale 2 --page A3
777
- draftwright part.step --script # write an editable .py drawing script instead
778
- ```
790
+ ## Going further
779
791
 
780
792
  ### Scale and page control
781
793
 
@@ -833,3 +845,5 @@ build → plan → render). See
833
845
  [`docs/target-architecture.md`](docs/target-architecture.md) and
834
846
  [`docs/adr/`](docs/adr/). The engine handles view layout (strip/zone model), scale
835
847
  selection, annotation placement, and section rendering.
848
+
849
+ The codebase has been entirely written by LLM (Claude Code) under detailed guidance from a person.
@@ -9,24 +9,70 @@
9
9
  Automated technical-drawing generation for [build123d](https://github.com/gumyr/build123d).
10
10
  Point it at a solid (or a STEP file) and get a fully-annotated multi-view engineering
11
11
  drawing — orthographic views, dimensions, section A–A, ISO hatching, title block — ready
12
- to export as SVG and DXF.
12
+ to export as PDF, SVG, and DXF.
13
+
14
+ ![Example engineering drawing generated by draftwright](docs/images/example.png)
15
+
16
+ *A mounting plate, generated automatically: three dimensioned orthographic views, a
17
+ counterbored bore callout (`⌀18 THRU ⊔ ⌀30 ↓14`), a `4× ⌀8 THRU` hole-pattern callout,
18
+ Section A–A with ISO hatching, an isometric, and an ISO 7200 title block — every
19
+ annotation placed by the engine.*
20
+
21
+ ## Quick start
22
+
23
+ ```
24
+ pip install draftwright
25
+ ```
26
+
27
+ ### Command line
28
+
29
+ Point it at a STEP file — that's the whole workflow:
30
+
31
+ ```
32
+ draftwright my_part.step --title "Mounting Plate" --number DWG-001
33
+ # writes my_part.pdf (the default)
34
+ ```
35
+
36
+ Choose formats, scale, and page; or emit an editable drawing script:
37
+
38
+ ```
39
+ draftwright my_part.step --format pdf,dxf # also: svg, all
40
+ draftwright my_part.step --scale 2 --page A3 # override the auto scale / page
41
+ draftwright my_part.step --script # write an editable .py drawing script
42
+ ```
43
+
44
+ `draftwright --help` lists every flag; `--version` prints the version.
45
+
46
+ ### Python
47
+
48
+ One call turns a build123d solid (or a STEP file) into a drawing. This is the exact
49
+ part in the image above:
13
50
 
14
51
  ```python
15
52
  from build123d import Box, Cylinder, Pos
16
53
  from draftwright import make_drawing
17
54
 
18
- part = Box(80, 60, 20) - Pos(0, 0, 5) * Cylinder(8, 20)
19
- make_drawing(part, out="my_part", title="Mounting Block", number="DWG-001")
20
- # writes my_part.svg and my_part.dxf
55
+ part = (
56
+ Box(100, 70, 24)
57
+ - Pos(0, 0, 0) * Cylinder(9, 40) # central bore, counterbored below
58
+ - Pos(0, 0, 8) * Cylinder(15, 20) # counterbore → triggers Section A–A
59
+ - Pos(-38, 24, 0) * Cylinder(4, 40) # 4× corner holes (recognised as a pattern)
60
+ - Pos(38, 24, 0) * Cylinder(4, 40)
61
+ - Pos(-38, -24, 0) * Cylinder(4, 40)
62
+ - Pos(38, -24, 0) * Cylinder(4, 40)
63
+ )
64
+ make_drawing(part, out="my_part", title="Mounting Plate", number="DWG-001")
65
+ # writes my_part.svg and my_part.dxf (or from a STEP file: make_drawing(step_file="p.step", out="my_part"))
21
66
  ```
22
67
 
23
- Or from the command line:
68
+ For inspection and editing, get a composable `Drawing`:
24
69
 
25
- ```
26
- draftwright my_part.step --title "Mounting Block" --number DWG-001
27
- # writes my_part.pdf (the default); add --format to choose:
28
- draftwright my_part.step --format pdf,dxf # PDF + DXF
29
- draftwright my_part.step --format all # PDF + SVG + DXF
70
+ ```python
71
+ from draftwright import build_drawing
72
+
73
+ dwg = build_drawing(part, title="Mounting Plate")
74
+ issues = dwg.lint() # list[LintIssue] coverage, page bounds, ISO
75
+ svg_path, dxf_path = dwg.export("my_part")
30
76
  ```
31
77
 
32
78
  ## What it produces
@@ -43,48 +89,14 @@ draftwright my_part.step --format all # PDF + SVG + DXF
43
89
  - **Lint** — `Drawing.lint()` checks annotation coverage, page bounds, and ISO compliance
44
90
  and returns structured `LintIssue` objects
45
91
 
46
- All output is real build123d geometry, so SVG and DXF export come from the same source
47
- and dimensions are live on the DXF layer.
48
-
49
- ## Installation
50
-
51
- ```
52
- pip install draftwright
53
- ```
92
+ All output is real build123d geometry, so PDF, SVG, and DXF all come from the same
93
+ source and dimensions are live on the DXF layer.
54
94
 
55
95
  Requires Python ≥ 3.10 and build123d ≥ 0.9.0. Annotation primitives are provided by
56
96
  [`build123d-drafting-helpers`](https://github.com/pzfreo/build123d-drafting-helpers),
57
- which is installed automatically as a dependency.
58
-
59
- ## Usage
97
+ installed automatically as a dependency.
60
98
 
61
- ### From a build123d solid
62
-
63
- ```python
64
- from draftwright import make_drawing, build_drawing, Drawing
65
-
66
- # One-shot: write SVG + DXF
67
- make_drawing(part, out="drawing", title="My Part", number="DWG-001")
68
-
69
- # Composable: get a Drawing object to inspect or extend
70
- dwg = build_drawing(part, title="My Part")
71
- issues = dwg.lint() # list[LintIssue]
72
- svg_path, dxf_path = dwg.export("drawing")
73
- ```
74
-
75
- ### From a STEP file
76
-
77
- ```python
78
- from draftwright import make_drawing
79
- make_drawing(step_file="part.step", out="drawing")
80
- ```
81
-
82
- Or via the CLI:
83
-
84
- ```
85
- draftwright part.step --out drawing --scale 2 --page A3
86
- draftwright part.step --script # write an editable .py drawing script instead
87
- ```
99
+ ## Going further
88
100
 
89
101
  ### Scale and page control
90
102
 
@@ -142,3 +154,5 @@ build → plan → render). See
142
154
  [`docs/target-architecture.md`](docs/target-architecture.md) and
143
155
  [`docs/adr/`](docs/adr/). The engine handles view layout (strip/zone model), scale
144
156
  selection, annotation placement, and section rendering.
157
+
158
+ The codebase has been entirely written by LLM (Claude Code) under detailed guidance from a person.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -162,6 +162,15 @@ def _dim(p1, p2, side, distance, draft, **kwargs):
162
162
  return d
163
163
 
164
164
 
165
+ # Dimension-line spacing (page-mm, scale-independent), the single source of truth for
166
+ # BOTH the ADR 0009 strip carve (via the `Strip` dataclass defaults below) and the
167
+ # sheet.py halo/depth estimates that must reserve the same space. Per ISO 129-1 / ASME
168
+ # Y14.5, the FIRST dimension line sits furthest from the outline (clears the outline +
169
+ # extension-line origins) and subsequent parallel lines stack tighter and uniform (#347).
170
+ _STRIP_GAP = 10.0 # clearance between the view outline and the first dimension line
171
+ _STRIP_SPACING = 2.5 # clear gap between successive parallel dimension lines (beyond the label)
172
+
173
+
165
174
  @dataclass
166
175
  class Strip:
167
176
  """A one-dimensional annotation band adjacent to an orthographic view.
@@ -184,8 +193,8 @@ class Strip:
184
193
  anchor: float
185
194
  outer_limit: float
186
195
  direction: float = 1.0
187
- gap: float = 8.0
188
- spacing: float = 4.0
196
+ gap: float = _STRIP_GAP
197
+ spacing: float = _STRIP_SPACING
189
198
 
190
199
  @property
191
200
  def available(self) -> float:
@@ -220,7 +229,6 @@ _SLOT_DIM_DEPTH = 2 * _FONT_SIZE + _PAD # sv_zones.below: overall depth dimensi
220
229
  _SLOT_DIM_HEIGHT = 2 * _FONT_SIZE + 2 * _PAD # fv_zones.right: overall height dim
221
230
 
222
231
 
223
- _STRIP_SPACING = 4.0 # page-mm between successive annotations in a strip
224
232
  _MIN_VIEW_MM = (
225
233
  10.0 # min projected view dimension; below it annotation geometry degenerates (#129)
226
234
  )
@@ -572,8 +580,11 @@ def _iso_bbox(dwg):
572
580
  # Relocated from make_drawing for the sheet.py split (#162). Shared by sheet.py
573
581
  # (choose_scale/_layout_geometry) and make_drawing's repack pass, so they live
574
582
  # here in the shared base to keep the DAG acyclic.
575
- _DIM_PAD = 18.0
576
- _STRIP_GAP = 8.0
583
+ # The base inter-view corridor: one first-line gap + one dimension tier. Tracks
584
+ # _STRIP_GAP so widening the first-line gap (#347) keeps the below-plan / between-view
585
+ # corridors from razor-fitting the first dim line (the #130 slack guarantee): 10 + 10.
586
+ _DIM_PAD = _STRIP_GAP + _SLOT_DIM_HEIGHT # 20.0
587
+ # _STRIP_GAP / _STRIP_SPACING are defined above (beside the `Strip` dataclass they seed).
577
588
 
578
589
  _PAGE_SIZES = {
579
590
  "A4": (297.0, 210.0),
@@ -583,7 +594,11 @@ _PAGE_SIZES = {
583
594
  "A0": (1189.0, 841.0),
584
595
  }
585
596
 
586
- _SCALES = [10.0, 5.0, 2.0, 1.0, 0.5, 0.2]
597
+ # ISO 5455 scale series (1-2-5 decades). Enlargements + 1:1 first, then reductions
598
+ # down to 1:10000 so a very large part still gets a scale that FITS rather than an
599
+ # overflowing layout (#350). Ordered largest-scale-first for "least reduction first".
600
+ _SCALES = [10.0, 5.0, 2.0, 1.0]
601
+ _SCALES += [0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005, 0.002, 0.001, 0.0005, 0.0002, 0.0001]
587
602
 
588
603
  # Horizontal page budget to reserve for the isometric view during scale
589
604
  # selection and view placement, as a fraction of bbox_max * scale. This is a
@@ -634,6 +649,19 @@ _LADDER = [
634
649
  (0.2, 841.0, 594.0, 150.0), # A1 1:5
635
650
  (0.5, 1189.0, 841.0, 150.0), # A0 1:2
636
651
  (0.2, 1189.0, 841.0, 150.0), # A0 1:5
652
+ # Past 1:5 keep reducing on A0 (the largest sheet) through the rest of the ISO 5455
653
+ # series, so a part too big for A0 1:5 still gets a scale that FITS rather than an
654
+ # overflowing layout (#350). A0 1:10000 holds anything up to ~8.4 m of drawn height.
655
+ (0.1, 1189.0, 841.0, 150.0), # A0 1:10
656
+ (0.05, 1189.0, 841.0, 150.0), # A0 1:20
657
+ (0.02, 1189.0, 841.0, 150.0), # A0 1:50
658
+ (0.01, 1189.0, 841.0, 150.0), # A0 1:100
659
+ (0.005, 1189.0, 841.0, 150.0), # A0 1:200
660
+ (0.002, 1189.0, 841.0, 150.0), # A0 1:500
661
+ (0.001, 1189.0, 841.0, 150.0), # A0 1:1000
662
+ (0.0005, 1189.0, 841.0, 150.0), # A0 1:2000
663
+ (0.0002, 1189.0, 841.0, 150.0), # A0 1:5000
664
+ (0.0001, 1189.0, 841.0, 150.0), # A0 1:10000
637
665
  ]
638
666
 
639
667
 
@@ -10,4 +10,5 @@ from draftwright.annotations.from_model import _detect_step_repeat # noqa: F401
10
10
  from draftwright.annotations.orchestrator import ( # noqa: F401
11
11
  _auto_annotate,
12
12
  _wrap_rows,
13
+ build_model,
13
14
  )
@@ -269,7 +269,137 @@ def _segment_hits_box(p1, p2, box) -> bool:
269
269
  return any(_seg_seg(p1, p2, corners[i], corners[(i + 1) % 4]) for i in range(4))
270
270
 
271
271
 
272
- def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False):
272
+ @dataclass
273
+ class CorridorCandidate:
274
+ """One datum-referenced linear dim collected for a shared corridor's single solve
275
+ (ADR 0009 end state, #345/#346). Multiple render passes (`render_locations`,
276
+ `render_slots`) feed the SAME above-view strip; committing per-pass interleaves the
277
+ dims and cannot dedup coincident spans. Each pass instead registers a candidate here;
278
+ one :func:`solve_corridor` per strip dedups, orders, and places the whole set.
279
+
280
+ Attributes:
281
+ name/build: the ``(name, pos->Dimension)`` pair :func:`place_strip_candidates`
282
+ consumes — unchanged.
283
+ order: sort key placing the candidate in the corridor ladder. Location dims
284
+ key on datum distance (the monotonic ISO ladder); size dims form a separate
285
+ contiguous run so a slot length never lands mid-ladder (#346).
286
+ dedup: coincidence key ``(view, meas-origin, meas-endpoint)`` on the MEASURED
287
+ axis, or ``None`` to never dedup (size dims). Two candidates with equal keys are
288
+ the same physical dimension; the higher-``precedence`` one survives (#345).
289
+ precedence: dedup survivor rank — a hole *location* dim (feeds coverage/table
290
+ escalation) outranks a coincident slot *position* line.
291
+ on_place/on_drop: the pass's own post-placement bookkeeping — coverage
292
+ registration / drop lint + `Escalation`, or a slot's below-side fallthrough.
293
+ force: policy-B force-keep after the corridor-respecting pass (locations have
294
+ no alternate view); size/position slot dims fall through instead (``on_drop``).
295
+ """
296
+
297
+ name: str
298
+ build: object
299
+ order: tuple
300
+ on_place: object
301
+ on_drop: object
302
+ dedup: tuple | None = None
303
+ precedence: int = 0
304
+ force: bool = False
305
+ # The source IR feature this dim was rendered for — recorded as provenance when the
306
+ # dim is placed at drain (ADR 0010). ``None`` leaves the annotation feature-less.
307
+ feature: object | None = None
308
+
309
+
310
+ def solve_corridor(dwg, strip, view, axis, cands, tier):
311
+ """One collect-then-solve over every :class:`CorridorCandidate` a shared strip
312
+ accumulated across passes (ADR 0009 end state). Dedup → order → one non-force
313
+ :func:`place_strip_candidates` pass → a force pass for the force-eligible leftovers →
314
+ dispatch each candidate's ``on_place``/``on_drop``. This is what removes the duplicate
315
+ span (#345) and the interleaved ladder (#346) by construction: a single solve sees the
316
+ full set, so coincident spans collapse and the order is one monotonic chain."""
317
+ if not cands:
318
+ return
319
+ # Dedup: keep the highest-precedence candidate per coincidence key (tie-break on name,
320
+ # deterministic — ADR 0001). A displaced duplicate is a *loser*: while its winner is
321
+ # drawn it is silently dropped (never starved, so firing its pass's drop lint would be a
322
+ # false report) — but if the winner itself fails to place, the top loser is promoted so
323
+ # the measurement still gets its pass's fallthrough/drop handling (no silent vanish).
324
+ winners: dict = {}
325
+ for c in cands:
326
+ if c.dedup is None:
327
+ continue
328
+ prev = winners.get(c.dedup)
329
+ # Winner: highest precedence, ties broken by the lexicographically smaller name.
330
+ if (
331
+ prev is None
332
+ or c.precedence > prev.precedence
333
+ or (c.precedence == prev.precedence and c.name < prev.name)
334
+ ):
335
+ winners[c.dedup] = c
336
+ kept = [c for c in cands if c.dedup is None or winners.get(c.dedup) is c]
337
+ losers: dict = {} # dedup key → its displaced candidates (highest precedence first)
338
+ for c in cands:
339
+ if c.dedup is not None and winners.get(c.dedup) is not c:
340
+ losers.setdefault(c.dedup, []).append(c)
341
+ for group in losers.values():
342
+ group.sort(key=lambda c: (-c.precedence, c.name))
343
+ kept.sort(key=lambda c: c.order)
344
+
345
+ def _promote_losers(dropped_winner):
346
+ # The winner did not place → hand its measurement to the best surviving loser
347
+ # (e.g. the slot position's below-strip fallthrough), then stop.
348
+ for loser in losers.get(dropped_winner.dedup, ()):
349
+ loser.on_drop(loser.name)
350
+ break
351
+
352
+ if strip is None: # no such strip on this drawing — every candidate drops
353
+ for c in kept:
354
+ c.on_drop(c.name)
355
+ if c.dedup is not None:
356
+ _promote_losers(c)
357
+ return
358
+ pairs = [(c.name, c.build) for c in kept]
359
+ feats = {c.name: c.feature for c in kept if c.feature is not None} # provenance (ADR 0010)
360
+ left = {
361
+ n for n, _ in place_strip_candidates(dwg, strip, view, axis, pairs, tier, features=feats)
362
+ }
363
+ force_pairs = [(c.name, c.build) for c in kept if c.name in left and c.force]
364
+ still = (
365
+ {
366
+ n
367
+ for n, _ in place_strip_candidates(
368
+ dwg, strip, view, axis, force_pairs, tier, force=True, features=feats
369
+ )
370
+ }
371
+ if force_pairs
372
+ else set()
373
+ )
374
+ for c in kept:
375
+ placed = c.name not in left or (c.force and c.name not in still)
376
+ if placed:
377
+ c.on_place(c.name) # placed in the corridor-respecting pass or the force pass
378
+ else:
379
+ c.on_drop(c.name) # dropped / not force-kept — the pass's drop handler runs
380
+ if c.dedup is not None: # a deduped winner failed → promote its top loser
381
+ _promote_losers(c)
382
+
383
+
384
+ def register_corridor(dwg, key, strip, view, axis, tier, cand):
385
+ """Queue a :class:`CorridorCandidate` under a shared corridor *key* so one
386
+ :func:`drain_corridors` places the whole cross-pass set together (ADR 0009 end state).
387
+ The first registration for a key fixes its ``(strip, view, axis, tier)``."""
388
+ b = dwg._corridor_batch.setdefault(
389
+ key, {"strip": strip, "view": view, "axis": axis, "tier": tier, "cands": []}
390
+ )
391
+ b["cands"].append(cand)
392
+
393
+
394
+ def drain_corridors(dwg):
395
+ """Solve every registered corridor (one :func:`solve_corridor` per strip), then clear
396
+ the batch. Called once, after all corridor-feeding passes have registered."""
397
+ for b in dwg._corridor_batch.values():
398
+ solve_corridor(dwg, b["strip"], b["view"], b["axis"], b["cands"], b["tier"])
399
+ dwg._corridor_batch = {}
400
+
401
+
402
+ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False, features=None):
273
403
  """Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
274
404
  The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
275
405
  P3): each candidate in *cands* — an ``(name, build(pos)->dim)`` pair — is spaced by
@@ -357,7 +487,9 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False):
357
487
  if not force and _box_hits(_geom_box(dim), blockers): # corridor crosses a leader
358
488
  todo.append((name, build))
359
489
  continue
360
- dwg.add(dim, name, view=view)
490
+ # Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
491
+ # dims — `features` maps this batch's names to their source IR feature.
492
+ dwg.add(dim, name, view=view, feature=(features or {}).get(name))
361
493
  return todo
362
494
 
363
495
 
@@ -372,8 +504,23 @@ def carve_free_position(dwg, strip, view, axis, tier, perp_span, *, outermost=Fa
372
504
  next — the height-ladder leapfrog chain, where each step dim's witness base is the
373
505
  previous dim's line — uses this. Same carve: outer-label tier reservation, the
374
506
  perpendicular-band filter (*perp_span* drops obstacles disjoint from this dim's own
375
- perpendicular extent), and innermost-first fill. No corridor check (a single-strip
376
- ladder has no alternate view to route to; obstacle tiers are still avoided)."""
507
+ perpendicular extent), and innermost-first fill.
508
+
509
+ **No corridor check, by construction — not just omission.** This avoids obstacle
510
+ *tiers* on the strip but does not reject a position whose witness *corridor* (feature
511
+ → dim line, across *perp_span*) crosses a leader/callout. Crucially, a single-position
512
+ return *cannot* fix a corridor crossing by choosing a different tier: every tier on
513
+ one side shares that corridor, and a farther tier's corridor is a **superset** of a
514
+ nearer one's, so the innermost free tier this already returns has the shortest
515
+ corridor and the fewest crossings — moving outward only adds crossings. Corridor
516
+ avoidance is therefore inherently a **relocation** problem (reject this position →
517
+ place on another view/side), which is :func:`place_strip_candidates`' job and out of
518
+ scope for a position return. Per caller: the height-ladder chain has no alternate
519
+ view (correct to omit); public ``Drawing.place_dim`` takes the view AND side from the
520
+ caller, so it cannot relocate; the PMI dim helpers already fall through sides
521
+ (``_try_above(...) or _try_below(...)``) and are where a corridor-reject would go if
522
+ ever wanted. Left as a documented known-limitation — the crossing is unobserved on
523
+ the corpus (the cleanliness ratchet would catch it)."""
377
524
  if strip is None:
378
525
  return None
379
526
  lo, hi, inner = strip_free_span(strip)