draftwright 0.2.9__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.9 → draftwright-0.2.10}/CHANGELOG.md +43 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/PKG-INFO +4 -4
- {draftwright-0.2.9 → draftwright-0.2.10}/README.md +3 -2
- {draftwright-0.2.9 → draftwright-0.2.10}/pyproject.toml +1 -2
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/_core.py +12 -1
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/analysis.py +34 -12
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/annotations/_common.py +36 -2
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/annotations/from_model.py +45 -5
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/annotations/holes.py +59 -20
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/builder.py +18 -1
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/cli.py +20 -21
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/drawing.py +7 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/layout.py +27 -63
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/linting/__init__.py +2 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/linting/coverage.py +69 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/model/__init__.py +4 -1
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/model/declare.py +88 -27
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/model/detect.py +31 -19
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/model/ir.py +21 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/sheet_dsl.py +136 -9
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/sheet_emit.py +74 -8
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_declare.py +81 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_layout.py +5 -6
- draftwright-0.2.10/tests/test_lint_reconciliation.py +135 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_make_drawing.py +52 -15
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_pmi.py +40 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_sheet_emit.py +151 -11
- 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.9 → draftwright-0.2.10}/tests/test_strip_layout.py +125 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/.gitignore +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/LICENSE +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/skills/SKILL.md +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/annotations/orchestrator.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/annotations/sections.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/export.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/features.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/fits.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/_kernel.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/bracket.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/dshape.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/side_drilled.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_fits.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_gdt_placement.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_linting.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_object_aspects.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_part_model.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_recognition.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_registry.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_sheet_of.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_tolerances.py +0 -0
- {draftwright-0.2.9 → draftwright-0.2.10}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,48 @@
|
|
|
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
|
+
|
|
3
46
|
## v0.2.9 — 2026-07-06
|
|
4
47
|
|
|
5
48
|
**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.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
|
|
@@ -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.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
|
|
|
@@ -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,6 +306,7 @@ 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.
|
|
@@ -369,10 +375,20 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
369
375
|
feats = {c.name: c.feature for c in kept if c.feature is not None} # provenance (ADR 0010)
|
|
370
376
|
sizes = {c.name: c.size for c in kept if c.size is not None} # real footprint (#61)
|
|
371
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)
|
|
372
379
|
left = {
|
|
373
380
|
n
|
|
374
381
|
for n, _ in place_strip_candidates(
|
|
375
|
-
dwg,
|
|
382
|
+
dwg,
|
|
383
|
+
strip,
|
|
384
|
+
view,
|
|
385
|
+
axis,
|
|
386
|
+
pairs,
|
|
387
|
+
tier,
|
|
388
|
+
features=feats,
|
|
389
|
+
sizes=sizes,
|
|
390
|
+
forbid=forbid,
|
|
391
|
+
priorities=prio,
|
|
376
392
|
)
|
|
377
393
|
}
|
|
378
394
|
force_pairs = [(c.name, c.build) for c in kept if c.name in left and c.force]
|
|
@@ -390,6 +406,7 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
390
406
|
features=feats,
|
|
391
407
|
sizes=sizes,
|
|
392
408
|
forbid=forbid,
|
|
409
|
+
priorities=prio,
|
|
393
410
|
)
|
|
394
411
|
}
|
|
395
412
|
if force_pairs
|
|
@@ -424,7 +441,18 @@ def drain_corridors(dwg):
|
|
|
424
441
|
|
|
425
442
|
|
|
426
443
|
def place_strip_candidates(
|
|
427
|
-
dwg,
|
|
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,
|
|
428
456
|
):
|
|
429
457
|
"""Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
|
|
430
458
|
The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
|
|
@@ -450,6 +478,11 @@ def place_strip_candidates(
|
|
|
450
478
|
frame, #61) sets it so :func:`plan_strip` enforces its true stacking gap — over
|
|
451
479
|
capacity it is relocated to the next segment or dropped, never overlapped.
|
|
452
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
|
+
|
|
453
486
|
``force=True`` skips that corridor check — the caller's last resort when no view took
|
|
454
487
|
the dim cleanly: keep it on its natural view and accept the (same-feature) leader
|
|
455
488
|
crossing rather than drop a real dimension (policy B). Candidates that find no strip
|
|
@@ -520,6 +553,7 @@ def place_strip_candidates(
|
|
|
520
553
|
f"{(k if inner == lo else len(take) - 1 - k):04d}",
|
|
521
554
|
anch,
|
|
522
555
|
(sizes or {}).get(nb[0], (tier, tier)),
|
|
556
|
+
priority=(priorities or {}).get(nb[0], 0.0),
|
|
523
557
|
),
|
|
524
558
|
nb,
|
|
525
559
|
)
|
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import math
|
|
22
22
|
|
|
23
|
-
from build123d_drafting import DatumFeature, FeatureControlFrame, SurfaceFinish
|
|
23
|
+
from build123d_drafting import DatumFeature, FeatureControlFrame, SurfaceFinish, TextBlock
|
|
24
24
|
from build123d_drafting.helpers import Centerline, CenterMark, HoleCallout, Leader, TitleBlock
|
|
25
25
|
|
|
26
26
|
from draftwright._core import (
|
|
@@ -58,6 +58,7 @@ from draftwright.annotations._common import (
|
|
|
58
58
|
strip_free_span,
|
|
59
59
|
strip_obstacles,
|
|
60
60
|
)
|
|
61
|
+
from draftwright.layout import StripCandidate, plan_strip
|
|
61
62
|
from draftwright.model.ir import HoleFeature, PatternFeature
|
|
62
63
|
from draftwright.model.planner import DimensionGroup, plan_locations
|
|
63
64
|
|
|
@@ -1294,10 +1295,29 @@ def render_rotational(dwg, model, a) -> int:
|
|
|
1294
1295
|
left_edge = FX(a.bb.min.X)
|
|
1295
1296
|
if left_edge - a.margin >= a.DIM_PAD:
|
|
1296
1297
|
elbow_x = left_edge - a.DIM_PAD * 0.6
|
|
1297
|
-
nb = len(rot.bores)
|
|
1298
1298
|
pitch = max(10.0, draft.font_size * 3.0)
|
|
1299
|
+
# Bound the leader stack to the front-view height and space it via the shared
|
|
1300
|
+
# solve (#374). The old fixed `tip_z = cz + (i-(nb-1)/2)*pitch` had no bound,
|
|
1301
|
+
# so enough concentric bores overran the view (the CTC-02 defect shape). Each
|
|
1302
|
+
# leader keeps that same symmetric natural, so plan_strip reproduces the old
|
|
1303
|
+
# positions exactly when there is room (zero displacement) and only compresses /
|
|
1304
|
+
# drops (larger bore outranks smaller, priority=d) when the band is over capacity.
|
|
1305
|
+
nb = len(rot.bores)
|
|
1306
|
+
z_lo, z_hi = a.FV_Y - a.fv_hh, a.FV_Y + a.fv_hh
|
|
1307
|
+
cands = [
|
|
1308
|
+
StripCandidate(
|
|
1309
|
+
key=f"{i:03d}",
|
|
1310
|
+
anchor=(elbow_x, FZ(a.cz) + (i - (nb - 1) / 2) * pitch),
|
|
1311
|
+
size=(draft.font_size * 3, pitch),
|
|
1312
|
+
priority=d,
|
|
1313
|
+
)
|
|
1314
|
+
for i, d in enumerate(rot.bores)
|
|
1315
|
+
]
|
|
1316
|
+
placed = plan_strip(cands, z_lo, z_hi, pitch, axis="y").placed
|
|
1299
1317
|
for i, d in enumerate(rot.bores):
|
|
1300
|
-
tip_z =
|
|
1318
|
+
tip_z = placed.get(f"{i:03d}")
|
|
1319
|
+
if tip_z is None:
|
|
1320
|
+
continue # over the front-view capacity — dropped (ranked), logged below
|
|
1301
1321
|
dwg.add(
|
|
1302
1322
|
Leader(
|
|
1303
1323
|
tip=(FX(a.cx - d / 2), tip_z, 0),
|
|
@@ -1309,6 +1329,18 @@ def render_rotational(dwg, model, a) -> int:
|
|
|
1309
1329
|
view="front",
|
|
1310
1330
|
)
|
|
1311
1331
|
n += 1
|
|
1332
|
+
dropped = [d for i, d in enumerate(rot.bores) if placed.get(f"{i:03d}") is None]
|
|
1333
|
+
for d in dropped:
|
|
1334
|
+
dwg._drop_callout_diam(
|
|
1335
|
+
d
|
|
1336
|
+
) # exclude from coverage — else double-reported (#374 rev)
|
|
1337
|
+
if dropped:
|
|
1338
|
+
dwg._record_build_issue(
|
|
1339
|
+
"warning",
|
|
1340
|
+
"callout_dropped",
|
|
1341
|
+
f"{len(dropped)} concentric-bore diameter(s) {dropped} not annotated "
|
|
1342
|
+
"(front-view height full) — use a detail view",
|
|
1343
|
+
)
|
|
1312
1344
|
else:
|
|
1313
1345
|
_log.info(
|
|
1314
1346
|
"Additional diameters %s not annotated (insufficient left margin)",
|
|
@@ -1781,12 +1813,17 @@ def render_pmi(dwg, model, a) -> int:
|
|
|
1781
1813
|
|
|
1782
1814
|
# GD&T aspect side-layer (ADR 0011 §4, #61) — declared feature control frames / datum
|
|
1783
1815
|
# feature symbols / surface finishes. Placed as first-class ADR 0009 corridor candidates,
|
|
1784
|
-
# NOT through the dimension planner (their IR items carry no DimParameters).
|
|
1785
|
-
|
|
1816
|
+
# NOT through the dimension planner (their IR items carry no DimParameters). "note" is a
|
|
1817
|
+
# free-text manufacturing note (#488) — the same leader-into-a-strip mechanism, glyph = text.
|
|
1818
|
+
_GDT_KINDS = ("control_frame", "datum_ref", "finish", "note")
|
|
1786
1819
|
# Outer run of the shared corridor ladder: GD&T frames tier BEYOND the feature-size
|
|
1787
1820
|
# (_SIZE_SUBCHAIN=0) and datum-location (_LOC_SUBCHAIN=1) dim runs, so a frame never lands
|
|
1788
1821
|
# mid-ladder among the dimensions it annotates.
|
|
1789
1822
|
_GDT_SUBCHAIN = 2
|
|
1823
|
+
# Over-capacity survival rank for an authored GD&T frame (#357): a declared control frame /
|
|
1824
|
+
# datum / finish / note is deliberate intent, so on a strip too full for every candidate it is
|
|
1825
|
+
# kept over the auto dims (locations/slots, priority 0) rather than dropped by stacking-key order.
|
|
1826
|
+
_GDT_CORRIDOR_PRIORITY = 1.0
|
|
1790
1827
|
# Minimum GD&T leader shaft length (page-mm). A zero-length Leader (site == solved tier)
|
|
1791
1828
|
# makes OCC's edge builder raise; nudging to this keeps `_build` total (#61 review).
|
|
1792
1829
|
_MIN_LEADER = 0.05
|
|
@@ -1807,6 +1844,8 @@ def _gdt_glyph(item, draft):
|
|
|
1807
1844
|
)
|
|
1808
1845
|
if item.kind == "datum_ref":
|
|
1809
1846
|
return DatumFeature(item.letter, draft=draft)
|
|
1847
|
+
if item.kind == "note": # free-text manufacturing note (#488) — a single-line text glyph
|
|
1848
|
+
return TextBlock([item.text], position=(0.0, 0.0), draft=draft)
|
|
1810
1849
|
return SurfaceFinish(item.ra, position=(0.0, 0.0), draft=draft)
|
|
1811
1850
|
|
|
1812
1851
|
|
|
@@ -1943,6 +1982,7 @@ def render_gdt(dwg, model, a) -> int:
|
|
|
1943
1982
|
on_drop=_drop,
|
|
1944
1983
|
dedup=None,
|
|
1945
1984
|
precedence=0,
|
|
1985
|
+
priority=_GDT_CORRIDOR_PRIORITY, # authored intent outranks auto dims (#357)
|
|
1946
1986
|
# A declared frame has no alternate view — force-keep (policy B) rather than
|
|
1947
1987
|
# drop a user-authored annotation; only a physically full strip drops.
|
|
1948
1988
|
force=True,
|
|
@@ -861,32 +861,71 @@ def _place_pitch_dim(dwg, a: Analysis, view, loc1, loc2, n, pitch, to_page, name
|
|
|
861
861
|
else:
|
|
862
862
|
pref = (-0.3, 1.0) if view == "plan" else (-0.3, -1.0)
|
|
863
863
|
side, reach = max(cands, key=lambda c: c[0][0] * pref[0] + c[0][1] * pref[1])
|
|
864
|
-
|
|
864
|
+
|
|
865
|
+
def _place(off):
|
|
866
|
+
dwg.add(
|
|
867
|
+
_dim(
|
|
868
|
+
(p1[0], p1[1], 0),
|
|
869
|
+
(p2[0], p2[1], 0),
|
|
870
|
+
side,
|
|
871
|
+
off,
|
|
872
|
+
dwg.draft,
|
|
873
|
+
label=f"{n - 1}× {_fmt(pitch)}",
|
|
874
|
+
),
|
|
875
|
+
name,
|
|
876
|
+
view=view,
|
|
877
|
+
feature=feature,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# Place onto the zone strip for the chosen side (#374): each side is its own strip, so the
|
|
881
|
+
# obstacle-aware carve stacks this dim clear of placed content — where an arbitrary-direction
|
|
882
|
+
# 1-D search would be defeated by a dim on a *different* side (rotated/two-axis grids). An
|
|
883
|
+
# axis-aligned side maps to a populated strip; a diagonal side, the side view's absent left
|
|
884
|
+
# strip, or a genuinely full strip fall through to the bounded vector placement below
|
|
885
|
+
# (behaviour unchanged for those residual cases).
|
|
886
|
+
# Only a GENUINELY axis-aligned row uses the strip carve: its dim is a clean horizontal /
|
|
887
|
+
# vertical line occupying one strip tier, and the coord bridge is exact. `_dim` places the
|
|
888
|
+
# line at `mid + side*distance`, so the witness is the MIDPOINT component on the strip axis and
|
|
889
|
+
# `sgn` the exact outward sign (±1); `distance = sgn*(pos - mid[axis])` then lands the line at
|
|
890
|
+
# the carved `pos`. The tight threshold (≈1.6°) keeps a tilted row off this path — its dim
|
|
891
|
+
# isn't axis-aligned, so it can't cleanly occupy a tier — routing it to the vector fallback.
|
|
892
|
+
zones = {"plan": a.pv_zones, "front": a.fv_zones, "side": a.sv_zones}[view]
|
|
893
|
+
sx, sy = side[0], side[1]
|
|
894
|
+
strip = axis = perp = None
|
|
895
|
+
witness = sgn = 0.0
|
|
896
|
+
if abs(sx) > 0.9996: # horizontal side ⟂ a vertical row → left/right strip, stacks along x
|
|
897
|
+
strip, axis, perp, witness, sgn = (
|
|
898
|
+
(zones.right if sx > 0 else zones.left),
|
|
899
|
+
"x",
|
|
900
|
+
tuple(sorted((p1[1], p2[1]))),
|
|
901
|
+
mid[0],
|
|
902
|
+
math.copysign(1.0, sx),
|
|
903
|
+
)
|
|
904
|
+
elif abs(sy) > 0.9996: # vertical side ⟂ a horizontal row → above/below strip, stacks along y
|
|
905
|
+
strip, axis, perp, witness, sgn = (
|
|
906
|
+
(zones.above if sy > 0 else zones.below),
|
|
907
|
+
"y",
|
|
908
|
+
tuple(sorted((p1[0], p2[0]))),
|
|
909
|
+
mid[1],
|
|
910
|
+
math.copysign(1.0, sy),
|
|
911
|
+
)
|
|
912
|
+
if strip is not None:
|
|
913
|
+
tier = max(10.0, dwg.draft.font_size * 3.0)
|
|
914
|
+
pos = carve_free_position(dwg, strip, view, axis, tier, perp)
|
|
915
|
+
if pos is not None:
|
|
916
|
+
_place(sgn * (pos - witness))
|
|
917
|
+
return
|
|
918
|
+
|
|
919
|
+
# Fallback: diagonal side / absent strip / full strip → the pre-#374 bounded vector placement,
|
|
920
|
+
# count-stacked and margin-guarded, so those residual cases are byte-identical to before.
|
|
865
921
|
prior = sum(1 for nm, _ in dwg.iter_annotations() if nm.startswith(f"dim_pitch_{view}"))
|
|
866
922
|
offset = reach + 8 + 10 * prior
|
|
867
|
-
# never force-place: skip (and log) when the dim line would leave the page
|
|
868
923
|
ox = mid[0] + side[0] * (offset + 6)
|
|
869
924
|
oy = mid[1] + side[1] * (offset + 6)
|
|
870
925
|
if not (a.margin <= ox <= a.PAGE_W - a.margin and a.margin <= oy <= a.PAGE_H - a.margin):
|
|
871
|
-
_log.info(
|
|
872
|
-
"Pitch dimension for the %s× %s array skipped (no room)",
|
|
873
|
-
n,
|
|
874
|
-
_fmt(pitch),
|
|
875
|
-
)
|
|
926
|
+
_log.info("Pitch dimension for the %s× %s array skipped (no room)", n, _fmt(pitch))
|
|
876
927
|
return
|
|
877
|
-
|
|
878
|
-
_dim(
|
|
879
|
-
(p1[0], p1[1], 0),
|
|
880
|
-
(p2[0], p2[1], 0),
|
|
881
|
-
side,
|
|
882
|
-
offset,
|
|
883
|
-
dwg.draft,
|
|
884
|
-
label=f"{n - 1}× {_fmt(pitch)}",
|
|
885
|
-
),
|
|
886
|
-
name,
|
|
887
|
-
view=view,
|
|
888
|
-
feature=feature,
|
|
889
|
-
)
|
|
928
|
+
_place(offset)
|
|
890
929
|
|
|
891
930
|
|
|
892
931
|
def build_view_of_axis(a: Analysis):
|
|
@@ -40,7 +40,15 @@ from draftwright.annotate import _auto_annotate, build_model, build_rotational_f
|
|
|
40
40
|
from draftwright.annotations.sections import feature_hole_keys
|
|
41
41
|
from draftwright.drawing import Drawing
|
|
42
42
|
from draftwright.fonts import PLEX_MONO
|
|
43
|
-
from draftwright.model import
|
|
43
|
+
from draftwright.model import (
|
|
44
|
+
Datum,
|
|
45
|
+
Feature,
|
|
46
|
+
PartModel,
|
|
47
|
+
StepFeature,
|
|
48
|
+
build_pmi_features,
|
|
49
|
+
display,
|
|
50
|
+
plan_sections,
|
|
51
|
+
)
|
|
44
52
|
from draftwright.projection import (
|
|
45
53
|
_fit_iso_view,
|
|
46
54
|
_project_iso,
|
|
@@ -274,6 +282,15 @@ def _assemble(a, out, assembly, detail_view, auto_dims, model=None, decorations=
|
|
|
274
282
|
rot = build_rotational_feature(a)
|
|
275
283
|
if rot is not None:
|
|
276
284
|
dwg._part_model = replace(pm, features=[*pm.features, rot])
|
|
285
|
+
# PMI (STEP AP242) is likewise detection-sourced, so a declared / emitted-script model
|
|
286
|
+
# carries none. When PMI annotation is on, synthesise the same PmiFeatures detection
|
|
287
|
+
# would (render_pmi reads them off the model, gated on a.pmi_mode) so a re-run reproduces
|
|
288
|
+
# the PMI dims (#472). Gated on the caller not having declared PMI, so an explicit set wins.
|
|
289
|
+
pm = dwg._part_model
|
|
290
|
+
if a.pmi_mode == "annotate" and not any(f.kind == "pmi" for f in pm.features):
|
|
291
|
+
pmi_feats = build_pmi_features(a.pmi, a.part.bounding_box())
|
|
292
|
+
if pmi_feats:
|
|
293
|
+
dwg._part_model = replace(pm, features=[*pm.features, *pmi_feats])
|
|
277
294
|
dwg._model_declared = model is not None # ADR 0011 #448: gate model-driven hole render
|
|
278
295
|
|
|
279
296
|
part_s = a.part.scale(a.SCALE)
|
|
@@ -168,34 +168,33 @@ def main(
|
|
|
168
168
|
if style == "sheet":
|
|
169
169
|
from draftwright.sheet_emit import generate_sheet_script, resolve_object_spec
|
|
170
170
|
|
|
171
|
-
# The Sheet DSL
|
|
172
|
-
#
|
|
173
|
-
# `--script --drawn-by … --scale …` invocation isn't quietly a no-op.
|
|
174
|
-
ignored = [
|
|
175
|
-
flag
|
|
176
|
-
for flag, set_ in (
|
|
177
|
-
("--tolerance", tolerance != "ISO 2768-m"),
|
|
178
|
-
("--drawn-by", bool(drawn_by)),
|
|
179
|
-
("--scale", scale is not None),
|
|
180
|
-
("--page", page is not None),
|
|
181
|
-
)
|
|
182
|
-
if set_
|
|
183
|
-
]
|
|
184
|
-
if ignored:
|
|
185
|
-
typer.echo(
|
|
186
|
-
f"warning: {', '.join(ignored)} not supported by --style sheet; ignored "
|
|
187
|
-
"(use --style imperative to embed them)",
|
|
188
|
-
err=True,
|
|
189
|
-
)
|
|
171
|
+
# The Sheet DSL now carries the title-block / layout aspects (#474), so forward all
|
|
172
|
+
# four flags — the generated script reproduces them on re-run (no more inert warning).
|
|
190
173
|
if _looks_like_object_spec(step_file):
|
|
191
174
|
# STEP_FILE is a `module:attr` / `file.py:attr` spec → reference a live object
|
|
192
175
|
obj, seam = resolve_object_spec(step_file)
|
|
193
176
|
py_path = generate_sheet_script(
|
|
194
|
-
obj,
|
|
177
|
+
obj,
|
|
178
|
+
out=out,
|
|
179
|
+
title=title,
|
|
180
|
+
number=number,
|
|
181
|
+
tolerance=tolerance,
|
|
182
|
+
drawn_by=drawn_by,
|
|
183
|
+
scale=scale,
|
|
184
|
+
page=page,
|
|
185
|
+
part_expr=seam,
|
|
195
186
|
)
|
|
196
187
|
else:
|
|
197
188
|
py_path = generate_sheet_script(
|
|
198
|
-
step_file,
|
|
189
|
+
step_file,
|
|
190
|
+
out=out,
|
|
191
|
+
title=title,
|
|
192
|
+
number=number,
|
|
193
|
+
tolerance=tolerance,
|
|
194
|
+
drawn_by=drawn_by,
|
|
195
|
+
scale=scale,
|
|
196
|
+
page=page,
|
|
197
|
+
pmi=pmi.value,
|
|
199
198
|
)
|
|
200
199
|
else:
|
|
201
200
|
if _looks_like_object_spec(step_file):
|
|
@@ -70,6 +70,7 @@ from draftwright.linting import (
|
|
|
70
70
|
LintIssue,
|
|
71
71
|
_suggest_fix,
|
|
72
72
|
lint_axial_coverage,
|
|
73
|
+
lint_declaration_reconciliation,
|
|
73
74
|
lint_drawing,
|
|
74
75
|
lint_feature_coverage,
|
|
75
76
|
lint_location_coverage,
|
|
@@ -1649,6 +1650,12 @@ class Drawing:
|
|
|
1649
1650
|
holes=holes,
|
|
1650
1651
|
patterns=patterns,
|
|
1651
1652
|
)
|
|
1653
|
+
# Reverse direction (#487): a DECLARED feature with no matching geometry (a stale
|
|
1654
|
+
# phantom callout). Only for a caller-supplied model — detection can't over-declare.
|
|
1655
|
+
# _part_model is typed `object` (deliberately loose, #397); read features duck-typed.
|
|
1656
|
+
if self._model_declared and self._part_model is not None:
|
|
1657
|
+
features = getattr(self._part_model, "features", ())
|
|
1658
|
+
issues += lint_declaration_reconciliation(features, cyls)
|
|
1652
1659
|
issues += list(self._build_issues)
|
|
1653
1660
|
# Attach a ready-to-paste fix snippet where one is computable (#29).
|
|
1654
1661
|
# str | None — None when no concrete repair can be inferred.
|