draftwright 0.2.9__tar.gz → 0.2.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.
- {draftwright-0.2.9 → draftwright-0.2.11}/CHANGELOG.md +71 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/PKG-INFO +4 -4
- {draftwright-0.2.9 → draftwright-0.2.11}/README.md +3 -2
- {draftwright-0.2.9 → draftwright-0.2.11}/pyproject.toml +1 -2
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/_core.py +23 -6
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/analysis.py +193 -26
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/annotations/_common.py +84 -10
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/annotations/from_model.py +313 -170
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/annotations/holes.py +162 -51
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/annotations/orchestrator.py +17 -19
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/annotations/sections.py +11 -13
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/builder.py +51 -19
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/cli.py +20 -21
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/drawing.py +158 -57
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/layout.py +27 -63
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/linting/__init__.py +2 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/linting/coverage.py +69 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/model/__init__.py +4 -1
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/model/declare.py +88 -27
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/model/detect.py +31 -19
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/model/ir.py +21 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/repair.py +8 -25
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/sheet.py +239 -82
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/sheet_dsl.py +136 -9
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/sheet_emit.py +74 -8
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/bracket.json +12 -12
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_declare.py +81 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_layout.py +5 -6
- draftwright-0.2.11/tests/test_lint_reconciliation.py +135 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_make_drawing.py +400 -62
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_pmi.py +43 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_sheet_emit.py +151 -11
- draftwright-0.2.11/tests/test_sheet_notes.py +131 -0
- draftwright-0.2.11/tests/test_sheet_tables.py +124 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_strip_layout.py +178 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/.gitignore +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/LICENSE +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/skills/SKILL.md +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/export.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/features.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/fits.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/_kernel.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/dshape.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/side_drilled.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_fits.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_gdt_placement.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_linting.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_object_aspects.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_part_model.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_recognition.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_registry.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_sheet_of.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_tolerances.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.11}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.11 — 2026-07-08
|
|
4
|
+
|
|
5
|
+
**Automatic layout now has one authority.** Page/scale selection, section placement,
|
|
6
|
+
furniture footprints, and table/balloon escalation now negotiate through the same layout
|
|
7
|
+
fitness model instead of using separate fixed offsets or first-fit policies after the main
|
|
8
|
+
solve.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Solver placement paths are more consistent.** STEP PMI dimensions now queue into
|
|
13
|
+
the shared ADR 0009 corridor solve instead of carving after the drain; front-view
|
|
14
|
+
hole callouts use the strip solver instead of fixed row stepping; pitch-dimension
|
|
15
|
+
fallback searches bounded, obstacle-aware positions; repair no longer hides
|
|
16
|
+
`annotation_overlap` with a fixed-step nudge; and step-count sizing now handles
|
|
17
|
+
non-convergence conservatively. (#524)
|
|
18
|
+
- **Page/scale fitness is shared across initial selection and repack.** Later layout
|
|
19
|
+
passes now compare candidates with the same model as the page chooser, preventing a
|
|
20
|
+
nominally "better" repack from invalidating the original layout decision. (#519)
|
|
21
|
+
- **Section A-A participates in layout selection.** Section placement is measured as
|
|
22
|
+
part of page/scale fitness, so a section view is no longer a fixed-offset afterthought
|
|
23
|
+
that can disappear on dense sheets. (#515)
|
|
24
|
+
- **Furniture placement reserves full rendered footprints.** Section arrows, detail
|
|
25
|
+
views, balloons, hole/data tables, and other furniture now reserve/check their true
|
|
26
|
+
rendered footprint instead of just their labels. (#518)
|
|
27
|
+
- **Hole/data tables and balloon rings escalate through the layout model.** Dense table
|
|
28
|
+
and balloon outputs can move or negotiate for available room rather than dropping from
|
|
29
|
+
greedy first-fit placement when a small adjustment would fit. (#516, #517)
|
|
30
|
+
|
|
31
|
+
## v0.2.10 — 2026-07-07
|
|
32
|
+
|
|
33
|
+
**Declarative-surface fidelity and layout-engine unification.** Editing and re-running a
|
|
34
|
+
declarative `Sheet` script now reproduces more of the drawing (title block, layout, PMI on the
|
|
35
|
+
declared path), a new lint catches stale declarations, and an intentional scale is respected.
|
|
36
|
+
Internally the annotation placer moves further onto the single collect-then-solve (ADR 0009),
|
|
37
|
+
and `kiwisolver` is dropped as a dependency.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **Declaration-vs-geometry reconciliation lint** — `lint()` now flags a declared cylindrical
|
|
42
|
+
feature (hole/boss/step) with **no matching geometry** in the part (`declared_feature_absent`):
|
|
43
|
+
a callout drawn over empty space because the part was edited but the declaration went stale.
|
|
44
|
+
Closes the last gap in the "did my edit break the drawing?" loop (over-declaration; coverage
|
|
45
|
+
lint already caught under-declaration). Matches on axis + ⌀ + in-plane position + bore/boss
|
|
46
|
+
polarity. Gated on a caller-supplied `model=` (detection can't over-declare). (#487)
|
|
47
|
+
- **Title-block + layout aspects on the `Sheet` DSL** — `Sheet(drawn_by=…, tolerance=…, scale=…,
|
|
48
|
+
page=…)`; the generated `--script` reproduces them, and the CLI forwards
|
|
49
|
+
`--drawn-by`/`--tolerance`/`--scale`/`--page` on the sheet path (no more inert-flag warning). (#474)
|
|
50
|
+
- **PMI reproduced on the declared-model path** — `build_drawing(path, model=…, pmi="annotate")`
|
|
51
|
+
synthesises the STEP AP242 PMI into the declared model, so a declared build draws the same PMI
|
|
52
|
+
dimensions as the detection path. (#472)
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **An intentional explicit `scale=` below the legibility floor is honoured with a warning**
|
|
57
|
+
rather than rejected. A part deliberately drawn at 1:1 (or `Sheet(scale="1:10")`) whose smallest
|
|
58
|
+
feature falls under the 10 mm legibility floor now renders (annotations may crowd) instead of
|
|
59
|
+
raising `ValueError`; only a genuinely degenerate scale (< 0.1 mm projected, where OCCT arcs
|
|
60
|
+
collapse) is rejected. The floor now binds only the *automatic* scale. (#489)
|
|
61
|
+
- **`kiwisolver` is no longer a dependency.** The 1-D strip solve delegates to the built-in
|
|
62
|
+
deterministic minimum-total-leader-length PAVA algorithm, which supersedes the Cassowary
|
|
63
|
+
solver — same placement contract, one fewer third-party dependency. (#507)
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- **Dimension placement is more robust under contention.** Candidate **priority** is plumbed
|
|
68
|
+
through the shared corridor solve, so an authored GD&T frame is kept over a lower-value
|
|
69
|
+
automatic dimension when a strip is over capacity (#357). Rotational concentric-bore leaders
|
|
70
|
+
are now bounded to the front-view height with ranked drops, and pitch dimensions are placed
|
|
71
|
+
onto the obstacle-aware per-side zone-strip solve — retiring the last fixed-offset,
|
|
72
|
+
count-based stacking placers (the shape behind earlier dense-sheet overruns). (#374)
|
|
73
|
+
|
|
3
74
|
## v0.2.9 — 2026-07-06
|
|
4
75
|
|
|
5
76
|
**Declarative GD&T, datums, and surface finish (ADR 0011 Phase 2b/2c).** The `Sheet` DSL can
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -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
|
|
@@ -786,7 +785,7 @@ bore = Pos(0, 0, 0) * Cylinder(4, 20)
|
|
|
786
785
|
sheet = Sheet(plate - bore, title="Plate", number="DWG-002")
|
|
787
786
|
sheet.envelope()
|
|
788
787
|
sheet.datum("A", plate.faces().sort_by()[-1]) # datum A on the top face
|
|
789
|
-
hole = sheet.hole(bore).finish("1.6")
|
|
788
|
+
hole = sheet.hole(bore).finish("1.6").note("M3x0.5 TAP") # ⌀8 bore, Ra 1.6, tapped
|
|
790
789
|
sheet.control(hole).position(0.1, to="A", diameter=True) # ⌀0.1 position wrt A
|
|
791
790
|
sheet.export("plate") # writes plate.svg + plate.dxf
|
|
792
791
|
```
|
|
@@ -794,7 +793,8 @@ sheet.export("plate") # writes plate.svg + p
|
|
|
794
793
|
Every aspect the geometry can't carry is a declared verb: `.tolerance(±)` / `.fit("H7")`
|
|
795
794
|
(ISO 286), `.finish("Ra")` (ISO 1302), `sheet.datum(letter, ref)` (ISO 5459), and
|
|
796
795
|
`sheet.control(ref)` with all 14 ISO 1101 characteristics
|
|
797
|
-
(`.position`/`.flatness`/`.perpendicularity`/`.circular_runout`/…)
|
|
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
798
|
a chainable handle (`sheet.hole(bore)`) that the aspect and `control(...)` verbs decorate;
|
|
799
799
|
targets are placed automatically — the view and strip are derived from the referenced
|
|
800
800
|
feature or face, with `view=`/`side=` overrides.
|
|
@@ -92,7 +92,7 @@ bore = Pos(0, 0, 0) * Cylinder(4, 20)
|
|
|
92
92
|
sheet = Sheet(plate - bore, title="Plate", number="DWG-002")
|
|
93
93
|
sheet.envelope()
|
|
94
94
|
sheet.datum("A", plate.faces().sort_by()[-1]) # datum A on the top face
|
|
95
|
-
hole = sheet.hole(bore).finish("1.6")
|
|
95
|
+
hole = sheet.hole(bore).finish("1.6").note("M3x0.5 TAP") # ⌀8 bore, Ra 1.6, tapped
|
|
96
96
|
sheet.control(hole).position(0.1, to="A", diameter=True) # ⌀0.1 position wrt A
|
|
97
97
|
sheet.export("plate") # writes plate.svg + plate.dxf
|
|
98
98
|
```
|
|
@@ -100,7 +100,8 @@ sheet.export("plate") # writes plate.svg + p
|
|
|
100
100
|
Every aspect the geometry can't carry is a declared verb: `.tolerance(±)` / `.fit("H7")`
|
|
101
101
|
(ISO 286), `.finish("Ra")` (ISO 1302), `sheet.datum(letter, ref)` (ISO 5459), and
|
|
102
102
|
`sheet.control(ref)` with all 14 ISO 1101 characteristics
|
|
103
|
-
(`.position`/`.flatness`/`.perpendicularity`/`.circular_runout`/…)
|
|
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
|
|
104
105
|
a chainable handle (`sheet.hole(bore)`) that the aspect and `control(...)` verbs decorate;
|
|
105
106
|
targets are placed automatically — the view and strip are derived from the referenced
|
|
106
107
|
feature or face, with `view=`/`side=` overrides.
|
|
@@ -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.11"
|
|
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",
|
|
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING
|
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from draftwright.recognition import TurnedProfile
|
|
24
|
+
from draftwright.sheet import StripDepths
|
|
24
25
|
|
|
25
26
|
from build123d import Align, BoundBox, Location, Mode, Shape, Text
|
|
26
27
|
from build123d_drafting.helpers import (
|
|
@@ -255,10 +256,21 @@ _SLOT_DIM_HEIGHT = 2 * _FONT_SIZE + 2 * _PAD # fv_zones.right: overall height d
|
|
|
255
256
|
|
|
256
257
|
|
|
257
258
|
_MIN_VIEW_MM = (
|
|
258
|
-
10.0 #
|
|
259
|
+
10.0 # legibility floor: the projected size below which an *explicit* scale earns a warning.
|
|
260
|
+
# It is NOT a bound on the auto scale (choose_scale is a pure geometric page fit) and does NOT
|
|
261
|
+
# gate which annotations exist (step/location legibility use _MIN_STEP_*/_MIN_LOC_SEP_MM). Its
|
|
262
|
+
# only use is the explicit-scale advisory in analysis.py: below it a user scale is honoured
|
|
263
|
+
# with a warning, not rejected (#489).
|
|
259
264
|
)
|
|
260
265
|
|
|
261
266
|
|
|
267
|
+
# Hard geometry floor: below this projected size OCCT's annotation arcs collapse
|
|
268
|
+
# (Geom_TrimmedCurve U1==U2), which happens near 1e-4 mm empirically — 0.1 mm is a conservative
|
|
269
|
+
# floor far above that and far below any real drawing. An explicit scale under it is rejected with
|
|
270
|
+
# a clean message rather than a cryptic OCP error (#489).
|
|
271
|
+
_MIN_RENDER_MM = 0.1
|
|
272
|
+
|
|
273
|
+
|
|
262
274
|
_SLOT_DIM_STEP = 4 * _FONT_SIZE + _PAD # fv_zones.right: step-height dimension
|
|
263
275
|
|
|
264
276
|
|
|
@@ -327,7 +339,7 @@ def _legible_locations(positions, scale):
|
|
|
327
339
|
return kept, n_too_close
|
|
328
340
|
|
|
329
341
|
|
|
330
|
-
def _largest_empty_rect(drawable, obstacles):
|
|
342
|
+
def _largest_empty_rect(drawable, obstacles, *, warn: bool = True):
|
|
331
343
|
"""Largest axis-aligned empty rectangle in *drawable* avoiding *obstacles*.
|
|
332
344
|
|
|
333
345
|
*drawable* and each obstacle are ``(x0, y0, x1, y1)`` page-mm boxes. Returns
|
|
@@ -365,10 +377,11 @@ def _largest_empty_rect(drawable, obstacles):
|
|
|
365
377
|
# is unreachable in practice — choose_scale always leaves a gap — but
|
|
366
378
|
# if it ever happens the iso would render over the other views, so flag
|
|
367
379
|
# it rather than fail silently.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
380
|
+
if warn:
|
|
381
|
+
_log.warning(
|
|
382
|
+
"No empty rectangle found for the iso view; obstacles fill the "
|
|
383
|
+
"drawable area — iso may overlap other views"
|
|
384
|
+
)
|
|
372
385
|
return drawable
|
|
373
386
|
return best
|
|
374
387
|
|
|
@@ -503,6 +516,10 @@ class Analysis:
|
|
|
503
516
|
is_rotational: bool
|
|
504
517
|
od_axis: str # rotation/turning axis of a rotational part ("z" default; "x"/"y" #222)
|
|
505
518
|
step_zs: list[float]
|
|
519
|
+
layout_strips: StripDepths
|
|
520
|
+
layout_n_steps: int
|
|
521
|
+
layout_section: bool
|
|
522
|
+
layout_table_sizes: tuple[tuple[float, float], ...]
|
|
506
523
|
sv_right: float
|
|
507
524
|
iso_right_limit: float
|
|
508
525
|
SCALE: float
|
|
@@ -12,6 +12,8 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
14
|
import math
|
|
15
|
+
import warnings
|
|
16
|
+
from collections.abc import Callable
|
|
15
17
|
|
|
16
18
|
from build123d import Compound, Shape
|
|
17
19
|
from build123d_drafting.helpers import draft_preset
|
|
@@ -19,9 +21,11 @@ from OCP.IFSelect import IFSelect_ReturnStatus
|
|
|
19
21
|
from OCP.STEPControl import STEPControl_Reader
|
|
20
22
|
|
|
21
23
|
from draftwright._core import (
|
|
24
|
+
_CONCENTRIC_TOL_MM,
|
|
22
25
|
_DIM_PAD,
|
|
23
26
|
_FONT_SIZE,
|
|
24
27
|
_MARGIN,
|
|
28
|
+
_MIN_RENDER_MM,
|
|
25
29
|
_MIN_VIEW_MM,
|
|
26
30
|
Analysis,
|
|
27
31
|
_legible_steps,
|
|
@@ -38,9 +42,12 @@ from draftwright.recognition import (
|
|
|
38
42
|
full_cylinders,
|
|
39
43
|
)
|
|
40
44
|
from draftwright.sheet import (
|
|
45
|
+
StripDepths,
|
|
41
46
|
_build_zones,
|
|
47
|
+
_est_hole_table_sizes,
|
|
42
48
|
_layout_geometry,
|
|
43
49
|
_measure_strips,
|
|
50
|
+
_will_section,
|
|
44
51
|
choose_scale,
|
|
45
52
|
)
|
|
46
53
|
|
|
@@ -52,6 +59,41 @@ _log = logging.getLogger(__name__)
|
|
|
52
59
|
_SQUARENESS_TOL = 0.05
|
|
53
60
|
_OD_FILL_MIN = 0.8
|
|
54
61
|
_OD_AXIS_TOL = 0.05
|
|
62
|
+
_ScalePick = tuple[float, float, float, float]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _declared_will_section(model, *, is_rotational=False, cx=0.0, cy=0.0) -> bool:
|
|
66
|
+
"""True when a caller-supplied IR model contains a section-driving Z hole.
|
|
67
|
+
|
|
68
|
+
Detection-based layout uses recogniser holes; declared-model builds may
|
|
69
|
+
intentionally supply features detection missed. Inspect the public IR shape
|
|
70
|
+
duck-typed here so declared sections get the same page/scale reservation.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if model is None:
|
|
74
|
+
return False
|
|
75
|
+
features = getattr(model, "features", model)
|
|
76
|
+
|
|
77
|
+
def feature_member(pt) -> bool:
|
|
78
|
+
return not (is_rotational and math.hypot(pt[0] - cx, pt[1] - cy) <= _CONCENTRIC_TOL_MM)
|
|
79
|
+
|
|
80
|
+
for feat in features:
|
|
81
|
+
if getattr(feat, "kind", None) not in ("hole", "pattern"):
|
|
82
|
+
continue
|
|
83
|
+
frame = getattr(feat, "frame", None)
|
|
84
|
+
if frame is None or frame.axis != "z":
|
|
85
|
+
continue
|
|
86
|
+
members = getattr(feat, "members", ()) or (frame.origin,)
|
|
87
|
+
if not any(feature_member(m) for m in members):
|
|
88
|
+
continue
|
|
89
|
+
bore = getattr(feat, "member", feat)
|
|
90
|
+
if (
|
|
91
|
+
getattr(bore, "cbore", None) is not None
|
|
92
|
+
or getattr(bore, "spotface", None) is not None
|
|
93
|
+
or not getattr(bore, "through", True)
|
|
94
|
+
):
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
55
97
|
|
|
56
98
|
|
|
57
99
|
def _import_step(path) -> Compound:
|
|
@@ -104,6 +146,53 @@ def _is_rotational(x_size, y_size, od_diam, od_axis_offset) -> bool:
|
|
|
104
146
|
)
|
|
105
147
|
|
|
106
148
|
|
|
149
|
+
def _converge_step_sizing(
|
|
150
|
+
initial_steps: int,
|
|
151
|
+
measure_strips: Callable[[int], StripDepths],
|
|
152
|
+
pick_scale: Callable[[int, StripDepths], _ScalePick],
|
|
153
|
+
count_legible: Callable[[float], int],
|
|
154
|
+
) -> tuple[_ScalePick, StripDepths, int]:
|
|
155
|
+
"""Choose scale/page with a step-corridor count that matches legibility.
|
|
156
|
+
|
|
157
|
+
The right-side step ladder is reserved before the scale is known, but the
|
|
158
|
+
actual step list is filtered by the chosen scale. Iterate that dependency
|
|
159
|
+
until it reaches a fixed point; if it cycles, reserve the largest count seen
|
|
160
|
+
so the sheet is sized conservatively instead of silently accepting whichever
|
|
161
|
+
value happened to appear on a fixed iteration budget (#520).
|
|
162
|
+
"""
|
|
163
|
+
n_for_sizing = initial_steps
|
|
164
|
+
seen: set[int] = set()
|
|
165
|
+
attempted: list[int] = []
|
|
166
|
+
max_iter = max(4, initial_steps + 2)
|
|
167
|
+
|
|
168
|
+
for _ in range(max_iter):
|
|
169
|
+
if n_for_sizing in seen:
|
|
170
|
+
break
|
|
171
|
+
seen.add(n_for_sizing)
|
|
172
|
+
attempted.append(n_for_sizing)
|
|
173
|
+
|
|
174
|
+
strips = measure_strips(n_for_sizing)
|
|
175
|
+
scale_pick = pick_scale(n_for_sizing, strips)
|
|
176
|
+
n_next = count_legible(scale_pick[0])
|
|
177
|
+
if n_next == n_for_sizing:
|
|
178
|
+
return scale_pick, strips, n_for_sizing
|
|
179
|
+
if n_next in seen:
|
|
180
|
+
n_for_sizing = n_next
|
|
181
|
+
break
|
|
182
|
+
n_for_sizing = n_next
|
|
183
|
+
|
|
184
|
+
conservative_n = max(attempted + [n_for_sizing], default=initial_steps)
|
|
185
|
+
strips = measure_strips(conservative_n)
|
|
186
|
+
scale_pick = pick_scale(conservative_n, strips)
|
|
187
|
+
_log.warning(
|
|
188
|
+
"Step-corridor sizing did not converge from %d steps (tried %s); reserving %d steps",
|
|
189
|
+
initial_steps,
|
|
190
|
+
attempted,
|
|
191
|
+
conservative_n,
|
|
192
|
+
)
|
|
193
|
+
return scale_pick, strips, conservative_n
|
|
194
|
+
|
|
195
|
+
|
|
107
196
|
# A hole is "concentric" with a turned part's rotation axis when its drilling
|
|
108
197
|
# axis is the Z (OD) axis and its opening sits on the part centreline. Such
|
|
109
198
|
# bores are already dimensioned by the ldr_z bore leaders, so they must not
|
|
@@ -200,7 +289,16 @@ def _solids_body(part, src: str = "part"):
|
|
|
200
289
|
|
|
201
290
|
|
|
202
291
|
def _analyse(
|
|
203
|
-
step_file,
|
|
292
|
+
step_file,
|
|
293
|
+
title,
|
|
294
|
+
number,
|
|
295
|
+
tolerance,
|
|
296
|
+
drawn_by,
|
|
297
|
+
out,
|
|
298
|
+
scale=None,
|
|
299
|
+
page=None,
|
|
300
|
+
pmi="off",
|
|
301
|
+
model=None,
|
|
204
302
|
) -> Analysis:
|
|
205
303
|
"""Load STEP or use a build123d Shape, analyse geometry, compute layout.
|
|
206
304
|
|
|
@@ -315,6 +413,20 @@ def _analyse(
|
|
|
315
413
|
patterns = find_hole_patterns(holes)
|
|
316
414
|
bosses = find_bosses(part, cyls=(z_cyls, cross_cyls)) # detect once — the one inventory (#264)
|
|
317
415
|
slots = find_slots(part)
|
|
416
|
+
layout_section = _will_section(
|
|
417
|
+
holes,
|
|
418
|
+
patterns,
|
|
419
|
+
is_rotational=is_rotational,
|
|
420
|
+
cx=cx,
|
|
421
|
+
cy=cy,
|
|
422
|
+
) or _declared_will_section(model, is_rotational=is_rotational, cx=cx, cy=cy)
|
|
423
|
+
layout_table_sizes = _est_hole_table_sizes(
|
|
424
|
+
holes,
|
|
425
|
+
patterns,
|
|
426
|
+
bb,
|
|
427
|
+
font_size=_FONT_SIZE,
|
|
428
|
+
pad_around_text=_pad_around_text,
|
|
429
|
+
)
|
|
318
430
|
|
|
319
431
|
# Choose scale/page, iterating so the reserved step corridor matches the
|
|
320
432
|
# number of steps the legibility gate will actually place (#1) — not the raw
|
|
@@ -322,40 +434,79 @@ def _analyse(
|
|
|
322
434
|
# with 15 tiny treads) reserves a phantom step ladder that blocks a larger
|
|
323
435
|
# scale. Seed conservatively (all faces), then re-gate at the chosen scale;
|
|
324
436
|
# converges in a couple of rounds.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
for _ in range(3):
|
|
328
|
-
strips_i = _measure_strips(
|
|
437
|
+
def _measure_for_step_count(n_steps_i: int) -> StripDepths:
|
|
438
|
+
return _measure_strips(
|
|
329
439
|
holes,
|
|
330
440
|
patterns,
|
|
331
|
-
|
|
441
|
+
n_steps_i,
|
|
332
442
|
bb,
|
|
333
443
|
arrow_length=_arrow_length,
|
|
334
444
|
pad_around_text=_pad_around_text,
|
|
335
445
|
)
|
|
336
|
-
|
|
337
|
-
|
|
446
|
+
|
|
447
|
+
def _pick_for_step_count(n_steps_i: int, strips_i: StripDepths) -> _ScalePick:
|
|
448
|
+
return choose_scale(
|
|
449
|
+
x_size,
|
|
450
|
+
y_size,
|
|
451
|
+
z_size,
|
|
452
|
+
n_steps=n_steps_i,
|
|
453
|
+
scale=scale,
|
|
454
|
+
page=page,
|
|
455
|
+
strips=strips_i,
|
|
456
|
+
section=layout_section,
|
|
457
|
+
table_sizes=layout_table_sizes,
|
|
338
458
|
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
459
|
+
|
|
460
|
+
(SCALE, PAGE_W, PAGE_H, TB_W), strips_i, n_for_sizing = _converge_step_sizing(
|
|
461
|
+
len(step_zs),
|
|
462
|
+
_measure_for_step_count,
|
|
463
|
+
_pick_for_step_count,
|
|
464
|
+
lambda scale_i: len(_legible_steps(step_zs, bb.min.Z, scale_i)[0]),
|
|
465
|
+
)
|
|
343
466
|
if scale is not None:
|
|
467
|
+
# An explicit scale is the user's call — honour it (#489). Two floors apply:
|
|
468
|
+
# - _MIN_RENDER_MM: a hard geometry limit; below it OCCT's annotation arcs degenerate
|
|
469
|
+
# (Geom_TrimmedCurve U1==U2, ~1e-4 mm empirically — 0.1 mm is a conservative floor).
|
|
470
|
+
# Reject with a clean message; there is no meaningful drawing this small anyway.
|
|
471
|
+
# - _MIN_VIEW_MM: a legibility floor; below it annotations crowd but the drawing is
|
|
472
|
+
# valid, so honour the scale with a warning. This floor does NOT bound the auto scale
|
|
473
|
+
# (choose_scale is a pure geometric page fit), so a warning is only useful when a
|
|
474
|
+
# legible page-fitting scale actually exists — i.e. the auto scale is itself legible.
|
|
475
|
+
min_dim = min(x_size, y_size, z_size)
|
|
476
|
+
min_view = min_dim * SCALE
|
|
477
|
+
if min_view < _MIN_RENDER_MM:
|
|
478
|
+
safe = _MIN_RENDER_MM / min_dim
|
|
479
|
+
raise ValueError(
|
|
480
|
+
f"scale {SCALE!r} projects the smallest part dimension "
|
|
481
|
+
f"({min_dim:.0f} mm) to {min_view:.3g} mm — the drawing geometry degenerates "
|
|
482
|
+
f"below {_MIN_RENDER_MM:g} mm (OCCT arc construction fails). "
|
|
483
|
+
f"Use scale ≥ {safe:.3g} or omit the scale for automatic selection."
|
|
484
|
+
)
|
|
344
485
|
auto_scale, _, _, _ = choose_scale(
|
|
345
|
-
x_size,
|
|
486
|
+
x_size,
|
|
487
|
+
y_size,
|
|
488
|
+
z_size,
|
|
489
|
+
n_steps=n_for_sizing,
|
|
490
|
+
scale=None,
|
|
491
|
+
page=page,
|
|
492
|
+
strips=strips_i,
|
|
493
|
+
section=layout_section,
|
|
494
|
+
table_sizes=layout_table_sizes,
|
|
346
495
|
)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
496
|
+
# Warn only when omitting the scale would truly give a legible fit (auto scale itself is
|
|
497
|
+
# legible) but the requested scale is below the floor. A part illegible at every
|
|
498
|
+
# page-fitting scale can't be helped by a bigger one, so nagging there would be false.
|
|
499
|
+
if min_view < _MIN_VIEW_MM <= auto_scale * min_dim:
|
|
500
|
+
safe = _MIN_VIEW_MM / min_dim
|
|
501
|
+
# No stacklevel that reaches user code: this fires deep in _analyse, and the public
|
|
502
|
+
# entry points (make_drawing, Sheet.export, build_drawing) sit at different depths.
|
|
503
|
+
# The message is self-contained (names the scale, the projection, and the fix).
|
|
504
|
+
warnings.warn(
|
|
505
|
+
f"scale {SCALE!r} projects the smallest part dimension ({min_dim:.0f} mm) to "
|
|
506
|
+
f"{min_view:.1f} mm, below the {_MIN_VIEW_MM:.0f} mm legibility floor — "
|
|
507
|
+
f"annotations may crowd or overlap. Honouring the requested scale; use "
|
|
508
|
+
f"scale ≥ {safe:.3g} or omit the scale for an automatic legible fit."
|
|
509
|
+
)
|
|
359
510
|
DIM_PAD = _DIM_PAD
|
|
360
511
|
margin = _MARGIN
|
|
361
512
|
# Refine: apply the same legibility gate _auto_annotate uses for dim_step.
|
|
@@ -371,7 +522,19 @@ def _analyse(
|
|
|
371
522
|
# View positions + iso empty-rectangle, shared with scale selection (_fits)
|
|
372
523
|
# via _layout_geometry so placement and fit never diverge (#11). _fit_iso_view
|
|
373
524
|
# later scales the iso to fill its rectangle.
|
|
374
|
-
_g = _layout_geometry(
|
|
525
|
+
_g = _layout_geometry(
|
|
526
|
+
x_size,
|
|
527
|
+
y_size,
|
|
528
|
+
z_size,
|
|
529
|
+
SCALE,
|
|
530
|
+
PAGE_W,
|
|
531
|
+
PAGE_H,
|
|
532
|
+
TB_W,
|
|
533
|
+
strips,
|
|
534
|
+
n_steps,
|
|
535
|
+
section=layout_section,
|
|
536
|
+
table_sizes=layout_table_sizes,
|
|
537
|
+
)
|
|
375
538
|
fv_hw = _g.fv_hw
|
|
376
539
|
fv_hh = _g.fv_hh
|
|
377
540
|
pv_hh = _g.pv_hh
|
|
@@ -439,6 +602,10 @@ def _analyse(
|
|
|
439
602
|
is_rotational=is_rotational,
|
|
440
603
|
od_axis=od_axis,
|
|
441
604
|
step_zs=step_zs,
|
|
605
|
+
layout_strips=strips,
|
|
606
|
+
layout_n_steps=n_steps,
|
|
607
|
+
layout_section=layout_section,
|
|
608
|
+
layout_table_sizes=layout_table_sizes,
|
|
442
609
|
sv_right=sv_right,
|
|
443
610
|
iso_right_limit=iso_right_limit,
|
|
444
611
|
SCALE=SCALE,
|