draftwright 0.1.9__tar.gz → 0.1.11__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 (28) hide show
  1. {draftwright-0.1.9 → draftwright-0.1.11}/CHANGELOG.md +82 -0
  2. {draftwright-0.1.9 → draftwright-0.1.11}/PKG-INFO +25 -28
  3. {draftwright-0.1.9 → draftwright-0.1.11}/README.md +22 -26
  4. {draftwright-0.1.9 → draftwright-0.1.11}/pyproject.toml +3 -2
  5. draftwright-0.1.11/skills/SKILL.md +273 -0
  6. draftwright-0.1.11/src/draftwright/_core.py +432 -0
  7. draftwright-0.1.11/src/draftwright/annotate.py +2039 -0
  8. draftwright-0.1.11/src/draftwright/layout.py +317 -0
  9. {draftwright-0.1.9 → draftwright-0.1.11}/src/draftwright/make_drawing.py +465 -2081
  10. {draftwright-0.1.9 → draftwright-0.1.11}/tests/test_e2e_standards.py +2 -2
  11. draftwright-0.1.11/tests/test_layout.py +255 -0
  12. {draftwright-0.1.9 → draftwright-0.1.11}/tests/test_make_drawing.py +499 -21
  13. draftwright-0.1.9/skills/SKILL.md +0 -183
  14. {draftwright-0.1.9 → draftwright-0.1.11}/.gitignore +0 -0
  15. {draftwright-0.1.9 → draftwright-0.1.11}/LICENSE +0 -0
  16. {draftwright-0.1.9 → draftwright-0.1.11}/src/draftwright/__init__.py +0 -0
  17. {draftwright-0.1.9 → draftwright-0.1.11}/src/draftwright/pmi.py +0 -0
  18. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  19. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  20. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  21. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  22. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  23. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  24. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  25. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  26. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  27. {draftwright-0.1.9 → draftwright-0.1.11}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  28. {draftwright-0.1.9 → draftwright-0.1.11}/tests/test_pmi.py +0 -0
