draftwright 0.2.6__tar.gz → 0.2.8__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.6 → draftwright-0.2.8}/CHANGELOG.md +46 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/PKG-INFO +6 -3
- {draftwright-0.2.6 → draftwright-0.2.8}/README.md +1 -1
- {draftwright-0.2.6 → draftwright-0.2.8}/pyproject.toml +18 -2
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/cli.py +71 -11
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/model/declare.py +39 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/sheet_dsl.py +88 -4
- draftwright-0.2.8/src/draftwright/sheet_emit.py +286 -0
- draftwright-0.2.8/tests/_kernel.py +19 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_layout_snapshot.py +5 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_make_drawing.py +6 -0
- draftwright-0.2.8/tests/test_object_aspects.py +89 -0
- draftwright-0.2.8/tests/test_sheet_emit.py +471 -0
- draftwright-0.2.8/tests/test_sheet_of.py +81 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/.gitignore +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/LICENSE +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/skills/SKILL.md +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/_core.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/analysis.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/annotations/_common.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/annotations/from_model.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/annotations/holes.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/annotations/orchestrator.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/annotations/sections.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/builder.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/drawing.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/export.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/features.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/fits.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/bracket.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/dshape.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/side_drilled.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_declare.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_fits.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_layout.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_linting.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_part_model.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_pmi.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_recognition.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_registry.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_strip_layout.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_tolerances.py +0 -0
- {draftwright-0.2.6 → draftwright-0.2.8}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.8 — 2026-07-06
|
|
4
|
+
|
|
5
|
+
**`--script` now defaults to the declarative `Sheet` DSL.** The editable script the CLI
|
|
6
|
+
writes is the beautiful-Python surface — one commentable line per feature — for both STEP and
|
|
7
|
+
build123d-object input.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`draftwright … --script` now emits the declarative `Sheet` script by default** (was the
|
|
12
|
+
imperative edit-verb reconstruction). The imperative reconstruction is still available via
|
|
13
|
+
`--script --style imperative`. For prismatic parts the generated script reproduces the same
|
|
14
|
+
drawing as a direct build; a turned shaft's centre lines and base-diameter style are not yet
|
|
15
|
+
identical (the remaining validity gap, tracked separately). `--style imperative` now errors
|
|
16
|
+
clearly on a `module:attr` object spec (it reads a STEP file; use the default `sheet`).
|
|
17
|
+
The Sheet DSL doesn't yet model the title-block / layout aspects the imperative script
|
|
18
|
+
embeds, so `--script --style sheet` **warns** when `--drawn-by`, `--tolerance`, `--scale`
|
|
19
|
+
or `--page` is set rather than silently ignoring it (use `--style imperative` to embed them).
|
|
20
|
+
|
|
21
|
+
## v0.2.7 — 2026-07-05
|
|
22
|
+
|
|
23
|
+
**The three authoring modes (ADR 0011 Amendment 1).** Naming how you drive draftwright —
|
|
24
|
+
*just do it* / *auto then tweak* / *generate an editable script* — and filling in the third:
|
|
25
|
+
a declarative `Sheet`-DSL generator, and number-free authoring against the build123d objects
|
|
26
|
+
you built.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`draftwright part.step --script --style sheet`** — generate an **editable declarative
|
|
31
|
+
`Sheet` script** from a detected part: one commentable line per feature (hole / diameter /
|
|
32
|
+
step / slot / pattern / envelope), views noted, the auto-section flagged. Comment a line out
|
|
33
|
+
to drop that feature; the script re-runs to the drawing. The existing `--script` (imperative
|
|
34
|
+
edit-verb reconstruction) stays the default (#461).
|
|
35
|
+
- **Object-reading `.cbore(tool)` / `.spotface(tool)`** on a declared hole — read the
|
|
36
|
+
counterbore/spotface's ⌀ **and** depth off the tool object + part (depth measured from the
|
|
37
|
+
local open face, so a rib/wall elsewhere doesn't skew it). A `Sheet`-declared drawing can now
|
|
38
|
+
carry **zero magic numbers** and track the geometry parametrically (#462).
|
|
39
|
+
- **`Sheet.of(feature)`** — a fluent handle onto an **existing / detection-seeded** feature (by
|
|
40
|
+
the build123d object, an index, or the `Feature` itself), so you can `.fit(...)` /
|
|
41
|
+
`.tolerance(...)` a feature from `Sheet.from_part()` without re-declaring it (#463).
|
|
42
|
+
|
|
43
|
+
### Docs
|
|
44
|
+
|
|
45
|
+
- **ADR 0011 Amendment 1** — the three authoring modes and the mode-3 generation surface;
|
|
46
|
+
records the decision to emit **honest detected numbers** for a detected part rather than
|
|
47
|
+
fabricate a build123d reconstruction that misrepresents the geometry (#464).
|
|
48
|
+
|
|
3
49
|
## v0.2.6 — 2026-07-05
|
|
4
50
|
|
|
5
51
|
**ADR 0011 — the IR as a public input: declare features, don't only detect them.**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
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
|
|
@@ -678,10 +678,13 @@ Classifier: Programming Language :: Python :: 3
|
|
|
678
678
|
Classifier: Programming Language :: Python :: 3.10
|
|
679
679
|
Classifier: Programming Language :: Python :: 3.11
|
|
680
680
|
Classifier: Programming Language :: Python :: 3.12
|
|
681
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
682
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
681
683
|
Classifier: Topic :: Scientific/Engineering
|
|
682
684
|
Requires-Python: <3.15,>=3.10
|
|
683
685
|
Requires-Dist: build123d-drafting-helpers>=0.13.0
|
|
684
|
-
Requires-Dist: build123d
|
|
686
|
+
Requires-Dist: build123d<0.11,>=0.9.0; python_full_version < '3.13'
|
|
687
|
+
Requires-Dist: build123d<0.12,>=0.11; python_full_version >= '3.13'
|
|
685
688
|
Requires-Dist: kiwisolver<2,>=1.4
|
|
686
689
|
Requires-Dist: numpy>=1.24
|
|
687
690
|
Requires-Dist: reportlab>=4.0
|
|
@@ -729,7 +732,7 @@ Choose formats, scale, and page; or emit an editable drawing script:
|
|
|
729
732
|
```
|
|
730
733
|
draftwright my_part.step --format pdf,dxf # also: svg, all
|
|
731
734
|
draftwright my_part.step --scale 2 --page A3 # override the auto scale / page
|
|
732
|
-
draftwright my_part.step --script # write an editable
|
|
735
|
+
draftwright my_part.step --script # write an editable declarative Sheet script
|
|
733
736
|
```
|
|
734
737
|
|
|
735
738
|
`draftwright --help` lists every flag; `--version` prints the version.
|
|
@@ -38,7 +38,7 @@ Choose formats, scale, and page; or emit an editable drawing script:
|
|
|
38
38
|
```
|
|
39
39
|
draftwright my_part.step --format pdf,dxf # also: svg, all
|
|
40
40
|
draftwright my_part.step --scale 2 --page A3 # override the auto scale / page
|
|
41
|
-
draftwright my_part.step --script # write an editable
|
|
41
|
+
draftwright my_part.step --script # write an editable declarative Sheet script
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
`draftwright --help` lists every flag; `--version` prints the version.
|
|
@@ -4,14 +4,19 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "draftwright"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
description = "Automated technical-drawing generation for build123d"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
11
11
|
authors = [{ name = "Paul Fremantle", email = "pzfreo@gmail.com" }]
|
|
12
12
|
requires-python = ">=3.10,<3.15"
|
|
13
13
|
dependencies = [
|
|
14
|
-
|
|
14
|
+
# build123d ≤0.10 pulls cadquery-ocp (VTK), whose VTK caps at Python 3.12; 0.11 switched to
|
|
15
|
+
# cadquery-ocp-novtk, which ships 3.13/3.14 wheels and drops VTK entirely (draftwright is
|
|
16
|
+
# headless — it never imports VTK). Split by Python: keep the proven ≤3.12 stack, use 0.11 on
|
|
17
|
+
# 3.13+. Mirrors pzfreo/build123d-mcp's dependency handling.
|
|
18
|
+
"build123d>=0.9.0,<0.11 ; python_full_version < '3.13'",
|
|
19
|
+
"build123d>=0.11,<0.12 ; python_full_version >= '3.13'",
|
|
15
20
|
"build123d-drafting-helpers>=0.13.0",
|
|
16
21
|
"kiwisolver>=1.4,<2",
|
|
17
22
|
"numpy>=1.24",
|
|
@@ -30,6 +35,8 @@ classifiers = [
|
|
|
30
35
|
"Programming Language :: Python :: 3.10",
|
|
31
36
|
"Programming Language :: Python :: 3.11",
|
|
32
37
|
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Programming Language :: Python :: 3.14",
|
|
33
40
|
]
|
|
34
41
|
|
|
35
42
|
[project.scripts]
|
|
@@ -112,3 +119,12 @@ dev = [
|
|
|
112
119
|
"pytest-xdist>=3",
|
|
113
120
|
"ruff>=0.4",
|
|
114
121
|
]
|
|
122
|
+
|
|
123
|
+
[tool.uv]
|
|
124
|
+
# On Python 3.13/3.14, build123d resolves to 0.11, whose OCP kernel is
|
|
125
|
+
# cadquery-ocp-novtk (VTK dropped from the transitive tree — fine here, draftwright
|
|
126
|
+
# renders headless and never imports VTK). Its 7.9.3.1.1 release ships a broken macOS
|
|
127
|
+
# wheel (no OCP.GccEnt module → build123d fails to import), so pin it out. Only bites
|
|
128
|
+
# when novtk is actually resolved (build123d 0.11); harmless on 0.10/cadquery-ocp.
|
|
129
|
+
# Mirrors pzfreo/build123d-mcp. Remove once upstream fixes the wheel.
|
|
130
|
+
constraint-dependencies = ["cadquery-ocp-novtk!=7.9.3.1.1"]
|
|
@@ -73,6 +73,18 @@ def _emit(dwg, formats: list[str]) -> list[str]:
|
|
|
73
73
|
return [paths[f] for f in formats]
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _looks_like_object_spec(s: str) -> bool:
|
|
77
|
+
"""True when *s* is a ``module:attr`` / ``file.py:attr`` object reference rather than a STEP
|
|
78
|
+
path (#469). An existing file is never a spec; a plain path — a STEP file, or a Windows
|
|
79
|
+
``C:\\…\\part.step`` drive path — has no trailing ``:identifier``, so the pattern rejects it
|
|
80
|
+
(a Windows drive colon is followed by ``\\``, not an identifier; filenames can't contain ':')."""
|
|
81
|
+
import re
|
|
82
|
+
|
|
83
|
+
if os.path.exists(s):
|
|
84
|
+
return False
|
|
85
|
+
return re.match(r"^.+:[A-Za-z_][A-Za-z0-9_]*$", s) is not None
|
|
86
|
+
|
|
87
|
+
|
|
76
88
|
def _installed_version() -> str:
|
|
77
89
|
"""The installed distribution version (the PyPI version once pip-installed);
|
|
78
90
|
``"unknown"`` when running from a source tree with no installed metadata."""
|
|
@@ -105,6 +117,12 @@ def main(
|
|
|
105
117
|
script: bool = typer.Option(
|
|
106
118
|
False, "--script", help="Write an editable .py drawing script instead of SVG+DXF"
|
|
107
119
|
),
|
|
120
|
+
style: str = typer.Option(
|
|
121
|
+
"sheet",
|
|
122
|
+
"--style",
|
|
123
|
+
help="--script flavour: 'sheet' (declarative Sheet DSL - one line per feature, "
|
|
124
|
+
"default) or 'imperative' (edit-verb reconstruction)",
|
|
125
|
+
),
|
|
108
126
|
pmi: PmiMode = typer.Option(
|
|
109
127
|
PmiMode.off,
|
|
110
128
|
help=(
|
|
@@ -136,6 +154,9 @@ def main(
|
|
|
136
154
|
logging.basicConfig(level=logging.INFO if verbose else logging.WARNING, format="%(message)s")
|
|
137
155
|
|
|
138
156
|
formats = _parse_formats(output_format)
|
|
157
|
+
if script and style not in ("imperative", "sheet"):
|
|
158
|
+
# validate before the ~5 s engine import so a typo fails fast
|
|
159
|
+
raise typer.BadParameter("--style must be 'imperative' or 'sheet'", param_hint="--style")
|
|
139
160
|
|
|
140
161
|
# Import the engine lazily, only on the build path: it pulls in build123d/OCP
|
|
141
162
|
# (~5 s of CAD-kernel import). Keeping it out of module scope means shell
|
|
@@ -144,17 +165,56 @@ def main(
|
|
|
144
165
|
from draftwright.builder import build_drawing, generate_script
|
|
145
166
|
|
|
146
167
|
if script:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
title
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
if style == "sheet":
|
|
169
|
+
from draftwright.sheet_emit import generate_sheet_script, resolve_object_spec
|
|
170
|
+
|
|
171
|
+
# The Sheet DSL doesn't yet model the title-block / layout aspects the
|
|
172
|
+
# imperative script embeds. Warn rather than silently drop them so a
|
|
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
|
+
)
|
|
190
|
+
if _looks_like_object_spec(step_file):
|
|
191
|
+
# STEP_FILE is a `module:attr` / `file.py:attr` spec → reference a live object
|
|
192
|
+
obj, seam = resolve_object_spec(step_file)
|
|
193
|
+
py_path = generate_sheet_script(
|
|
194
|
+
obj, out=out, title=title, number=number, part_expr=seam
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
py_path = generate_sheet_script(
|
|
198
|
+
step_file, out=out, title=title, number=number, pmi=pmi.value
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
if _looks_like_object_spec(step_file):
|
|
202
|
+
raise typer.BadParameter(
|
|
203
|
+
"the imperative reconstruction reads a STEP file; use --style sheet "
|
|
204
|
+
"(the default) to reference a 'module:attr' object",
|
|
205
|
+
param_hint="--style",
|
|
206
|
+
)
|
|
207
|
+
py_path = generate_script(
|
|
208
|
+
step_file=step_file,
|
|
209
|
+
out=out,
|
|
210
|
+
title=title,
|
|
211
|
+
number=number,
|
|
212
|
+
tolerance=tolerance,
|
|
213
|
+
drawn_by=drawn_by,
|
|
214
|
+
pmi=pmi.value,
|
|
215
|
+
scale=scale,
|
|
216
|
+
page=page,
|
|
217
|
+
)
|
|
158
218
|
print(py_path)
|
|
159
219
|
return
|
|
160
220
|
|
|
@@ -128,6 +128,45 @@ def _read_cylinder(obj) -> tuple[str, float, Point]:
|
|
|
128
128
|
return axis, dia, center
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def read_bore_step(part, tool, axis: str) -> tuple[float, float]:
|
|
132
|
+
"""``(diameter, depth)`` of a counterbore / spotface *tool* cut into *part* (ADR 0011 #462).
|
|
133
|
+
|
|
134
|
+
⌀ comes from the tool's cylindrical face (like :func:`_read_cylinder`); **depth** is measured
|
|
135
|
+
along the hole *axis* from the part's **open face at the hole** to the tool's inner (floor)
|
|
136
|
+
face — so both values track the geometry you built, restating no numbers.
|
|
137
|
+
|
|
138
|
+
The open face is found *locally*: the part is intersected with an axial prism over the tool's
|
|
139
|
+
cross-section, so a rib / wall / boss elsewhere on the part doesn't skew the reading (the
|
|
140
|
+
global bounding box would). The counterbore opens on whichever end of that local column the
|
|
141
|
+
tool sits nearer, even when the tool overhangs the part."""
|
|
142
|
+
from build123d import Box, Pos
|
|
143
|
+
|
|
144
|
+
axis = _norm_axis(axis)
|
|
145
|
+
dia = _read_cylinder(tool)[1]
|
|
146
|
+
i = "xyz".index(axis)
|
|
147
|
+
tb, pb = tool.bounding_box(), part.bounding_box()
|
|
148
|
+
t_lo, t_hi = [tb.min.X, tb.min.Y, tb.min.Z][i], [tb.max.X, tb.max.Y, tb.max.Z][i]
|
|
149
|
+
|
|
150
|
+
# A prism over the tool's cross-section, spanning the part along the axis; intersect it with
|
|
151
|
+
# the part to get the LOCAL material column under the tool (the open face is its axial edge).
|
|
152
|
+
size = [tb.size.X, tb.size.Y, tb.size.Z]
|
|
153
|
+
cen = [tb.center().X, tb.center().Y, tb.center().Z]
|
|
154
|
+
size[i] = [pb.size.X, pb.size.Y, pb.size.Z][i] * 3 or 1.0
|
|
155
|
+
cen[i] = [pb.center().X, pb.center().Y, pb.center().Z][i]
|
|
156
|
+
try:
|
|
157
|
+
column = part & (Pos(*cen) * Box(*size))
|
|
158
|
+
cb = column.bounding_box()
|
|
159
|
+
p_lo, p_hi = [cb.min.X, cb.min.Y, cb.min.Z][i], [cb.max.X, cb.max.Y, cb.max.Z][i]
|
|
160
|
+
except Exception: # noqa: BLE001 — degenerate boolean; fall back to the global bbox
|
|
161
|
+
p_lo, p_hi = [pb.min.X, pb.min.Y, pb.min.Z][i], [pb.max.X, pb.max.Y, pb.max.Z][i]
|
|
162
|
+
|
|
163
|
+
if (t_lo + t_hi) / 2 >= (p_lo + p_hi) / 2:
|
|
164
|
+
depth = p_hi - t_lo # floor at the tool bottom, open at the max face
|
|
165
|
+
else:
|
|
166
|
+
depth = t_hi - p_lo # floor at the tool top, open at the min face
|
|
167
|
+
return (round(dia, 3), round(depth, 3))
|
|
168
|
+
|
|
169
|
+
|
|
131
170
|
def _span(at: Point, axis: str, length: float) -> tuple[Point, Point]:
|
|
132
171
|
"""The two axial end-points of a segment of *length* centred at *at* along *axis*."""
|
|
133
172
|
idx = "xyz".index(axis)
|
|
@@ -15,15 +15,17 @@ detection is skipped and the auto-pass dimensions exactly the declared features:
|
|
|
15
15
|
**Scope (this module):** the *feature-declaration* surface over the renderers the
|
|
16
16
|
engine has today — dimensions, ⌀ callouts, holes (through / blind), turned steps,
|
|
17
17
|
slots, patterns, the overall envelope, and the auto section — plus the P2a
|
|
18
|
-
**``.tolerance``**
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
**``.tolerance``** (a ± / limit tolerance on a diameter, a step, or a hole bore) and
|
|
19
|
+
**``.fit``** (fit-class → ISO 286 deviation, P2a.2) aspects. The remaining #445 aspect
|
|
20
|
+
verbs that still need new rendering — ``.thread``, ``.finish`` (surface symbols) and
|
|
21
21
|
``control(...)`` (GD&T) — are the later Phase-2 items (roadmap #446) and are
|
|
22
22
|
deliberately **not** stubbed here, so the surface only exposes what actually draws.
|
|
23
23
|
|
|
24
24
|
**Hybrid.** :meth:`Sheet.from_part` seeds the declared set from *detection*, so you
|
|
25
25
|
can start from the detected model and override specific features (declaration is for
|
|
26
|
-
where you know better than detection, not everywhere — ADR 0011 §3).
|
|
26
|
+
where you know better than detection, not everywhere — ADR 0011 §3); :meth:`Sheet.of`
|
|
27
|
+
returns a fluent handle onto one of those generated features (by object, index, or the
|
|
28
|
+
feature itself) so you can ``.fit(...)`` / ``.tolerance(...)`` it without re-declaring (#463).
|
|
27
29
|
"""
|
|
28
30
|
|
|
29
31
|
from __future__ import annotations
|
|
@@ -33,12 +35,15 @@ from dataclasses import replace
|
|
|
33
35
|
from draftwright.analysis import _solids_body
|
|
34
36
|
from draftwright.builder import _coerce_model, build_drawing, detect_part_model
|
|
35
37
|
from draftwright.fits import fit_class
|
|
38
|
+
from draftwright.model import Feature
|
|
36
39
|
from draftwright.model import boss as _boss
|
|
37
40
|
from draftwright.model import envelope as _envelope
|
|
38
41
|
from draftwright.model import hole as _hole
|
|
39
42
|
from draftwright.model import pattern as _pattern
|
|
40
43
|
from draftwright.model import slot as _slot
|
|
41
44
|
from draftwright.model import step as _step
|
|
45
|
+
from draftwright.model.declare import _norm_axis, _read_cylinder, _require_positive
|
|
46
|
+
from draftwright.model.declare import read_bore_step as _read_bore_step
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
def _parse_scale(scale):
|
|
@@ -98,6 +103,34 @@ class _Hole:
|
|
|
98
103
|
)
|
|
99
104
|
return self
|
|
100
105
|
|
|
106
|
+
def cbore(
|
|
107
|
+
self, obj=None, *, diameter: float | None = None, depth: float | None = None
|
|
108
|
+
) -> _Hole:
|
|
109
|
+
"""A counterbore on this hole. ``.cbore(cbore_cyl)`` reads its ⌀ + depth off the
|
|
110
|
+
counterbore tool object (⌀ from the cylindrical face, depth from the part + tool along
|
|
111
|
+
the hole axis — no numbers restated), or pass explicit ``.cbore(diameter=…, depth=…)``.
|
|
112
|
+
An object supplies defaults; explicit kwargs override (#462)."""
|
|
113
|
+
return self._set(cbore=self._read_step("cbore", obj, diameter, depth))
|
|
114
|
+
|
|
115
|
+
def spotface(
|
|
116
|
+
self, obj=None, *, diameter: float | None = None, depth: float | None = None
|
|
117
|
+
) -> _Hole:
|
|
118
|
+
"""A spotface on this hole — same as :meth:`cbore` but a shallow facing (#462)."""
|
|
119
|
+
return self._set(spotface=self._read_step("spotface", obj, diameter, depth))
|
|
120
|
+
|
|
121
|
+
def _read_step(self, kind, obj, diameter, depth) -> tuple[float, float]:
|
|
122
|
+
if obj is not None:
|
|
123
|
+
rd, rdp = _read_bore_step(
|
|
124
|
+
self._sheet._part, obj, self._sheet._features[self._i].frame.axis
|
|
125
|
+
)
|
|
126
|
+
diameter = rd if diameter is None else diameter
|
|
127
|
+
depth = rdp if depth is None else depth
|
|
128
|
+
if diameter is None or depth is None:
|
|
129
|
+
raise ValueError(f"{kind} needs a tool object, or explicit diameter= and depth=")
|
|
130
|
+
# same positivity guard declare.hole() applies to cbore/spotface (#452/#462 review)
|
|
131
|
+
_require_positive(**{f"{kind} diameter": diameter, f"{kind} depth": depth})
|
|
132
|
+
return (diameter, depth)
|
|
133
|
+
|
|
101
134
|
def _set(self, **kw) -> _Hole:
|
|
102
135
|
self._sheet._features[self._i] = replace(self._sheet._features[self._i], **kw)
|
|
103
136
|
return self
|
|
@@ -168,6 +201,57 @@ class Sheet:
|
|
|
168
201
|
self._features.append(feature)
|
|
169
202
|
return self
|
|
170
203
|
|
|
204
|
+
def of(self, ref) -> _Hole | _Dim:
|
|
205
|
+
"""A decoratable handle onto an **existing** feature — the hybrid seam (#463).
|
|
206
|
+
|
|
207
|
+
*ref* is a feature index, a :class:`Feature` already in :attr:`features` (e.g. seeded by
|
|
208
|
+
:meth:`from_part`), or the build123d **object** you built (matched by ⌀ + in-plane
|
|
209
|
+
position). Returns the same fluent handle the declaration verbs do, so you can
|
|
210
|
+
``.fit(...)`` / ``.tolerance(...)`` — and, for a hole, ``.cbore(...)`` — a feature you
|
|
211
|
+
did not declare from scratch. Raises if the object matches no feature or is ambiguous."""
|
|
212
|
+
i = self._index_of(ref)
|
|
213
|
+
kind = self._features[i].kind
|
|
214
|
+
if kind == "hole":
|
|
215
|
+
return _Hole(self, i)
|
|
216
|
+
if kind in ("boss", "step"):
|
|
217
|
+
return _Dim(self, i, "diameter" if kind == "boss" else "length")
|
|
218
|
+
raise ValueError(f"of(): no aspect handle for a {kind!r} feature (holes / bosses / steps)")
|
|
219
|
+
|
|
220
|
+
def _index_of(self, ref) -> int:
|
|
221
|
+
if isinstance(ref, bool):
|
|
222
|
+
raise TypeError("of(): ref must be an index, a Feature, or a build123d object")
|
|
223
|
+
if isinstance(ref, int):
|
|
224
|
+
n = len(self._features)
|
|
225
|
+
if not -n <= ref < n:
|
|
226
|
+
raise IndexError(f"of(): feature index {ref} out of range (have {n})")
|
|
227
|
+
return ref % n
|
|
228
|
+
if isinstance(ref, Feature):
|
|
229
|
+
for i, f in enumerate(self._features):
|
|
230
|
+
if f is ref:
|
|
231
|
+
return i
|
|
232
|
+
raise ValueError("of(): that Feature is not in this sheet's features")
|
|
233
|
+
return self._match_object(ref)
|
|
234
|
+
|
|
235
|
+
def _match_object(self, obj) -> int:
|
|
236
|
+
"""The index of the declared feature the build123d *obj* refers to, by axis + ⌀ + the
|
|
237
|
+
two in-plane coordinates (the axial position is where they legitimately differ)."""
|
|
238
|
+
axis, dia, center = _read_cylinder(obj)
|
|
239
|
+
axis = _norm_axis(axis)
|
|
240
|
+
perp = [k for k in range(3) if k != "xyz".index(axis)]
|
|
241
|
+
matches = [
|
|
242
|
+
i
|
|
243
|
+
for i, f in enumerate(self._features)
|
|
244
|
+
if getattr(f, "diameter", None) is not None
|
|
245
|
+
and _norm_axis(f.frame.axis) == axis # same axis — a cross-hole must not match
|
|
246
|
+
and abs(f.diameter - dia) <= 0.2
|
|
247
|
+
and all(abs(f.frame.origin[k] - center[k]) <= 0.5 for k in perp)
|
|
248
|
+
]
|
|
249
|
+
if not matches:
|
|
250
|
+
raise ValueError("of(): no declared feature matches that object (⌀ + position)")
|
|
251
|
+
if len(matches) > 1:
|
|
252
|
+
raise ValueError("of(): the object matches several features — pass an index instead")
|
|
253
|
+
return matches[0]
|
|
254
|
+
|
|
171
255
|
def hole(self, obj=None, **kw) -> _Hole:
|
|
172
256
|
"""Declare a hole from the tool cylinder you subtracted (or explicit values).
|
|
173
257
|
Returns a fluent handle: ``.through()`` (default) / ``.depth(d)``."""
|