draftwright 0.1.13__tar.gz → 0.2.0__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.1.13 → draftwright-0.2.0}/CHANGELOG.md +68 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/PKG-INFO +21 -8
- {draftwright-0.1.13 → draftwright-0.2.0}/README.md +17 -5
- {draftwright-0.1.13 → draftwright-0.2.0}/pyproject.toml +5 -5
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/__init__.py +27 -5
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/_core.py +62 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/analysis.py +54 -47
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/annotate.py +1 -1
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/annotations/_common.py +1 -1
- draftwright-0.2.0/src/draftwright/annotations/from_model.py +1296 -0
- draftwright-0.2.0/src/draftwright/annotations/holes.py +700 -0
- draftwright-0.2.0/src/draftwright/annotations/orchestrator.py +379 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/annotations/sections.py +12 -26
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/builder.py +13 -88
- draftwright-0.2.0/src/draftwright/cli.py +172 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/drawing.py +132 -52
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/export.py +31 -36
- draftwright-0.2.0/src/draftwright/features.py +23 -0
- draftwright-0.2.0/src/draftwright/linting/__init__.py +37 -0
- draftwright-0.2.0/src/draftwright/linting/coverage.py +394 -0
- draftwright-0.2.0/src/draftwright/linting/issues.py +21 -0
- draftwright-0.2.0/src/draftwright/linting/structural.py +635 -0
- draftwright-0.2.0/src/draftwright/linting/suggest.py +100 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/make_drawing.py +3 -0
- draftwright-0.2.0/src/draftwright/model/__init__.py +72 -0
- draftwright-0.2.0/src/draftwright/model/detect.py +301 -0
- draftwright-0.2.0/src/draftwright/model/ir.py +337 -0
- draftwright-0.2.0/src/draftwright/model/planner.py +259 -0
- draftwright-0.2.0/src/draftwright/recognition/__init__.py +54 -0
- draftwright-0.2.0/src/draftwright/recognition/_features.py +1038 -0
- draftwright-0.2.0/src/draftwright/recognition/levels.py +51 -0
- draftwright-0.1.13/src/draftwright/features.py → draftwright-0.2.0/src/draftwright/recognition/slots.py +5 -6
- draftwright-0.2.0/src/draftwright/recognition/turned.py +165 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/registry.py +21 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/repair.py +4 -7
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/sheet.py +5 -6
- draftwright-0.2.0/tests/test_e2e_slice.py +72 -0
- draftwright-0.2.0/tests/test_lint_structural.py +419 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/test_linting.py +10 -6
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/test_make_drawing.py +531 -91
- draftwright-0.2.0/tests/test_part_model.py +326 -0
- draftwright-0.2.0/tests/test_recognition.py +847 -0
- draftwright-0.2.0/tests/test_render_seam.py +63 -0
- draftwright-0.2.0/tests/test_turned_steps.py +87 -0
- draftwright-0.1.13/src/draftwright/annotations/holes.py +0 -1063
- draftwright-0.1.13/src/draftwright/annotations/orchestrator.py +0 -551
- draftwright-0.1.13/src/draftwright/annotations/pmi.py +0 -371
- draftwright-0.1.13/src/draftwright/annotations/turned.py +0 -196
- draftwright-0.1.13/src/draftwright/linting.py +0 -268
- draftwright-0.1.13/tests/golden/ctc01.json +0 -335
- draftwright-0.1.13/tests/golden/cylinder.json +0 -176
- draftwright-0.1.13/tests/golden/plate.json +0 -156
- draftwright-0.1.13/tests/golden/stepped.json +0 -148
- draftwright-0.1.13/tests/test_golden.py +0 -309
- {draftwright-0.1.13 → draftwright-0.2.0}/.gitignore +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/LICENSE +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/skills/SKILL.md +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/layout.py +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/pmi.py +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/src/draftwright/projection.py +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/test_layout.py +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/test_pmi.py +0 -0
- {draftwright-0.1.13 → draftwright-0.2.0}/tests/test_registry.py +0 -0
|
@@ -2,6 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.2.0 — 2026-06-30
|
|
6
|
+
|
|
7
|
+
A major release. draftwright took ownership of feature recognition and linting
|
|
8
|
+
(ADR 0007) and was re-architected onto a feature-IR + dimensioning-planner
|
|
9
|
+
"compiler" (ADR 0008), gaining a Typer CLI and a portable, pure-Python PDF path
|
|
10
|
+
along the way. **Generated drawings change** versus v0.1.13: the new pipeline
|
|
11
|
+
dimensions parts more completely and consistently, so placement and the set of
|
|
12
|
+
dimensions can differ — output is not byte-compatible with prior releases.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Re-architected onto a feature-IR + dimensioning planner** (ADR 0008). The
|
|
17
|
+
engine is now a compiler: detectors build one feature inventory → a typed IR /
|
|
18
|
+
`PartModel` → a dimensioning planner emits render-intents → shared
|
|
19
|
+
layout/projection/export. Orientation and feature-kind are *data in the IR*,
|
|
20
|
+
not code branches, and every feature class (holes, patterns, counterbores,
|
|
21
|
+
slots, turned diameters/steps, centre marks, location dims, envelope/OD,
|
|
22
|
+
section A–A, PMI/GD&T) was migrated onto this one path; the old parallel
|
|
23
|
+
recognisers and placement passes were deleted as each was replaced. Net effect
|
|
24
|
+
for users: more complete, more consistent drawings — but output differs from
|
|
25
|
+
v0.1.13.
|
|
26
|
+
- **draftwright owns feature recognition and linting** (ADR 0007). The hole/
|
|
27
|
+
boss/cylinder/pattern recognisers, the slot/turned-step recognisers, and the
|
|
28
|
+
feature-coverage lint engine are vendored into `recognition/` and `linting.py`;
|
|
29
|
+
`build123d-drafting-helpers` is now purely the rendering library.
|
|
30
|
+
- **The CLI writes a PDF by default** and takes a `--format` selector (#288).
|
|
31
|
+
Previously `draftwright part.step` emitted SVG + DXF; it now emits a single PDF.
|
|
32
|
+
Choose outputs with `--format` (a comma-list, with an `all` alias) —
|
|
33
|
+
`--format pdf,dxf`, `--format svg`, `--format all`. The library API is
|
|
34
|
+
unchanged: `make_drawing(...)` / `Drawing.export()` still write SVG + DXF.
|
|
35
|
+
- **PDF export is now pure-Python and a core capability** (#288). The renderer
|
|
36
|
+
moved from `cairosvg` (which `dlopen`s the native `libcairo` system library —
|
|
37
|
+
absent on stock macOS/Windows, so PDF-by-default would have crashed there) to
|
|
38
|
+
`svglib` + `reportlab`, both pip-installable wheels with no system dependency.
|
|
39
|
+
PDF therefore works out of the box on every platform; output is visually
|
|
40
|
+
identical to the cairo renderer.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **A Typer command-line interface** (#289/#291): shell completion
|
|
45
|
+
(`--install-completion` / `--show-completion`), rich `--help`, and `--version`
|
|
46
|
+
(reports the installed distribution version). All existing flags are preserved.
|
|
47
|
+
- **`--format` output selector** (#288) — `pdf` (default), `svg`, `dxf`, or `all`,
|
|
48
|
+
as a comma-list.
|
|
49
|
+
- **Turned-part dimensioning**: axial step-length recognition and chains
|
|
50
|
+
(#188/#189/#231), step-diameter callouts, collapse of a uniform step
|
|
51
|
+
staircase to an "N× length" note (#290), and OD of a horizontal (X/Y) round
|
|
52
|
+
body dimensioned on the profile view (#292).
|
|
53
|
+
- **Slot recognition and dimensioning** converged onto the IR as `SlotFeature`
|
|
54
|
+
(#242), and **section A–A** is now triggered by the planner (#271).
|
|
55
|
+
- **A Contributor License Agreement** (#183).
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
- **The `--pdf` flag** (use `--format pdf`, the new default), the **`[pdf]`
|
|
60
|
+
install extra**, and the **`cairosvg` dependency** (#288).
|
|
61
|
+
- The byte-exact golden-output test harness (#190) — regression coverage rests on
|
|
62
|
+
the geometry-level and standards suites (ADR 0005 §3 / ADR 0007).
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- Locate **every** side-drilled (off-axis) hole, not just the first (#225/#286).
|
|
67
|
+
- Don't mis-detect a prismatic part with incidental cylinders as a turned part
|
|
68
|
+
(#293/#294); drop phantom zero-diameter turned steps (#279/#284); skip an
|
|
69
|
+
illegibly-dense step-length chain rather than cram it (#293/#296).
|
|
70
|
+
- Degraded hole-pattern callout consistency (#262/#274).
|
|
71
|
+
- Windows `python -m draftwright.make_drawing` CLI smoke / entrypoint (#181/#182).
|
|
72
|
+
|
|
5
73
|
## v0.1.13 — 2026-06-27
|
|
6
74
|
|
|
7
75
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -684,8 +684,9 @@ Requires-Dist: build123d-drafting-helpers>=0.13.0
|
|
|
684
684
|
Requires-Dist: build123d>=0.9.0
|
|
685
685
|
Requires-Dist: kiwisolver<2,>=1.4
|
|
686
686
|
Requires-Dist: numpy>=1.24
|
|
687
|
-
|
|
688
|
-
Requires-Dist:
|
|
687
|
+
Requires-Dist: reportlab>=4.0
|
|
688
|
+
Requires-Dist: svglib>=1.5
|
|
689
|
+
Requires-Dist: typer>=0.12
|
|
689
690
|
Description-Content-Type: text/markdown
|
|
690
691
|
|
|
691
692
|
# draftwright
|
|
@@ -714,6 +715,9 @@ Or from the command line:
|
|
|
714
715
|
|
|
715
716
|
```
|
|
716
717
|
draftwright my_part.step --title "Mounting Block" --number DWG-001
|
|
718
|
+
# writes my_part.pdf (the default); add --format to choose:
|
|
719
|
+
draftwright my_part.step --format pdf,dxf # PDF + DXF
|
|
720
|
+
draftwright my_part.step --format all # PDF + SVG + DXF
|
|
717
721
|
```
|
|
718
722
|
|
|
719
723
|
## What it produces
|
|
@@ -808,7 +812,10 @@ generation, the lint→repair loop, and the constraint-based layout engine).
|
|
|
808
812
|
|
|
809
813
|
## Architecture
|
|
810
814
|
|
|
811
|
-
draftwright is a
|
|
815
|
+
draftwright is structured as a **part-drawing compiler** (ADR 0008): feature
|
|
816
|
+
detectors → a `PartModel` IR (`Feature`s exposing `DimParameter`s) → a dimensioning
|
|
817
|
+
planner → renderers, feeding a shared layout/projection/export stack. It builds on
|
|
818
|
+
two libraries:
|
|
812
819
|
|
|
813
820
|
```
|
|
814
821
|
draftwright
|
|
@@ -816,7 +823,13 @@ draftwright
|
|
|
816
823
|
└── build123d — CAD kernel
|
|
817
824
|
```
|
|
818
825
|
|
|
819
|
-
|
|
820
|
-
(
|
|
821
|
-
|
|
822
|
-
|
|
826
|
+
It owns feature recognition (`recognition/`) and linting (`linting/`); annotation
|
|
827
|
+
primitives (`Dimension`, `Leader`, etc.) live in `build123d-drafting-helpers` and can
|
|
828
|
+
be used independently. The compiler is largely converged in production — turned
|
|
829
|
+
dims/lengths, centre marks, envelope, slots, holes (callouts/locations/grouping),
|
|
830
|
+
the section A–A trigger, the prismatic step-ladder + rotational furniture, and PMI/GD&T
|
|
831
|
+
are all on the IR — the migration is complete (one path; the orchestrator is
|
|
832
|
+
build → plan → render). See
|
|
833
|
+
[`docs/target-architecture.md`](docs/target-architecture.md) and
|
|
834
|
+
[`docs/adr/`](docs/adr/). The engine handles view layout (strip/zone model), scale
|
|
835
|
+
selection, annotation placement, and section rendering.
|
|
@@ -24,6 +24,9 @@ Or from the command line:
|
|
|
24
24
|
|
|
25
25
|
```
|
|
26
26
|
draftwright my_part.step --title "Mounting Block" --number DWG-001
|
|
27
|
+
# writes my_part.pdf (the default); add --format to choose:
|
|
28
|
+
draftwright my_part.step --format pdf,dxf # PDF + DXF
|
|
29
|
+
draftwright my_part.step --format all # PDF + SVG + DXF
|
|
27
30
|
```
|
|
28
31
|
|
|
29
32
|
## What it produces
|
|
@@ -118,7 +121,10 @@ generation, the lint→repair loop, and the constraint-based layout engine).
|
|
|
118
121
|
|
|
119
122
|
## Architecture
|
|
120
123
|
|
|
121
|
-
draftwright is a
|
|
124
|
+
draftwright is structured as a **part-drawing compiler** (ADR 0008): feature
|
|
125
|
+
detectors → a `PartModel` IR (`Feature`s exposing `DimParameter`s) → a dimensioning
|
|
126
|
+
planner → renderers, feeding a shared layout/projection/export stack. It builds on
|
|
127
|
+
two libraries:
|
|
122
128
|
|
|
123
129
|
```
|
|
124
130
|
draftwright
|
|
@@ -126,7 +132,13 @@ draftwright
|
|
|
126
132
|
└── build123d — CAD kernel
|
|
127
133
|
```
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
(
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
It owns feature recognition (`recognition/`) and linting (`linting/`); annotation
|
|
136
|
+
primitives (`Dimension`, `Leader`, etc.) live in `build123d-drafting-helpers` and can
|
|
137
|
+
be used independently. The compiler is largely converged in production — turned
|
|
138
|
+
dims/lengths, centre marks, envelope, slots, holes (callouts/locations/grouping),
|
|
139
|
+
the section A–A trigger, the prismatic step-ladder + rotational furniture, and PMI/GD&T
|
|
140
|
+
are all on the IR — the migration is complete (one path; the orchestrator is
|
|
141
|
+
build → plan → render). See
|
|
142
|
+
[`docs/target-architecture.md`](docs/target-architecture.md) and
|
|
143
|
+
[`docs/adr/`](docs/adr/). The engine handles view layout (strip/zone model), scale
|
|
144
|
+
selection, annotation placement, and section rendering.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "draftwright"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Automated technical-drawing generation for build123d"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -15,6 +15,9 @@ dependencies = [
|
|
|
15
15
|
"build123d-drafting-helpers>=0.13.0",
|
|
16
16
|
"kiwisolver>=1.4,<2",
|
|
17
17
|
"numpy>=1.24",
|
|
18
|
+
"reportlab>=4.0",
|
|
19
|
+
"svglib>=1.5",
|
|
20
|
+
"typer>=0.12",
|
|
18
21
|
]
|
|
19
22
|
keywords = ["build123d", "cad", "drafting", "engineering-drawing", "technical-drawing", "automation"]
|
|
20
23
|
classifiers = [
|
|
@@ -29,11 +32,8 @@ classifiers = [
|
|
|
29
32
|
"Programming Language :: Python :: 3.12",
|
|
30
33
|
]
|
|
31
34
|
|
|
32
|
-
[project.optional-dependencies]
|
|
33
|
-
pdf = ["cairosvg>=2.7"]
|
|
34
|
-
|
|
35
35
|
[project.scripts]
|
|
36
|
-
draftwright = "draftwright.
|
|
36
|
+
draftwright = "draftwright.cli:app"
|
|
37
37
|
|
|
38
38
|
[project.urls]
|
|
39
39
|
Homepage = "https://github.com/pzfreo/draftwright"
|
|
@@ -11,19 +11,41 @@ Requires build123d-drafting-helpers for annotation primitives.
|
|
|
11
11
|
Licensed under the GNU Affero General Public License v3 (AGPL-3.0).
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import sys as _sys
|
|
15
|
+
import types as _types
|
|
16
|
+
|
|
14
17
|
from draftwright.analysis import analyse_face_levels, dedup_diams
|
|
15
|
-
from draftwright.
|
|
16
|
-
Drawing,
|
|
17
|
-
FeatureInfo,
|
|
18
|
+
from draftwright.builder import (
|
|
18
19
|
build_drawing,
|
|
19
|
-
fix_svg_page_size,
|
|
20
20
|
generate_script,
|
|
21
|
-
lint_feature_coverage,
|
|
22
21
|
make_drawing,
|
|
23
22
|
)
|
|
23
|
+
from draftwright.drawing import Drawing, FeatureInfo
|
|
24
|
+
from draftwright.export import fix_svg_page_size
|
|
25
|
+
from draftwright.linting import lint_feature_coverage
|
|
24
26
|
from draftwright.pmi import PmiRecord, extract_pmi
|
|
25
27
|
from draftwright.sheet import choose_scale
|
|
26
28
|
|
|
29
|
+
_make_drawing_function = make_drawing
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _DraftwrightModule(_types.ModuleType):
|
|
33
|
+
def __getattribute__(self, name):
|
|
34
|
+
if name == "make_drawing":
|
|
35
|
+
namespace = _types.ModuleType.__getattribute__(self, "__dict__")
|
|
36
|
+
value = namespace.get(name)
|
|
37
|
+
if (
|
|
38
|
+
isinstance(value, _types.ModuleType)
|
|
39
|
+
and value.__name__ == "draftwright.make_drawing"
|
|
40
|
+
):
|
|
41
|
+
public = namespace["_make_drawing_function"]
|
|
42
|
+
_types.ModuleType.__setattr__(self, name, public)
|
|
43
|
+
return public
|
|
44
|
+
return _types.ModuleType.__getattribute__(self, name)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_sys.modules[__name__].__class__ = _DraftwrightModule
|
|
48
|
+
|
|
27
49
|
__all__ = [
|
|
28
50
|
"Drawing",
|
|
29
51
|
"FeatureInfo",
|
|
@@ -16,6 +16,10 @@ import re
|
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from types import SimpleNamespace
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from draftwright.recognition import TurnedProfile
|
|
19
23
|
|
|
20
24
|
from build123d import Align, BoundBox, Location, Mode, Shape, Text
|
|
21
25
|
from build123d_drafting.helpers import (
|
|
@@ -36,6 +40,38 @@ _MARGIN = 10.0
|
|
|
36
40
|
|
|
37
41
|
_TB_CLEAR = _MARGIN + 1.0 # title-block inset: one extra mm over _MARGIN for clearance
|
|
38
42
|
|
|
43
|
+
# The orthographic view a feature is dimensioned end-on in — the view normal to its
|
|
44
|
+
# axis. Single source of truth shared by the planner (view choice) and the lint
|
|
45
|
+
# coverage checks (where to look for a feature's dims). Orientation is data.
|
|
46
|
+
_END_ON = {"x": "side", "y": "front", "z": "plan"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _xyz(loc) -> tuple[float, float, float]:
|
|
50
|
+
"""A build123d ``Vector`` (has ``.X/.Y/.Z``) or an ``(x, y, z)`` sequence → an
|
|
51
|
+
``(x, y, z)`` float tuple. Shared by the detectors and the lint coverage checks
|
|
52
|
+
so the Vector-unpacking idiom lives in one place."""
|
|
53
|
+
if hasattr(loc, "X"):
|
|
54
|
+
return (loc.X, loc.Y, loc.Z)
|
|
55
|
+
x, y, z = loc
|
|
56
|
+
return (float(x), float(y), float(z))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class HoleRef:
|
|
61
|
+
"""A position-keyed reference to a hole — the IR-typed value the cover / hole-table
|
|
62
|
+
bookkeeping matches on, so the shared escalation never needs a recogniser ``Hole``
|
|
63
|
+
object (ADR 0008 Amendment 6). Built from any location via :meth:`of` (rounded, so
|
|
64
|
+
two references at the same position compare equal)."""
|
|
65
|
+
|
|
66
|
+
x: float
|
|
67
|
+
y: float
|
|
68
|
+
z: float
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def of(cls, loc) -> HoleRef:
|
|
72
|
+
x, y, z = _xyz(loc)
|
|
73
|
+
return cls(round(x, 3), round(y, 3), round(z, 3))
|
|
74
|
+
|
|
39
75
|
|
|
40
76
|
_FONT_SIZE = 3.0 # annotation text height (page-mm); the draft preset is built with this
|
|
41
77
|
|
|
@@ -277,6 +313,29 @@ def _legible_steps(step_zs, bb_min_z, scale):
|
|
|
277
313
|
return kept, n_too_close
|
|
278
314
|
|
|
279
315
|
|
|
316
|
+
def _legible_locations(positions, scale):
|
|
317
|
+
"""Axis positions far enough apart on the page to dimension legibly.
|
|
318
|
+
|
|
319
|
+
Given world-coordinate *positions* along one axis, keep a position only if it
|
|
320
|
+
is at least ``_MIN_LOC_SEP_MM`` page-mm from the previously kept one;
|
|
321
|
+
consecutive holes closer than that produce baseline witness lines that read
|
|
322
|
+
as a single busy cluster (#43). Returns ``(kept, n_too_close)``: the positions
|
|
323
|
+
to dimension and the count dropped for spacing (the caller surfaces these via
|
|
324
|
+
``location_ref_dropped`` lint; the full-fidelity answer is a detail view, #42).
|
|
325
|
+
Mirrors :func:`_legible_steps` for hole locations.
|
|
326
|
+
"""
|
|
327
|
+
kept: list[float] = []
|
|
328
|
+
n_too_close = 0
|
|
329
|
+
last = None
|
|
330
|
+
for p in sorted(positions):
|
|
331
|
+
if last is not None and (p - last) * scale < _MIN_LOC_SEP_MM:
|
|
332
|
+
n_too_close += 1
|
|
333
|
+
continue
|
|
334
|
+
kept.append(p)
|
|
335
|
+
last = p
|
|
336
|
+
return kept, n_too_close
|
|
337
|
+
|
|
338
|
+
|
|
280
339
|
def _largest_empty_rect(drawable, obstacles):
|
|
281
340
|
"""Largest axis-aligned empty rectangle in *drawable* avoiding *obstacles*.
|
|
282
341
|
|
|
@@ -401,12 +460,15 @@ class Analysis:
|
|
|
401
460
|
bbox_max: float
|
|
402
461
|
holes: list
|
|
403
462
|
patterns: list
|
|
463
|
+
bosses: list # external bosses (find_bosses), detected once — the one inventory (#244)
|
|
404
464
|
slots: list
|
|
405
465
|
z_diams: list[float]
|
|
406
466
|
cross_diams: list[float]
|
|
407
467
|
cyls: tuple[list, list]
|
|
468
|
+
prof: TurnedProfile | None # turned step profile (find_turned_steps), detected once
|
|
408
469
|
od_diam: float | None
|
|
409
470
|
is_rotational: bool
|
|
471
|
+
od_axis: str # rotation/turning axis of a rotational part ("z" default; "x"/"y" #222)
|
|
410
472
|
step_zs: list[float]
|
|
411
473
|
sv_right: float
|
|
412
474
|
iso_right_limit: float
|
|
@@ -14,17 +14,7 @@ import logging
|
|
|
14
14
|
import math
|
|
15
15
|
|
|
16
16
|
from build123d import Compound, Shape
|
|
17
|
-
from build123d_drafting.features import (
|
|
18
|
-
analyse_cylinders,
|
|
19
|
-
find_hole_patterns,
|
|
20
|
-
find_holes,
|
|
21
|
-
full_cylinders,
|
|
22
|
-
)
|
|
23
17
|
from build123d_drafting.helpers import draft_preset
|
|
24
|
-
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
|
25
|
-
from OCP.BRepGProp import BRepGProp
|
|
26
|
-
from OCP.GeomAbs import GeomAbs_Plane
|
|
27
|
-
from OCP.GProp import GProp_GProps
|
|
28
18
|
from OCP.IFSelect import IFSelect_ReturnStatus
|
|
29
19
|
from OCP.STEPControl import STEPControl_Reader
|
|
30
20
|
|
|
@@ -37,7 +27,16 @@ from draftwright._core import (
|
|
|
37
27
|
_legible_steps,
|
|
38
28
|
_Projector,
|
|
39
29
|
)
|
|
40
|
-
from draftwright.
|
|
30
|
+
from draftwright.recognition import (
|
|
31
|
+
analyse_cylinders,
|
|
32
|
+
analyse_face_levels,
|
|
33
|
+
find_bosses,
|
|
34
|
+
find_hole_patterns,
|
|
35
|
+
find_holes,
|
|
36
|
+
find_slots,
|
|
37
|
+
find_turned_steps,
|
|
38
|
+
full_cylinders,
|
|
39
|
+
)
|
|
41
40
|
from draftwright.sheet import (
|
|
42
41
|
_build_zones,
|
|
43
42
|
_layout_geometry,
|
|
@@ -115,40 +114,6 @@ def _is_rotational(x_size, y_size, od_diam, od_axis_offset) -> bool:
|
|
|
115
114
|
# --- lint scoring (see Drawing.lint_summary) -------------------------------
|
|
116
115
|
|
|
117
116
|
|
|
118
|
-
def analyse_face_levels(part, tol: float = 0.5, min_area_frac: float = 0.0) -> list:
|
|
119
|
-
"""Return sorted unique Z-coords of horizontal (normal≈±Z) planar faces.
|
|
120
|
-
|
|
121
|
-
Uses tol-bucket deduplication but returns the actual face Z, not the rounded
|
|
122
|
-
bucket centre, so dimension labels match the true geometry.
|
|
123
|
-
|
|
124
|
-
When *min_area_frac* > 0, a Z level is kept only if the total area of its
|
|
125
|
-
horizontal faces is at least ``min_area_frac × (x_size × y_size)`` (the
|
|
126
|
-
part's plan footprint). This drops sub-feature faces — e.g. fragments of
|
|
127
|
-
engraved text/numbers — that are not real steps and would otherwise be
|
|
128
|
-
dimensioned as phantom shoulders (staircase.step review).
|
|
129
|
-
"""
|
|
130
|
-
buckets: dict = {} # bucket key -> representative z
|
|
131
|
-
areas: dict = {} # bucket key -> total horizontal-face area
|
|
132
|
-
for face in part.faces():
|
|
133
|
-
surf = BRepAdaptor_Surface(face.wrapped)
|
|
134
|
-
if surf.GetType() == GeomAbs_Plane:
|
|
135
|
-
ax = surf.Plane().Axis().Direction()
|
|
136
|
-
if abs(ax.Z()) > 0.99:
|
|
137
|
-
z = surf.Plane().Location().Z()
|
|
138
|
-
key = round(z / tol) * tol
|
|
139
|
-
buckets.setdefault(key, z)
|
|
140
|
-
if min_area_frac > 0.0:
|
|
141
|
-
props = GProp_GProps()
|
|
142
|
-
BRepGProp.SurfaceProperties_s(face.wrapped, props)
|
|
143
|
-
areas[key] = areas.get(key, 0.0) + props.Mass()
|
|
144
|
-
if min_area_frac > 0.0:
|
|
145
|
-
bb = part.bounding_box()
|
|
146
|
-
footprint = (bb.max.X - bb.min.X) * (bb.max.Y - bb.min.Y)
|
|
147
|
-
threshold = min_area_frac * footprint
|
|
148
|
-
return sorted(z for key, z in buckets.items() if areas.get(key, 0.0) >= threshold)
|
|
149
|
-
return sorted(buckets.values())
|
|
150
|
-
|
|
151
|
-
|
|
152
117
|
# ---------------------------------------------------------------------------
|
|
153
118
|
# Strip / zone layout model
|
|
154
119
|
# ---------------------------------------------------------------------------
|
|
@@ -288,11 +253,49 @@ def _analyse(
|
|
|
288
253
|
math.hypot(od_cyl["axis_xyz"][0] - cx, od_cyl["axis_xyz"][1] - cy) if od_cyl else 0.0
|
|
289
254
|
)
|
|
290
255
|
is_rotational = _is_rotational(x_size, y_size, od_diam, od_axis_offset)
|
|
256
|
+
od_axis = "z"
|
|
257
|
+
if not is_rotational:
|
|
258
|
+
# Fallback: a *horizontal* (X/Y) round body — its OD is a cross-axis cylinder
|
|
259
|
+
# filling the square envelope perpendicular to that axis (#222). The Z check
|
|
260
|
+
# above is untouched, so vertical parts classify exactly as before; this only
|
|
261
|
+
# fires when Z-rotational fails and a cross-axis round body is present.
|
|
262
|
+
sizes = {"x": x_size, "y": y_size, "z": z_size}
|
|
263
|
+
ctr = {"x": cx, "y": cy, "z": cz}
|
|
264
|
+
cross_full = full_cylinders(cross_cyls)
|
|
265
|
+
for ax in ("x", "y"):
|
|
266
|
+
ext = [c for c in cross_full if c.get("axis") == ax and c["external"]]
|
|
267
|
+
# #222 targets a *single-OD* round body. A stepped shaft (multiple distinct
|
|
268
|
+
# cross diameters) stays on the turned-diameter path, not the OD furniture.
|
|
269
|
+
if len({round(c["diameter"], 1) for c in ext}) != 1:
|
|
270
|
+
continue
|
|
271
|
+
oc = max(ext, key=lambda c: c["diameter"], default=None)
|
|
272
|
+
if oc is None:
|
|
273
|
+
continue
|
|
274
|
+
p0, p1 = (a for a in "xyz" if a != ax)
|
|
275
|
+
off = math.hypot(
|
|
276
|
+
oc["axis_xyz"]["xyz".index(p0)] - ctr[p0],
|
|
277
|
+
oc["axis_xyz"]["xyz".index(p1)] - ctr[p1],
|
|
278
|
+
)
|
|
279
|
+
if _is_rotational(sizes[p0], sizes[p1], oc["diameter"], off):
|
|
280
|
+
od_diam, od_axis_offset, od_axis, is_rotational = oc["diameter"], off, ax, True
|
|
281
|
+
break
|
|
291
282
|
if z_diams and not is_rotational:
|
|
292
283
|
_log.info("Part classified prismatic; skipping OD/centreline/bore annotations")
|
|
293
284
|
|
|
294
|
-
|
|
295
|
-
|
|
285
|
+
# Step Z-levels feed both the step-height ladder and the page-sizing step
|
|
286
|
+
# count. For a vertical (Z-axis) turned part, take them from the unified
|
|
287
|
+
# turned-step model (ADR 0008 step 1): it filters shoulders by the OD
|
|
288
|
+
# silhouette, so an internal feature face — a blind bore's flat floor — is
|
|
289
|
+
# never read as a phantom OD shoulder (the area-only filter in
|
|
290
|
+
# analyse_face_levels admitted it). Prismatic and other parts keep the
|
|
291
|
+
# general face-level scan, which find_turned_steps cannot replace (no
|
|
292
|
+
# cylinders → no profile).
|
|
293
|
+
_turned = find_turned_steps(part)
|
|
294
|
+
if _turned is not None and _turned.axis == "z":
|
|
295
|
+
step_zs = [z for z in _turned.shoulders if bb.min.Z + 0.6 < z < bb.max.Z - 0.6]
|
|
296
|
+
else:
|
|
297
|
+
face_zs = analyse_face_levels(part, min_area_frac=_STEP_MIN_AREA_FRAC)
|
|
298
|
+
step_zs = [z for z in face_zs if z > bb.min.Z + 0.6 and z < bb.max.Z - 0.6]
|
|
296
299
|
|
|
297
300
|
# Pass 1 (two-pass layout, #131): measure annotation strip depths before
|
|
298
301
|
# view positions are fixed. font_size=3.0 is a fixed page-mm constant so
|
|
@@ -305,6 +308,7 @@ def _analyse(
|
|
|
305
308
|
_pad_around_text = _draft_est.pad_around_text
|
|
306
309
|
holes = find_holes(part, cyls=(z_cyls, cross_cyls))
|
|
307
310
|
patterns = find_hole_patterns(holes)
|
|
311
|
+
bosses = find_bosses(part, cyls=(z_cyls, cross_cyls)) # detect once — the one inventory (#264)
|
|
308
312
|
slots = find_slots(part)
|
|
309
313
|
|
|
310
314
|
# Choose scale/page, iterating so the reserved step corridor matches the
|
|
@@ -420,12 +424,15 @@ def _analyse(
|
|
|
420
424
|
bbox_max=bbox_max,
|
|
421
425
|
holes=holes,
|
|
422
426
|
patterns=patterns,
|
|
427
|
+
bosses=bosses,
|
|
423
428
|
slots=slots,
|
|
424
429
|
z_diams=z_diams,
|
|
425
430
|
cross_diams=cross_diams,
|
|
426
431
|
cyls=(z_cyls, cross_cyls),
|
|
432
|
+
prof=_turned,
|
|
427
433
|
od_diam=od_diam,
|
|
428
434
|
is_rotational=is_rotational,
|
|
435
|
+
od_axis=od_axis,
|
|
429
436
|
step_zs=step_zs,
|
|
430
437
|
sv_right=sv_right,
|
|
431
438
|
iso_right_limit=iso_right_limit,
|
|
@@ -6,8 +6,8 @@ the orchestrator entry point and the two helpers still referenced by name elsewh
|
|
|
6
6
|
so `from draftwright.annotate import _auto_annotate` keeps working.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from draftwright.annotations.from_model import _detect_step_repeat # noqa: F401
|
|
9
10
|
from draftwright.annotations.orchestrator import ( # noqa: F401
|
|
10
11
|
_auto_annotate,
|
|
11
|
-
_detect_step_repeat,
|
|
12
12
|
_wrap_rows,
|
|
13
13
|
)
|
|
@@ -29,7 +29,7 @@ def _occupied_boxes(dwg):
|
|
|
29
29
|
hatch. Bare centrelines/leaders are excluded — those legitimately cross a
|
|
30
30
|
dimension and lint does not flag them."""
|
|
31
31
|
boxes = []
|
|
32
|
-
for name, o in dwg.
|
|
32
|
+
for name, o in dwg.iter_annotations():
|
|
33
33
|
if getattr(o, "label_bbox", None) is None and name != "section_hatch":
|
|
34
34
|
continue
|
|
35
35
|
bb = _anno_box(o)
|