@@ -1,5 +1,87 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## v0.1.11 — 2026-06-19
6
+
7
+ ### Changed
8
+
9
+ - **Feature-coverage lint is assembly-aware.** A general-arrangement drawing of
10
+ a multi-solid part deliberately omits each part's bores (they belong on detail
11
+ sheets), so `feature_not_dimensioned` / `feature_count_mismatch` are now
12
+ emitted at `info` rather than `warning` when the part is multi-solid — out of
13
+ the warning count and quality score, but still queryable. Auto-detected;
14
+ override with `build_drawing(..., assembly=True/False)` or
15
+ `lint_feature_coverage(..., assembly=...)` (#69).
16
+
17
+ ### Fixed
18
+
19
+ - **`place_dim` now labels the real-world length, not the page distance**, at
20
+ non-1:1 scale. Previously a dimension placed at a scale other than 1:1 showed
21
+ the on-page millimetre span instead of the true model dimension (#104).
22
+
23
+ ### Internal
24
+
25
+ - **`make_drawing.py` decomposed (#98).** The per-view projection math and the
26
+ analysis namespace were deduplicated and typed (the namespace is now a frozen
27
+ `Analysis` dataclass), and the annotation passes were extracted into a new
28
+ `draftwright.annotate` module on top of a shared `draftwright._core`. The
29
+ module graph is a DAG (`layout → _core → {make_drawing, annotate}`) and
30
+ `make_drawing.py` shrank from ~5,270 to ~2,930 lines. No public API or
31
+ behaviour change.
32
+
33
+ ## v0.1.10 — 2026-06-18
34
+
35
+ ### Added
36
+
37
+ - **Constraint-based layout engine (ADR 0003).** A new `draftwright.layout`
38
+ module with a `Placeable` protocol and a `LayoutSolver`: a 1D Cassowary strip
39
+ solver (`solve_strip`, with per-pair gaps) and a 2D free-rectangle placer
40
+ (`place_box` / `fit_box`) that positions a box in a free part of the page
41
+ clear of the views, title block, and existing annotations. Hole-callout and
42
+ turned-diameter placement now run on the solver. The engine grows per real
43
+ consumer; a monolithic global 2D solve is deferred (see the ADR).
44
+ - **Hole table + balloons (#93).** `dwg.add_table(rows)` places a generic data
45
+ table (gear data, BOM, revision block, …) in a free corner via `place_box`;
46
+ `dwg.add_hole_table(view)` builds a hole chart from the detected holes with a
47
+ circled balloon tag at each hole. A **too-dense plan view now auto-escalates**:
48
+ a part the layout cannot legibly dimension hole-by-hole is replaced by a
49
+ complete per-instance hole chart (`TAG | ⌀ | X | Y`, datum-relative) plus
50
+ balloons, instead of silently dropping callouts and location dims. The chart
51
+ wraps into multiple column-blocks to fit the page.
52
+ - **External turned diameters (#77).** A turned part lying along the X axis now
53
+ gets ø leader-callouts for its external stepped diameters, with thread/worm
54
+ patches collapsed into a single boss.
55
+ - **Pin / manual override (#89).** `dwg.pin(name)` / `dwg.unpin(name)` fix an
56
+ annotation's position so `repair()` — and the layout engine — never move it; a
57
+ deliberate (human or AI) placement wins over automatic layout.
58
+
59
+ ### Changed
60
+
61
+ - Hole-callout and turned-diameter placement is deconflicted through the shared
62
+ `LayoutSolver` instead of ad-hoc per-pass logic (no output change).
63
+
64
+ ### Fixed
65
+
66
+ - **Exact circles recovered for revolution silhouettes.** `project_to_viewport`'s
67
+ HLR returns the on-axis silhouette of a turned feature (or a concentric
68
+ gear-tooth-tip arc) as an approximating spline, not a true circle — splines in
69
+ the DXF where CAM expects `CIRCLE`/`ARC`, and fitted rather than exact radii.
70
+ `add_view` now refits any silhouette whose samples are equidistant from a
71
+ recognised revolution axis back to an exact circle/arc (#67).
72
+ - **Blind-hole depth no longer measured across solid boundaries.** On a
73
+ multi-solid assembly, coaxial bores in different bodies were merged into one
74
+ hole, reporting a depth spanning the inter-body gap (the ⌀9.8 ↓111.4 symptom).
75
+ Fixed upstream in `build123d-drafting-helpers` 0.10.1; the dependency pin is
76
+ bumped to `>=0.10.1` to pick it up (#68).
77
+
78
+ ### Docs
79
+
80
+ - The skill and generated-script header now lead with the domain API
81
+ (`features` / `place_dim` / `repair` / `lint_summary`) and the
82
+ build → critique → fix loop. ADR 0003 records the layout architecture; ADRs
83
+ 0001/0002 remain the editing-model and lint→repair foundations.
84
+
3
85
  ## v0.1.9 — 2026-06-16
4
86
 
5
87
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draftwright
3
- Version: 0.1.9
3
+ Version: 0.1.11
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
@@ -680,9 +680,10 @@ Classifier: Programming Language :: Python :: 3.11
680
680
  Classifier: Programming Language :: Python :: 3.12
681
681
  Classifier: Topic :: Scientific/Engineering
682
682
  Requires-Python: >=3.10
683
- Requires-Dist: build123d-drafting-helpers>=0.10.0
683
+ Requires-Dist: build123d-drafting-helpers>=0.10.1
684
684
  Requires-Dist: build123d>=0.9.0
685
685
  Requires-Dist: kiwisolver<2,>=1.4
686
+ Requires-Dist: numpy>=1.24
686
687
  Provides-Extra: pdf
687
688
  Requires-Dist: cairosvg>=2.7; extra == 'pdf'
688
689
  Description-Content-Type: text/markdown
@@ -720,7 +721,8 @@ draftwright my_part.step --title "Mounting Block" --number DWG-001
720
721
  - **Three orthographic views** (front, plan, side) sized and scaled automatically to the
721
722
  page
722
723
  - **Dimensions** on every principal envelope face, plus bore callouts (diameter, depth,
723
- counterbore, spotface) on all holes
724
+ counterbore, spotface) on all holes, and ø leader-callouts for the external stepped
725
+ diameters of turned parts
724
726
  - **Section A–A** with ISO 128-44 solid filled cutting-plane arrows and ISO 128-50 45°
725
727
  hatching on the cut face, triggered automatically when blind or stepped holes would
726
728
  otherwise be hidden-line-only
@@ -784,43 +786,38 @@ scale, page_w, page_h, n_steps = choose_scale(80, 60, 20, page="A3")
784
786
  make_drawing(part, out="drawing", scale=2.0, page="A2")
785
787
  ```
786
788
 
787
- ### Lint and coverage
789
+ ### Edit, critique, and self-repair
788
790
 
789
- ```python
790
- from draftwright import build_drawing, lint_feature_coverage
791
+ Edit a `Drawing` in **domain vocabulary** — the engine places annotations
792
+ automatically, so you say *what* to dimension, not *where*:
791
793
 
794
+ ```python
792
795
  dwg = build_drawing(part)
793
- issues = dwg.lint() # geometry lint + feature-coverage check
794
- for i in issues:
795
- print(i.severity, i.message)
796
+
797
+ # Inspect detected features and add a dimension in domain terms (auto-placed):
798
+ for f in dwg.features("plan"):
799
+ dwg.place_dim(f.page_pos, (f.page_pos[0] + f.diameter, f.page_pos[1]),
800
+ "below", "plan", dwg.draft, name="dim_pocket")
801
+
802
+ crit = dwg.lint_summary() # {"passed", "score", "by_code", "issues":[…suggestion]}
803
+ dwg.repair() # auto-fix mechanically-fixable lint; never worsens
796
804
  ```
797
805
 
806
+ Each `LintIssue` carries a domain-meaningful `code` and, when computable, a
807
+ ready-to-apply `suggestion`. See `docs/adr/` for the design (deterministic
808
+ generation, the lint→repair loop, and the constraint-based layout engine).
809
+
798
810
  ## Architecture
799
811
 
800
- draftwright is a single module (`make_drawing.py`) on top of two Apache 2.0 libraries:
812
+ draftwright is a single module (`make_drawing.py`) on top of two libraries:
801
813
 
802
814
  ```
803
- draftwright (AGPL-3.0)
804
- └── build123d-drafting-helpers (Apache 2.0) — Dimension, Leader, HoleCallout, …
805
- └── build123d (Apache 2.0) — CAD kernel
815
+ draftwright
816
+ └── build123d-drafting-helpers — Dimension, Leader, HoleCallout, …
817
+ └── build123d — CAD kernel
806
818
  ```
807
819
 
808
820
  The engine handles view layout (strip/zone model), scale selection, feature recognition
809
821
  (holes, bosses, bolt circles), annotation placement, and section view generation.
810
822
  Annotation primitives (`Dimension`, `Leader`, `lint_drawing`, etc.) live in
811
823
  `build123d-drafting-helpers` and can be used independently.
812
-
813
- ## License
814
-
815
- draftwright is licensed under the **GNU Affero General Public License v3 (AGPL-3.0)**.
816
-
817
- This means:
818
- - You can use, modify, and distribute it freely in open-source projects
819
- - If you run draftwright as part of a **network service** (e.g. an API that generates
820
- drawings), you must make your application's source code available to users
821
-
822
- For commercial use without these obligations, contact pzfreo@gmail.com.
823
-
824
- The annotation primitives in
825
- [`build123d-drafting-helpers`](https://github.com/pzfreo/build123d-drafting-helpers)
826
- are Apache 2.0 and may be embedded in commercial applications without restriction.
@@ -31,7 +31,8 @@ draftwright my_part.step --title "Mounting Block" --number DWG-001
31
31
  - **Three orthographic views** (front, plan, side) sized and scaled automatically to the
32
32
  page
33
33
  - **Dimensions** on every principal envelope face, plus bore callouts (diameter, depth,
34
- counterbore, spotface) on all holes
34
+ counterbore, spotface) on all holes, and ø leader-callouts for the external stepped
35
+ diameters of turned parts
35
36
  - **Section A–A** with ISO 128-44 solid filled cutting-plane arrows and ISO 128-50 45°
36
37
  hatching on the cut face, triggered automatically when blind or stepped holes would
37
38
  otherwise be hidden-line-only
@@ -95,43 +96,38 @@ scale, page_w, page_h, n_steps = choose_scale(80, 60, 20, page="A3")
95
96
  make_drawing(part, out="drawing", scale=2.0, page="A2")
96
97
  ```
97
98
 
98
- ### Lint and coverage
99
+ ### Edit, critique, and self-repair
99
100
 
100
- ```python
101
- from draftwright import build_drawing, lint_feature_coverage
101
+ Edit a `Drawing` in **domain vocabulary** — the engine places annotations
102
+ automatically, so you say *what* to dimension, not *where*:
102
103
 
104
+ ```python
103
105
  dwg = build_drawing(part)
104
- issues = dwg.lint() # geometry lint + feature-coverage check
105
- for i in issues:
106
- print(i.severity, i.message)
106
+
107
+ # Inspect detected features and add a dimension in domain terms (auto-placed):
108
+ for f in dwg.features("plan"):
109
+ dwg.place_dim(f.page_pos, (f.page_pos[0] + f.diameter, f.page_pos[1]),
110
+ "below", "plan", dwg.draft, name="dim_pocket")
111
+
112
+ crit = dwg.lint_summary() # {"passed", "score", "by_code", "issues":[…suggestion]}
113
+ dwg.repair() # auto-fix mechanically-fixable lint; never worsens
107
114
  ```
108
115
 
116
+ Each `LintIssue` carries a domain-meaningful `code` and, when computable, a
117
+ ready-to-apply `suggestion`. See `docs/adr/` for the design (deterministic
118
+ generation, the lint→repair loop, and the constraint-based layout engine).
119
+
109
120
  ## Architecture
110
121
 
111
- draftwright is a single module (`make_drawing.py`) on top of two Apache 2.0 libraries:
122
+ draftwright is a single module (`make_drawing.py`) on top of two libraries:
112
123
 
113
124
  ```
114
- draftwright (AGPL-3.0)
115
- └── build123d-drafting-helpers (Apache 2.0) — Dimension, Leader, HoleCallout, …
116
- └── build123d (Apache 2.0) — CAD kernel
125
+ draftwright
126
+ └── build123d-drafting-helpers — Dimension, Leader, HoleCallout, …
127
+ └── build123d — CAD kernel
117
128
  ```
118
129
 
119
130
  The engine handles view layout (strip/zone model), scale selection, feature recognition
120
131
  (holes, bosses, bolt circles), annotation placement, and section view generation.
121
132
  Annotation primitives (`Dimension`, `Leader`, `lint_drawing`, etc.) live in
122
133
  `build123d-drafting-helpers` and can be used independently.
123
-
124
- ## License
125
-
126
- draftwright is licensed under the **GNU Affero General Public License v3 (AGPL-3.0)**.
127
-
128
- This means:
129
- - You can use, modify, and distribute it freely in open-source projects
130
- - If you run draftwright as part of a **network service** (e.g. an API that generates
131
- drawings), you must make your application's source code available to users
132
-
133
- For commercial use without these obligations, contact pzfreo@gmail.com.
134
-
135
- The annotation primitives in
136
- [`build123d-drafting-helpers`](https://github.com/pzfreo/build123d-drafting-helpers)
137
- are Apache 2.0 and may be embedded in commercial applications without restriction.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.1.9"
7
+ version = "0.1.11"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -12,8 +12,9 @@ authors = [{ name = "Paul Fremantle", email = "pzfreo@gmail.com" }]
12
12
  requires-python = ">=3.10"
13
13
  dependencies = [
14
14
  "build123d>=0.9.0",
15
- "build123d-drafting-helpers>=0.10.0",
15
+ "build123d-drafting-helpers>=0.10.1",
16
16
  "kiwisolver>=1.4,<2",
17
+ "numpy>=1.24",
17
18
  ]
18
19
  keywords = ["build123d", "cad", "drafting", "engineering-drawing", "technical-drawing", "automation"]
19
20
  classifiers = [
@@ -0,0 +1,273 @@
1
+ # Automated Engineering Drawing with draftwright
2
+
3
+ Use this skill when asked to create an engineering drawing automatically from a
4
+ build123d solid or STEP file. It requires the **draftwright** package (AGPL-3.0)
5
+ to be installed in the execution environment.
6
+
7
+ > **License notice**: draftwright is AGPL-3.0. If you deploy code that uses it
8
+ > as part of a network service you must make your application source available.
9
+ > For Apache 2.0 annotation primitives only (`Dimension`, `Leader`, `TitleBlock`,
10
+ > etc.), use `build123d-drafting-helpers` instead without this obligation.
11
+
12
+ **There are two paths — start with the automatic one.**
13
+
14
+ 1. **Automatic (`make_drawing`)** — one call turns a part (or STEP file) into a
15
+ four-view SVG + DXF with dimensions, centrelines, and an ISO 7200 title
16
+ block.
17
+ 2. **Builder (`build_drawing`)** — the same pipeline, but it hands back a live
18
+ `Drawing` you can edit before export.
19
+
20
+ Requires `draftwright >= 0.1.9` and `build123d-drafting-helpers >= 0.10.1`.
21
+ Install: `pip install draftwright`.
22
+
23
+ **Design model (worth knowing before you edit):** the engine is *deterministic*
24
+ — no AI inside it — and you refine a drawing by **stating domain intent**
25
+ (dimension this feature, section through here) and letting the engine **place
26
+ everything automatically** (placement is constraint-based; you never compute page
27
+ coordinates). When the first pass isn't perfect, you drive a **build → critique
28
+ → fix** loop (Step "Lint → critique → fix" below), not a hand-layout edit. The
29
+ rationale lives in `docs/adr/`: 0001 (deterministic generation over an editable
30
+ DSL), 0002 (the lint critique → domain-repair loop), 0003 (the constraint-based
31
+ layout engine). Edit through the domain API; treat `Placeable`/page mechanics as
32
+ internals.
33
+
34
+ ---
35
+
36
+ ## Step 0 — Understand the part first
37
+
38
+ ```
39
+ mcp__build123d-mcp__execute — build the part in the session
40
+ mcp__build123d-mcp__measure — confirm volume, bbox, face count
41
+ mcp__build123d-mcp__render_view (save_to='/tmp/preview.png') — visual sanity check
42
+ ```
43
+
44
+ Register the part under a stable name with `show(part, "part")`.
45
+
46
+ ---
47
+
48
+ ## Step 1 — Generate the drawing automatically (start here)
49
+
50
+ ```python
51
+ from draftwright import make_drawing
52
+
53
+ svg, dxf = make_drawing(
54
+ part, # an in-session build123d object, OR a "path/to/part.step"
55
+ out="drawings/bracket", # output stem; ".svg"/".dxf" are appended
56
+ title="BRACKET", # ISO 7200 document title
57
+ number="DWG-042", # ISO 7200 document identifier
58
+ tolerance="ISO 2768-f", # general tolerance
59
+ drawn_by="Your Name",
60
+ )
61
+ ```
62
+
63
+ `make_drawing` chooses the scale + ISO page size, projects front/plan/side/iso
64
+ views, and annotates automatically — then lints and writes both SVG and DXF.
65
+
66
+ Automatic annotation covers **prismatic parts in full**: every recognised hole
67
+ gets a grouped callout ("4× ø10 THRU", counterbore/depth symbols), bolt circles
68
+ get "EQ SP ON øD BC" callouts with a pitch-circle centreline, linear arrays get
69
+ pitch dims, every hole gets a centre mark and baseline X/Y location dims from the
70
+ min-X/Y datum corner, and blind/counterbored holes trigger an automatic SECTION
71
+ A–A with ISO 128-44 solid filled arrows and ISO 128-50 45° hatching. Turned
72
+ parts get OD/length dims, centrelines, bore leaders, and — for parts turned
73
+ about a horizontal (X) axis — their external stepped diameters as ø
74
+ leader-callouts.
75
+
76
+ Then verify (Step 3). For most parts you are done here.
77
+
78
+ ---
79
+
80
+ ## Step 2 — Customise with the Drawing builder
81
+
82
+ `build_drawing(...)` returns a live `Drawing`. **Edit it in *domain* vocabulary
83
+ — locate things with `features()`, add dimensions with `place_dim()`, choose a
84
+ side and view.** You give *what* and *where on the part*; the engine decides the
85
+ *offset, stacking, and strip slot* (placement is automatic and constraint-based).
86
+ You still pass page-point endpoints, but you get them from `features()` or
87
+ `dwg.at(...)` — you never compute offsets or pick a strip. Hand-building a raw
88
+ `Leader` at `dwg.at(...)` coordinates is the escape hatch, not the default.
89
+
90
+ ```python
91
+ from draftwright import build_drawing
92
+
93
+ dwg = build_drawing(part, out="drawings/bracket", title="BRACKET",
94
+ number="DWG-042", tolerance="ISO 2768-f", drawn_by="Your Name")
95
+ ```
96
+
97
+ **Inspect what the engine found and placed (read APIs):**
98
+
99
+ ```python
100
+ dwg.features("plan") # detected features in a view → [FeatureInfo(...)]
101
+ # each: .type .diameter .through .depth .count .page_pos
102
+ dwg.annotations() # {name: type} of every named annotation already on the sheet
103
+ dwg.get_annotation(name) # the named annotation object, or None
104
+ dwg.view_bounds("front") # (x_min, y_min, x_max, y_max) page bbox of a view, or None
105
+ dwg.items # the ordered, mutable list of annotation objects
106
+ dwg.views # {"front","plan","side","iso"} → (visible, hidden) compounds
107
+ dwg.draft / dwg.scale / dwg.page_w / dwg.page_h
108
+ ```
109
+
110
+ **Add a linear dimension with `place_dim`** — it allocates the offset and stacks
111
+ clear of existing dims; you give two page-point endpoints and a side/view:
112
+
113
+ ```python
114
+ # side ∈ {"above","below","left","right"}; view ∈ {"front","plan","side"}.
115
+ p1 = dwg.at("front", 0, 0, 0) # world → page point
116
+ p2 = dwg.at("front", 40, 0, 0)
117
+ dwg.place_dim(p1, p2, "above", "front", dwg.draft, name="dim_len")
118
+
119
+ dwg.remove("dim_od") # drop an automatic annotation by name
120
+ ```
121
+
122
+ **Add a diameter callout on a hole the auto-pass missed** — locate it with
123
+ `features()` and attach a `HoleCallout` (this is what the `feature_not_dimensioned`
124
+ lint suggestion hands you verbatim — see the loop below):
125
+
126
+ ```python
127
+ from build123d_drafting import HoleCallout, Leader
128
+
129
+ for f in dwg.features("plan"): # plan→Z holes, front→Y, side→X
130
+ if abs(f.diameter - 4.0) < 0.2:
131
+ callout = HoleCallout(f.diameter, count=f.count, through=f.through,
132
+ depth=f.depth, draft=dwg.draft)
133
+ elbow = (f.page_pos[0] + 15, f.page_pos[1] + 10, 0)
134
+ dwg.add(Leader((*f.page_pos, 0), elbow, "", dwg.draft, callout=callout),
135
+ name="hole_4")
136
+ ```
137
+
138
+ **Escape hatch** — only when no domain verb fits (e.g. a free-form note at an
139
+ exact spot). Prefer the above; this couples you to page mechanics:
140
+
141
+ ```python
142
+ from build123d_drafting import Leader
143
+ dwg.add(Leader(tip=dwg.at("front", 10, 0, 5), elbow=(8, 40, 0),
144
+ label="ø4 BORE", draft=dwg.draft), "ldr_bore")
145
+ ```
146
+
147
+ Then re-lint and export:
148
+
149
+ ```python
150
+ issues = dwg.lint() # list of LintIssue; [] when clean
151
+ svg, dxf = dwg.export("drawings/bracket")
152
+ ```
153
+
154
+ `make_drawing(...)` is exactly `build_drawing(...).export()`.
155
+
156
+ **Add a section or auxiliary view** with `add_view()`:
157
+
158
+ ```python
159
+ look = dwg.look_at
160
+ bottom = (look[0], look[1], look[2] - dwg.dist)
161
+ vc = dwg.add_view("bottom", part, bottom, (0, 1, 0), (260.0, 60.0))
162
+ px, py = vc.pp(world_x, world_y, world_z)
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Step 3 — Verify
168
+
169
+ ```
170
+ mcp__build123d-mcp__render_drawing(svg_path='drawings/part_name.svg', save_to='/tmp/dwg.png')
171
+ mcp__build123d-mcp__save_drawing_annotations(svg_path='drawings/part_name.svg')
172
+ mcp__build123d-mcp__inspect_drawing(svg_path='drawings/part_name.svg')
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Step 4 — Save a standalone regeneration script (default)
178
+
179
+ **A — Drawing from a STEP file**:
180
+
181
+ ```python
182
+ from draftwright import generate_script
183
+
184
+ generate_script(
185
+ "path/to/part.step",
186
+ out="scripts/drawings/bracket",
187
+ title="BRACKET", number="DWG-042",
188
+ tolerance="ISO 2768-f", drawn_by="Your Name",
189
+ )
190
+ ```
191
+
192
+ **B — Drawing an in-session object** (hand-write the script):
193
+
194
+ ```python
195
+ #!/usr/bin/env python3
196
+ """BRACKET — regenerates drawings/bracket.svg + .dxf in one run."""
197
+ from draftwright import make_drawing
198
+ from myproject.bracket import build_bracket
199
+
200
+ part = build_bracket()
201
+ make_drawing(part, out="drawings/bracket", title="BRACKET",
202
+ number="DWG-042", tolerance="ISO 2768-f", drawn_by="Your Name")
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Scale and page control
208
+
209
+ ```python
210
+ from draftwright import choose_scale
211
+
212
+ SCALE, PAGE_W, PAGE_H, TB_W = choose_scale(x_size, y_size, z_size)
213
+ make_drawing(part, out="drawing", scale=2.0, page="A2")
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Lint → critique → fix (the loop to drive as an AI)
219
+
220
+ draftwright is built to be *iterated*: build, read a machine-readable critique,
221
+ apply a domain-level fix, re-build. This is the supported refinement model —
222
+ prefer it over eyeballing the SVG and hand-placing annotations.
223
+
224
+ ```python
225
+ dwg = build_drawing(part)
226
+
227
+ # 1. Critique — the machine channel. JSON-friendly aggregate of lint().
228
+ crit = dwg.lint_summary()
229
+ # {"passed": bool, "score": 0..1, "errors": n, "warnings": n, "infos": n,
230
+ # "by_code": {code: n}, "issues": [{code, severity, message, suggestion?}, ...]}
231
+ # Gate on the severity/code COUNTS, not the scalar score.
232
+
233
+ # 2. Each issue names the problem in DOMAIN terms and (when computable) carries a
234
+ # ready-to-apply suggestion — a domain-API call you paste in, not page maths.
235
+ for i in dwg.lint():
236
+ print(i.severity, i.code, i.message)
237
+ if getattr(i, "suggestion", None):
238
+ print(" fix:", i.suggestion) # e.g. dwg.place_dim(...) / dwg.add_view(...)
239
+
240
+ # 3. Self-repair — auto-applies the mechanically-fixable issues (overlapping
241
+ # labels pushed apart, wrong-side dims flipped). Runs by default inside
242
+ # build_drawing; call again after manual edits. It never makes a sheet worse.
243
+ dwg.repair()
244
+
245
+ # Pin a deliberate placement so repair won't move it (the constraint solver will
246
+ # honour it too as it lands — ADR 0003):
247
+ dwg.pin(name) # name from dwg.annotations(); dwg.unpin(name) to release
248
+ ```
249
+
250
+ Codes are domain-meaningful (`feature_not_dimensioned`, `feature_count_mismatch`,
251
+ `callout_dropped`, `location_ref_dropped`, `step_dim_dropped`, …), so a fix is
252
+ always expressible through the domain API (`place_dim`, `features`, `add_view`),
253
+ never the page-layout internals. Loop until `passed` (or the score plateaus).
254
+
255
+ Coverage-only check, standalone:
256
+
257
+ ```python
258
+ from draftwright import build_drawing, lint_feature_coverage
259
+ issues = build_drawing(part).lint() # geometry lint + feature-coverage check
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Using this skill with build123d-mcp
265
+
266
+ This skill requires `draftwright` to be in the server's import allowlist.
267
+ The build123d-mcp server allows it by default from v0.3.51 onwards. If you see
268
+ `ImportError: draftwright is not in the import allowlist`, pass
269
+ `--allow-imports draftwright` to the server CLI or set
270
+ `BUILD123D_ALLOW_IMPORTS=draftwright` in the environment.
271
+
272
+ For annotation primitives (`Dimension`, `Leader`, `TitleBlock`, `lint_drawing`,
273
+ `ViewCoordinates`, etc.) continue to import from `build123d_drafting`.