draftwright 0.2.8__tar.gz → 0.2.10__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.
- {draftwright-0.2.8 → draftwright-0.2.10}/CHANGELOG.md +82 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/PKG-INFO +32 -2
- {draftwright-0.2.8 → draftwright-0.2.10}/README.md +31 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/pyproject.toml +1 -2
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/_core.py +31 -3
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/analysis.py +34 -12
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/annotate.py +1 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/annotations/_common.py +103 -13
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/annotations/from_model.py +223 -2
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/annotations/holes.py +59 -20
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/annotations/orchestrator.py +22 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/builder.py +30 -2
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/cli.py +20 -21
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/drawing.py +7 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/layout.py +27 -63
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/linting/__init__.py +2 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/linting/coverage.py +69 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/model/__init__.py +18 -2
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/model/declare.py +218 -27
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/model/detect.py +31 -19
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/model/ir.py +91 -0
- draftwright-0.2.10/src/draftwright/sheet_dsl.py +658 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/sheet_emit.py +74 -8
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_declare.py +81 -0
- draftwright-0.2.10/tests/test_gdt_placement.py +230 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_layout.py +5 -6
- draftwright-0.2.10/tests/test_lint_reconciliation.py +135 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_make_drawing.py +52 -15
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_pmi.py +40 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_sheet_emit.py +222 -12
- draftwright-0.2.10/tests/test_sheet_gdt.py +188 -0
- draftwright-0.2.10/tests/test_sheet_notes.py +131 -0
- draftwright-0.2.10/tests/test_sheet_tables.py +124 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_strip_layout.py +125 -0
- draftwright-0.2.8/src/draftwright/sheet_dsl.py +0 -328
- {draftwright-0.2.8 → draftwright-0.2.10}/.gitignore +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/LICENSE +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/skills/SKILL.md +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/annotations/sections.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/export.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/features.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/fits.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/_kernel.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/bracket.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/dshape.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/side_drilled.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_fits.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_linting.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_object_aspects.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_part_model.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_recognition.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_registry.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_sheet_of.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_tolerances.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.10}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.10 — 2026-07-07
|
|
4
|
+
|
|
5
|
+
**Declarative-surface fidelity and layout-engine unification.** Editing and re-running a
|
|
6
|
+
declarative `Sheet` script now reproduces more of the drawing (title block, layout, PMI on the
|
|
7
|
+
declared path), a new lint catches stale declarations, and an intentional scale is respected.
|
|
8
|
+
Internally the annotation placer moves further onto the single collect-then-solve (ADR 0009),
|
|
9
|
+
and `kiwisolver` is dropped as a dependency.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Declaration-vs-geometry reconciliation lint** — `lint()` now flags a declared cylindrical
|
|
14
|
+
feature (hole/boss/step) with **no matching geometry** in the part (`declared_feature_absent`):
|
|
15
|
+
a callout drawn over empty space because the part was edited but the declaration went stale.
|
|
16
|
+
Closes the last gap in the "did my edit break the drawing?" loop (over-declaration; coverage
|
|
17
|
+
lint already caught under-declaration). Matches on axis + ⌀ + in-plane position + bore/boss
|
|
18
|
+
polarity. Gated on a caller-supplied `model=` (detection can't over-declare). (#487)
|
|
19
|
+
- **Title-block + layout aspects on the `Sheet` DSL** — `Sheet(drawn_by=…, tolerance=…, scale=…,
|
|
20
|
+
page=…)`; the generated `--script` reproduces them, and the CLI forwards
|
|
21
|
+
`--drawn-by`/`--tolerance`/`--scale`/`--page` on the sheet path (no more inert-flag warning). (#474)
|
|
22
|
+
- **PMI reproduced on the declared-model path** — `build_drawing(path, model=…, pmi="annotate")`
|
|
23
|
+
synthesises the STEP AP242 PMI into the declared model, so a declared build draws the same PMI
|
|
24
|
+
dimensions as the detection path. (#472)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **An intentional explicit `scale=` below the legibility floor is honoured with a warning**
|
|
29
|
+
rather than rejected. A part deliberately drawn at 1:1 (or `Sheet(scale="1:10")`) whose smallest
|
|
30
|
+
feature falls under the 10 mm legibility floor now renders (annotations may crowd) instead of
|
|
31
|
+
raising `ValueError`; only a genuinely degenerate scale (< 0.1 mm projected, where OCCT arcs
|
|
32
|
+
collapse) is rejected. The floor now binds only the *automatic* scale. (#489)
|
|
33
|
+
- **`kiwisolver` is no longer a dependency.** The 1-D strip solve delegates to the built-in
|
|
34
|
+
deterministic minimum-total-leader-length PAVA algorithm, which supersedes the Cassowary
|
|
35
|
+
solver — same placement contract, one fewer third-party dependency. (#507)
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **Dimension placement is more robust under contention.** Candidate **priority** is plumbed
|
|
40
|
+
through the shared corridor solve, so an authored GD&T frame is kept over a lower-value
|
|
41
|
+
automatic dimension when a strip is over capacity (#357). Rotational concentric-bore leaders
|
|
42
|
+
are now bounded to the front-view height with ranked drops, and pitch dimensions are placed
|
|
43
|
+
onto the obstacle-aware per-side zone-strip solve — retiring the last fixed-offset,
|
|
44
|
+
count-based stacking placers (the shape behind earlier dense-sheet overruns). (#374)
|
|
45
|
+
|
|
46
|
+
## v0.2.9 — 2026-07-06
|
|
47
|
+
|
|
48
|
+
**Declarative GD&T, datums, and surface finish (ADR 0011 Phase 2b/2c).** The `Sheet` DSL can
|
|
49
|
+
now author the drawing information geometry can't carry — geometric tolerances, datum feature
|
|
50
|
+
symbols, and surface-finish marks — by pointing at a build123d feature or face. They render
|
|
51
|
+
through the auto-layout engine as first-class ADR 0009 corridor candidates, placed and spaced
|
|
52
|
+
crossing-free alongside the automatic dimensions (not a post-hoc overlay).
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
sheet.datum("A", base_face)
|
|
56
|
+
sheet.control(bore).position(0.1, to="A", diameter=True).perpendicularity(0.05, to="A")
|
|
57
|
+
sheet.diameter(journal).finish("0.8")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **Feature control frames (ISO 1101)** — `sheet.control(ref)` returns a chainable builder with
|
|
63
|
+
one method per **all 14** characteristics (`position`, `flatness`, `perpendicularity`,
|
|
64
|
+
`cylindricity`, `circular_runout`, `total_runout`, …); `to=` names the referenced datum(s),
|
|
65
|
+
`diameter=` prefixes the ⌀ tolerance zone, `modifier=` adds a material-condition symbol (Ⓜ/Ⓛ/Ⓟ).
|
|
66
|
+
- **Datum feature symbols (ISO 5459)** — `sheet.datum("A", face_or_feature)`.
|
|
67
|
+
- **Surface finish (ISO 1302, Ra)** — `.finish("1.6")` on a hole / diameter / step handle, or
|
|
68
|
+
`sheet.finish(ra, face)`.
|
|
69
|
+
- The target view + strip side are **derived from the referenced geometry** (a feature's axis →
|
|
70
|
+
face-on view, a planar face's normal → edge-on view); `view=`/`side=` override.
|
|
71
|
+
- Render core: `ControlFrame` / `DatumRef` / `Finish` IR features placed by `render_gdt` as
|
|
72
|
+
first-class corridor candidates, with the real-footprint plumbing needed to space wide frames.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- A control frame referencing an **undeclared datum letter** now warns at build.
|
|
77
|
+
- GD&T placement is **title-block aware** and **falls through to the opposite strip side** before
|
|
78
|
+
dropping a frame, so stacked control frames place robustly rather than vanishing or overlapping
|
|
79
|
+
the title block.
|
|
80
|
+
- An invalid glyph spec / degenerate leader in a caller-supplied model **drops the one item with a
|
|
81
|
+
warning** instead of crashing the whole build (the IR is public input).
|
|
82
|
+
- Turned-shaft rotational furniture now reproduces correctly through the declared-model path
|
|
83
|
+
(`model=` / `Sheet`) (#476).
|
|
84
|
+
|
|
3
85
|
## v0.2.8 — 2026-07-06
|
|
4
86
|
|
|
5
87
|
**`--script` now defaults to the declarative `Sheet` DSL.** The editable script the CLI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
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
|
|
@@ -685,7 +685,6 @@ Requires-Python: <3.15,>=3.10
|
|
|
685
685
|
Requires-Dist: build123d-drafting-helpers>=0.13.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
|
-
Requires-Dist: kiwisolver<2,>=1.4
|
|
689
688
|
Requires-Dist: numpy>=1.24
|
|
690
689
|
Requires-Dist: reportlab>=4.0
|
|
691
690
|
Requires-Dist: svglib>=1.5
|
|
@@ -769,6 +768,37 @@ issues = dwg.lint() # list[LintIssue] — coverage, page b
|
|
|
769
768
|
svg_path, dxf_path = dwg.export("my_part")
|
|
770
769
|
```
|
|
771
770
|
|
|
771
|
+
### Declarative drawings — reference features, declare intent
|
|
772
|
+
|
|
773
|
+
Instead of relying on detection, **reference the build123d objects you built** and declare
|
|
774
|
+
only the drawing intent — geometry supplies the sizes (⌀ read off the object), you supply
|
|
775
|
+
tolerances, fits, datums, GD&T, and surface finish. The fluent `Sheet` skips detection and
|
|
776
|
+
dimensions exactly what you declare (ADR 0011):
|
|
777
|
+
|
|
778
|
+
```python
|
|
779
|
+
from build123d import Box, Cylinder, Pos
|
|
780
|
+
from draftwright import Sheet
|
|
781
|
+
|
|
782
|
+
plate = Box(120, 80, 20)
|
|
783
|
+
bore = Pos(0, 0, 0) * Cylinder(4, 20)
|
|
784
|
+
|
|
785
|
+
sheet = Sheet(plate - bore, title="Plate", number="DWG-002")
|
|
786
|
+
sheet.envelope()
|
|
787
|
+
sheet.datum("A", plate.faces().sort_by()[-1]) # datum A on the top face
|
|
788
|
+
hole = sheet.hole(bore).finish("1.6").note("M3x0.5 TAP") # ⌀8 bore, Ra 1.6, tapped
|
|
789
|
+
sheet.control(hole).position(0.1, to="A", diameter=True) # ⌀0.1 position wrt A
|
|
790
|
+
sheet.export("plate") # writes plate.svg + plate.dxf
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Every aspect the geometry can't carry is a declared verb: `.tolerance(±)` / `.fit("H7")`
|
|
794
|
+
(ISO 286), `.finish("Ra")` (ISO 1302), `sheet.datum(letter, ref)` (ISO 5459), and
|
|
795
|
+
`sheet.control(ref)` with all 14 ISO 1101 characteristics
|
|
796
|
+
(`.position`/`.flatness`/`.perpendicularity`/`.circular_runout`/…), plus `sheet.note(text, ref)`
|
|
797
|
+
/ `.note(text)` for free-text shop callouts (thread specs, `DEBURR`, knurl). A feature verb returns
|
|
798
|
+
a chainable handle (`sheet.hole(bore)`) that the aspect and `control(...)` verbs decorate;
|
|
799
|
+
targets are placed automatically — the view and strip are derived from the referenced
|
|
800
|
+
feature or face, with `view=`/`side=` overrides.
|
|
801
|
+
|
|
772
802
|
## What it produces
|
|
773
803
|
|
|
774
804
|
- **Three orthographic views** (front, plan, side) sized and scaled automatically to the
|
|
@@ -75,6 +75,37 @@ issues = dwg.lint() # list[LintIssue] — coverage, page b
|
|
|
75
75
|
svg_path, dxf_path = dwg.export("my_part")
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### Declarative drawings — reference features, declare intent
|
|
79
|
+
|
|
80
|
+
Instead of relying on detection, **reference the build123d objects you built** and declare
|
|
81
|
+
only the drawing intent — geometry supplies the sizes (⌀ read off the object), you supply
|
|
82
|
+
tolerances, fits, datums, GD&T, and surface finish. The fluent `Sheet` skips detection and
|
|
83
|
+
dimensions exactly what you declare (ADR 0011):
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from build123d import Box, Cylinder, Pos
|
|
87
|
+
from draftwright import Sheet
|
|
88
|
+
|
|
89
|
+
plate = Box(120, 80, 20)
|
|
90
|
+
bore = Pos(0, 0, 0) * Cylinder(4, 20)
|
|
91
|
+
|
|
92
|
+
sheet = Sheet(plate - bore, title="Plate", number="DWG-002")
|
|
93
|
+
sheet.envelope()
|
|
94
|
+
sheet.datum("A", plate.faces().sort_by()[-1]) # datum A on the top face
|
|
95
|
+
hole = sheet.hole(bore).finish("1.6").note("M3x0.5 TAP") # ⌀8 bore, Ra 1.6, tapped
|
|
96
|
+
sheet.control(hole).position(0.1, to="A", diameter=True) # ⌀0.1 position wrt A
|
|
97
|
+
sheet.export("plate") # writes plate.svg + plate.dxf
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Every aspect the geometry can't carry is a declared verb: `.tolerance(±)` / `.fit("H7")`
|
|
101
|
+
(ISO 286), `.finish("Ra")` (ISO 1302), `sheet.datum(letter, ref)` (ISO 5459), and
|
|
102
|
+
`sheet.control(ref)` with all 14 ISO 1101 characteristics
|
|
103
|
+
(`.position`/`.flatness`/`.perpendicularity`/`.circular_runout`/…), plus `sheet.note(text, ref)`
|
|
104
|
+
/ `.note(text)` for free-text shop callouts (thread specs, `DEBURR`, knurl). A feature verb returns
|
|
105
|
+
a chainable handle (`sheet.hole(bore)`) that the aspect and `control(...)` verbs decorate;
|
|
106
|
+
targets are placed automatically — the view and strip are derived from the referenced
|
|
107
|
+
feature or face, with `view=`/`side=` overrides.
|
|
108
|
+
|
|
78
109
|
## What it produces
|
|
79
110
|
|
|
80
111
|
- **Three orthographic views** (front, plan, side) sized and scaled automatically to the
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "draftwright"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.10"
|
|
8
8
|
description = "Automated technical-drawing generation for build123d"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -18,7 +18,6 @@ dependencies = [
|
|
|
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
20
|
"build123d-drafting-helpers>=0.13.0",
|
|
21
|
-
"kiwisolver>=1.4,<2",
|
|
22
21
|
"numpy>=1.24",
|
|
23
22
|
"reportlab>=4.0",
|
|
24
23
|
"svglib>=1.5",
|
|
@@ -255,10 +255,21 @@ _SLOT_DIM_HEIGHT = 2 * _FONT_SIZE + 2 * _PAD # fv_zones.right: overall height d
|
|
|
255
255
|
|
|
256
256
|
|
|
257
257
|
_MIN_VIEW_MM = (
|
|
258
|
-
10.0 #
|
|
258
|
+
10.0 # legibility floor: the projected size below which an *explicit* scale earns a warning.
|
|
259
|
+
# It is NOT a bound on the auto scale (choose_scale is a pure geometric page fit) and does NOT
|
|
260
|
+
# gate which annotations exist (step/location legibility use _MIN_STEP_*/_MIN_LOC_SEP_MM). Its
|
|
261
|
+
# only use is the explicit-scale advisory in analysis.py: below it a user scale is honoured
|
|
262
|
+
# with a warning, not rejected (#489).
|
|
259
263
|
)
|
|
260
264
|
|
|
261
265
|
|
|
266
|
+
# Hard geometry floor: below this projected size OCCT's annotation arcs collapse
|
|
267
|
+
# (Geom_TrimmedCurve U1==U2), which happens near 1e-4 mm empirically — 0.1 mm is a conservative
|
|
268
|
+
# floor far above that and far below any real drawing. An explicit scale under it is rejected with
|
|
269
|
+
# a clean message rather than a cryptic OCP error (#489).
|
|
270
|
+
_MIN_RENDER_MM = 0.1
|
|
271
|
+
|
|
272
|
+
|
|
262
273
|
_SLOT_DIM_STEP = 4 * _FONT_SIZE + _PAD # fv_zones.right: step-height dimension
|
|
263
274
|
|
|
264
275
|
|
|
@@ -557,8 +568,11 @@ def _attribution_author(drawn_by: str | None) -> str:
|
|
|
557
568
|
return f"{author} / draftwright" if author else "draftwright"
|
|
558
569
|
|
|
559
570
|
|
|
560
|
-
def
|
|
561
|
-
"""
|
|
571
|
+
def _make_title_block(dwg, a: Analysis):
|
|
572
|
+
"""Construct + page-locate the title block, returning ``(tb, cell)`` where *cell* is its
|
|
573
|
+
drawn-by cell bbox (for the hyperlink rect). Shared by :func:`_add_title_block` (which adds
|
|
574
|
+
it, last) and :func:`_title_block_box` (which measures its footprint for GD&T avoidance, #481)
|
|
575
|
+
so the two never drift."""
|
|
562
576
|
tb = TitleBlock(
|
|
563
577
|
a.title,
|
|
564
578
|
a.number,
|
|
@@ -582,6 +596,20 @@ def _add_title_block(dwg, a: Analysis):
|
|
|
582
596
|
# TitleBlock layout change. Build-frame bbox; translated to page space below.
|
|
583
597
|
cell = tb.drawn_by_cell_bbox()
|
|
584
598
|
tb = tb.locate(Location((a.PAGE_W - a.TB_W - _TB_CLEAR, _TB_CLEAR, 0)))
|
|
599
|
+
return tb, cell
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _title_block_box(dwg, a: Analysis):
|
|
603
|
+
"""The title block's real page-space bbox ``(x0, y0, x1, y1)``. GD&T placement avoids it
|
|
604
|
+
(#481): the block is added last, so strip placement can't see it, but it's deterministic."""
|
|
605
|
+
tb, _ = _make_title_block(dwg, a)
|
|
606
|
+
b = tb.bounding_box()
|
|
607
|
+
return (b.min.X, b.min.Y, b.max.X, b.max.Y)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _add_title_block(dwg, a: Analysis):
|
|
611
|
+
"""Add the title block annotation."""
|
|
612
|
+
tb, cell = _make_title_block(dwg, a)
|
|
585
613
|
dwg.add(tb, "title_block")
|
|
586
614
|
|
|
587
615
|
# Record that cell's page-space rectangle so export() can place a clickable
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
14
|
import math
|
|
15
|
+
import warnings
|
|
15
16
|
|
|
16
17
|
from build123d import Compound, Shape
|
|
17
18
|
from build123d_drafting.helpers import draft_preset
|
|
@@ -22,6 +23,7 @@ from draftwright._core import (
|
|
|
22
23
|
_DIM_PAD,
|
|
23
24
|
_FONT_SIZE,
|
|
24
25
|
_MARGIN,
|
|
26
|
+
_MIN_RENDER_MM,
|
|
25
27
|
_MIN_VIEW_MM,
|
|
26
28
|
Analysis,
|
|
27
29
|
_legible_steps,
|
|
@@ -341,21 +343,41 @@ def _analyse(
|
|
|
341
343
|
break
|
|
342
344
|
n_for_sizing = n_next
|
|
343
345
|
if scale is not None:
|
|
346
|
+
# An explicit scale is the user's call — honour it (#489). Two floors apply:
|
|
347
|
+
# - _MIN_RENDER_MM: a hard geometry limit; below it OCCT's annotation arcs degenerate
|
|
348
|
+
# (Geom_TrimmedCurve U1==U2, ~1e-4 mm empirically — 0.1 mm is a conservative floor).
|
|
349
|
+
# Reject with a clean message; there is no meaningful drawing this small anyway.
|
|
350
|
+
# - _MIN_VIEW_MM: a legibility floor; below it annotations crowd but the drawing is
|
|
351
|
+
# valid, so honour the scale with a warning. This floor does NOT bound the auto scale
|
|
352
|
+
# (choose_scale is a pure geometric page fit), so a warning is only useful when a
|
|
353
|
+
# legible page-fitting scale actually exists — i.e. the auto scale is itself legible.
|
|
354
|
+
min_dim = min(x_size, y_size, z_size)
|
|
355
|
+
min_view = min_dim * SCALE
|
|
356
|
+
if min_view < _MIN_RENDER_MM:
|
|
357
|
+
safe = _MIN_RENDER_MM / min_dim
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"scale {SCALE!r} projects the smallest part dimension "
|
|
360
|
+
f"({min_dim:.0f} mm) to {min_view:.3g} mm — the drawing geometry degenerates "
|
|
361
|
+
f"below {_MIN_RENDER_MM:g} mm (OCCT arc construction fails). "
|
|
362
|
+
f"Use scale ≥ {safe:.3g} or omit the scale for automatic selection."
|
|
363
|
+
)
|
|
344
364
|
auto_scale, _, _, _ = choose_scale(
|
|
345
365
|
x_size, y_size, z_size, n_steps=n_for_sizing, scale=None, page=page, strips=strips_i
|
|
346
366
|
)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
367
|
+
# Warn only when omitting the scale would truly give a legible fit (auto scale itself is
|
|
368
|
+
# legible) but the requested scale is below the floor. A part illegible at every
|
|
369
|
+
# page-fitting scale can't be helped by a bigger one, so nagging there would be false.
|
|
370
|
+
if min_view < _MIN_VIEW_MM <= auto_scale * min_dim:
|
|
371
|
+
safe = _MIN_VIEW_MM / min_dim
|
|
372
|
+
# No stacklevel that reaches user code: this fires deep in _analyse, and the public
|
|
373
|
+
# entry points (make_drawing, Sheet.export, build_drawing) sit at different depths.
|
|
374
|
+
# The message is self-contained (names the scale, the projection, and the fix).
|
|
375
|
+
warnings.warn(
|
|
376
|
+
f"scale {SCALE!r} projects the smallest part dimension ({min_dim:.0f} mm) to "
|
|
377
|
+
f"{min_view:.1f} mm, below the {_MIN_VIEW_MM:.0f} mm legibility floor — "
|
|
378
|
+
f"annotations may crowd or overlap. Honouring the requested scale; use "
|
|
379
|
+
f"scale ≥ {safe:.3g} or omit the scale for an automatic legible fit."
|
|
380
|
+
)
|
|
359
381
|
DIM_PAD = _DIM_PAD
|
|
360
382
|
margin = _MARGIN
|
|
361
383
|
# Refine: apply the same legibility gate _auto_annotate uses for dim_step.
|
|
@@ -288,6 +288,11 @@ class CorridorCandidate:
|
|
|
288
288
|
the same physical dimension; the higher-``precedence`` one survives (#345).
|
|
289
289
|
precedence: dedup survivor rank — a hole *location* dim (feeds coverage/table
|
|
290
290
|
escalation) outranks a coincident slot *position* line.
|
|
291
|
+
priority: over-capacity survival rank (#357). When a strip cannot hold every
|
|
292
|
+
candidate, :func:`plan_strip` drops the lowest ``(priority, key)`` — so a higher
|
|
293
|
+
``priority`` is kept. An authored GD&T frame sets this above the auto dims so it
|
|
294
|
+
is not dropped in favour of a lower-value auto dim purely by stacking-key order.
|
|
295
|
+
Default 0 (every auto dim) → key order, unchanged.
|
|
291
296
|
on_place/on_drop: the pass's own post-placement bookkeeping — coverage
|
|
292
297
|
registration / drop lint + `Escalation`, or a slot's below-side fallthrough.
|
|
293
298
|
force: policy-B force-keep after the corridor-respecting pass (locations have
|
|
@@ -301,10 +306,21 @@ class CorridorCandidate:
|
|
|
301
306
|
on_drop: object
|
|
302
307
|
dedup: tuple | None = None
|
|
303
308
|
precedence: int = 0
|
|
309
|
+
priority: float = 0
|
|
304
310
|
force: bool = False
|
|
305
311
|
# The source IR feature this dim was rendered for — recorded as provenance when the
|
|
306
312
|
# dim is placed at drain (ADR 0010). ``None`` leaves the annotation feature-less.
|
|
307
313
|
feature: object | None = None
|
|
314
|
+
# Real stacking-axis + perpendicular footprint ``(w, h)`` in page-mm, or ``None`` to
|
|
315
|
+
# use the dimension default ``(tier, tier)``. Wide/tall occupants (a GD&T feature
|
|
316
|
+
# control frame is ~24×6 mm) set this so the strip solve reserves their true extent
|
|
317
|
+
# instead of one label-height (ADR 0009 real-footprint plumbing, #61). A dim leaves
|
|
318
|
+
# it ``None`` — byte-identical to the pre-plumbing placement.
|
|
319
|
+
size: tuple | None = None
|
|
320
|
+
# An ``(x0, y0, x1, y1)`` page-box this candidate must NOT overlap even when force-kept —
|
|
321
|
+
# the title block, which is placed after the corridor drain so the strip carve can't see
|
|
322
|
+
# it (#481). ``None`` (every dim) skips the check → byte-identical.
|
|
323
|
+
forbid: object | None = None
|
|
308
324
|
|
|
309
325
|
|
|
310
326
|
def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
@@ -357,15 +373,40 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
357
373
|
return
|
|
358
374
|
pairs = [(c.name, c.build) for c in kept]
|
|
359
375
|
feats = {c.name: c.feature for c in kept if c.feature is not None} # provenance (ADR 0010)
|
|
376
|
+
sizes = {c.name: c.size for c in kept if c.size is not None} # real footprint (#61)
|
|
377
|
+
forbid = {c.name: c.forbid for c in kept if c.forbid is not None} # title-block box (#481)
|
|
378
|
+
prio = {c.name: c.priority for c in kept if c.priority} # over-capacity survival rank (#357)
|
|
360
379
|
left = {
|
|
361
|
-
n
|
|
380
|
+
n
|
|
381
|
+
for n, _ in place_strip_candidates(
|
|
382
|
+
dwg,
|
|
383
|
+
strip,
|
|
384
|
+
view,
|
|
385
|
+
axis,
|
|
386
|
+
pairs,
|
|
387
|
+
tier,
|
|
388
|
+
features=feats,
|
|
389
|
+
sizes=sizes,
|
|
390
|
+
forbid=forbid,
|
|
391
|
+
priorities=prio,
|
|
392
|
+
)
|
|
362
393
|
}
|
|
363
394
|
force_pairs = [(c.name, c.build) for c in kept if c.name in left and c.force]
|
|
364
395
|
still = (
|
|
365
396
|
{
|
|
366
397
|
n
|
|
367
398
|
for n, _ in place_strip_candidates(
|
|
368
|
-
dwg,
|
|
399
|
+
dwg,
|
|
400
|
+
strip,
|
|
401
|
+
view,
|
|
402
|
+
axis,
|
|
403
|
+
force_pairs,
|
|
404
|
+
tier,
|
|
405
|
+
force=True,
|
|
406
|
+
features=feats,
|
|
407
|
+
sizes=sizes,
|
|
408
|
+
forbid=forbid,
|
|
409
|
+
priorities=prio,
|
|
369
410
|
)
|
|
370
411
|
}
|
|
371
412
|
if force_pairs
|
|
@@ -399,7 +440,20 @@ def drain_corridors(dwg):
|
|
|
399
440
|
dwg._corridor_batch = {}
|
|
400
441
|
|
|
401
442
|
|
|
402
|
-
def place_strip_candidates(
|
|
443
|
+
def place_strip_candidates(
|
|
444
|
+
dwg,
|
|
445
|
+
strip,
|
|
446
|
+
view,
|
|
447
|
+
axis,
|
|
448
|
+
cands,
|
|
449
|
+
tier,
|
|
450
|
+
*,
|
|
451
|
+
force=False,
|
|
452
|
+
features=None,
|
|
453
|
+
sizes=None,
|
|
454
|
+
forbid=None,
|
|
455
|
+
priorities=None,
|
|
456
|
+
):
|
|
403
457
|
"""Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
|
|
404
458
|
The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
|
|
405
459
|
P3): each candidate in *cands* — an ``(name, build(pos)->dim)`` pair — is spaced by
|
|
@@ -418,6 +472,17 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False,
|
|
|
418
472
|
1-D strip carve cannot represent: a leader in that corridor is crossed no matter how
|
|
419
473
|
far out the dim line lands. By default such a placement is rejected so the caller can
|
|
420
474
|
route the dim to the other view (its disjoint block cannot cross this leader).
|
|
475
|
+
|
|
476
|
+
*sizes* maps a candidate's name to its real page-mm footprint ``(w, h)``; absent
|
|
477
|
+
names use the dimension default ``(tier, tier)``. A wide/tall occupant (a GD&T
|
|
478
|
+
frame, #61) sets it so :func:`plan_strip` enforces its true stacking gap — over
|
|
479
|
+
capacity it is relocated to the next segment or dropped, never overlapped.
|
|
480
|
+
|
|
481
|
+
*priorities* maps a candidate's name to its over-capacity survival rank (#357);
|
|
482
|
+
absent names default to 0. When a segment is over capacity :func:`plan_strip` drops
|
|
483
|
+
the lowest ``(priority, key)``, so a higher priority is kept — an authored GD&T frame
|
|
484
|
+
is not dropped for a lower-value auto dim purely by stacking-key order.
|
|
485
|
+
|
|
421
486
|
``force=True`` skips that corridor check — the caller's last resort when no view took
|
|
422
487
|
the dim cleanly: keep it on its natural view and accept the (same-feature) leader
|
|
423
488
|
crossing rather than drop a real dimension (policy B). Candidates that find no strip
|
|
@@ -426,17 +491,31 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False,
|
|
|
426
491
|
if strip is None or not cands:
|
|
427
492
|
return list(cands)
|
|
428
493
|
lo, hi, inner = strip_free_span(strip)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
# the
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
#
|
|
494
|
+
idx = 1 if axis == "y" else 0
|
|
495
|
+
|
|
496
|
+
# Reserve the outermost label's OUTWARD extent at the strip boundary. plan_strip bounds
|
|
497
|
+
# the dim-LINE position, but the label extends outward from it — so without this the last
|
|
498
|
+
# tier's label overshoots outer_limit (into the iso view / page margin), unlike the old
|
|
499
|
+
# Strip.allocate which checked `start + tier <= outer_limit` (#338 review). A plain dim's
|
|
500
|
+
# label extends one `tier` outward (one-sided). A GD&T glyph (#61) hangs off a Leader that
|
|
501
|
+
# CENTRES it on the elbow for an above/below strip (real outward extent = height/2) but
|
|
502
|
+
# places it one-sided for a left/right strip (extent = full width). Reserve the MAX real
|
|
503
|
+
# outward extent among these candidates — else a glyph wider than `tier` renders off the
|
|
504
|
+
# sheet (annotation_out_of_bounds) instead of dropping when the strip is too narrow (ADR
|
|
505
|
+
# 0009 Amdt 7 fixed inter-candidate gaps but not this edge). With no `sizes` (every dim)
|
|
506
|
+
# this is `tier`, byte-identical. The strip edge is not an obstacle (obstacles carry their
|
|
507
|
+
# own footprint + pad), so only the boundary needs it.
|
|
508
|
+
def _outward(name):
|
|
509
|
+
sz = (sizes or {}).get(name)
|
|
510
|
+
if sz is None:
|
|
511
|
+
return tier # a dim: one-sided tier reservation (unchanged)
|
|
512
|
+
return sz[idx] if axis == "x" else sz[idx] / 2 # GD&T: one-sided (L/R) vs centred (A/B)
|
|
513
|
+
|
|
514
|
+
reserve = max([tier, *(_outward(n) for n, _ in cands)])
|
|
435
515
|
if inner == lo:
|
|
436
|
-
hi -=
|
|
516
|
+
hi -= reserve
|
|
437
517
|
else:
|
|
438
|
-
lo +=
|
|
439
|
-
idx = 1 if axis == "y" else 0
|
|
518
|
+
lo += reserve
|
|
440
519
|
perp = 0 if axis == "y" else 1 # the axis the dims do NOT stack along
|
|
441
520
|
pad = tier + strip.spacing # min separation between stacked dim lines
|
|
442
521
|
# Perpendicular band of these candidates. The 1-D carve projects obstacles onto the
|
|
@@ -471,7 +550,10 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False,
|
|
|
471
550
|
triples = [
|
|
472
551
|
(
|
|
473
552
|
StripCandidate(
|
|
474
|
-
f"{(k if inner == lo else len(take) - 1 - k):04d}",
|
|
553
|
+
f"{(k if inner == lo else len(take) - 1 - k):04d}",
|
|
554
|
+
anch,
|
|
555
|
+
(sizes or {}).get(nb[0], (tier, tier)),
|
|
556
|
+
priority=(priorities or {}).get(nb[0], 0.0),
|
|
475
557
|
),
|
|
476
558
|
nb,
|
|
477
559
|
)
|
|
@@ -487,6 +569,14 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False,
|
|
|
487
569
|
if not force and _box_hits(_geom_box(dim), blockers): # corridor crosses a leader
|
|
488
570
|
todo.append((name, build))
|
|
489
571
|
continue
|
|
572
|
+
# A forbidden box (the title block, #481) is rejected even under force — it is
|
|
573
|
+
# placed after the drain, so the strip carve can't see it; a force-kept GD&T frame
|
|
574
|
+
# must still not stack onto it. `forbid` maps names to their box (only GD&T sets it,
|
|
575
|
+
# so dims are byte-identical). Returned unplaced → the caller's on_drop fallthrough.
|
|
576
|
+
fb = (forbid or {}).get(name)
|
|
577
|
+
if fb is not None and _box_hits(_geom_box(dim), (fb,)):
|
|
578
|
+
todo.append((name, build))
|
|
579
|
+
continue
|
|
490
580
|
# Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
|
|
491
581
|
# dims — `features` maps this batch's names to their source IR feature.
|
|
492
582
|
dwg.add(dim, name, view=view, feature=(features or {}).get(name))
|