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.
- {draftwright-0.2.3 → draftwright-0.2.5}/CHANGELOG.md +40 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/PKG-INFO +63 -49
- {draftwright-0.2.3 → draftwright-0.2.5}/README.md +62 -48
- {draftwright-0.2.3 → draftwright-0.2.5}/pyproject.toml +1 -1
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/_core.py +34 -6
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotate.py +1 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/_common.py +151 -4
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/from_model.py +296 -112
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/holes.py +502 -29
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/orchestrator.py +83 -65
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/sections.py +65 -7
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/builder.py +188 -18
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/cli.py +2 -5
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/drawing.py +592 -15
- draftwright-0.2.5/src/draftwright/intents.py +39 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/model/planner.py +15 -9
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/registry.py +50 -5
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/repair.py +2 -2
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/sheet.py +47 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/box.json +47 -47
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/bracket.json +86 -86
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/drive_screw_x.json +36 -36
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/dshape.json +54 -54
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/flange.json +66 -66
- draftwright-0.2.5/tests/layout_snapshots/holed_slot.json +299 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/plate_holes.json +61 -61
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/side_drilled.json +50 -50
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/slotted.json +72 -72
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/layout_snapshots/turned_shaft.json +79 -79
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_e2e_standards.py +62 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_layout_cleanliness.py +13 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_layout_snapshot.py +18 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_make_drawing.py +1429 -20
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_pmi.py +19 -17
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_registry.py +28 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_render_seam.py +6 -2
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_strip_layout.py +102 -8
- {draftwright-0.2.3 → draftwright-0.2.5}/.gitignore +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/LICENSE +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/skills/SKILL.md +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/analysis.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/export.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/features.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_layout.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_linting.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_part_model.py +0 -0
- {draftwright-0.2.3 → draftwright-0.2.5}/tests/test_recognition.py +0 -0
- {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
|
+
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
|
+

|
|
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 =
|
|
710
|
-
|
|
711
|
-
#
|
|
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
|
-
|
|
759
|
+
For inspection and editing, get a composable `Drawing`:
|
|
715
760
|
|
|
716
|
-
```
|
|
717
|
-
draftwright
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
## Usage
|
|
788
|
+
installed automatically as a dependency.
|
|
751
789
|
|
|
752
|
-
|
|
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
|
+

|
|
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 =
|
|
19
|
-
|
|
20
|
-
#
|
|
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
|
-
|
|
68
|
+
For inspection and editing, get a composable `Drawing`:
|
|
24
69
|
|
|
25
|
-
```
|
|
26
|
-
draftwright
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
## Usage
|
|
97
|
+
installed automatically as a dependency.
|
|
60
98
|
|
|
61
|
-
|
|
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.
|
|
@@ -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 =
|
|
188
|
-
spacing: float =
|
|
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
|
-
|
|
576
|
-
_STRIP_GAP
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
376
|
-
|
|
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)
|