draftwright 0.2.5__tar.gz → 0.2.7__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.5 → draftwright-0.2.7}/CHANGELOG.md +88 -7
- {draftwright-0.2.5 → draftwright-0.2.7}/PKG-INFO +1 -1
- {draftwright-0.2.5 → draftwright-0.2.7}/pyproject.toml +1 -1
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/__init__.py +3 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/_core.py +25 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/analysis.py +21 -16
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/annotations/from_model.py +98 -46
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/annotations/holes.py +8 -5
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/annotations/orchestrator.py +53 -4
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/builder.py +84 -7
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/cli.py +27 -11
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/drawing.py +12 -1
- draftwright-0.2.7/src/draftwright/fits.py +154 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/model/__init__.py +7 -0
- draftwright-0.2.7/src/draftwright/model/declare.py +515 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/model/detect.py +16 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/model/ir.py +13 -1
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/model/planner.py +8 -1
- draftwright-0.2.7/src/draftwright/sheet_dsl.py +328 -0
- draftwright-0.2.7/src/draftwright/sheet_emit.py +191 -0
- draftwright-0.2.7/tests/test_declare.py +566 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_e2e_standards.py +106 -0
- draftwright-0.2.7/tests/test_fits.py +225 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_make_drawing.py +48 -0
- draftwright-0.2.7/tests/test_object_aspects.py +89 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_render_seam.py +4 -3
- draftwright-0.2.7/tests/test_sheet_emit.py +199 -0
- draftwright-0.2.7/tests/test_sheet_of.py +81 -0
- draftwright-0.2.7/tests/test_tolerances.py +218 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/.gitignore +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/LICENSE +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/README.md +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/skills/SKILL.md +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/annotations/_common.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/annotations/sections.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/export.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/features.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/bracket.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/dshape.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/side_drilled.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_layout.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_linting.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_part_model.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_pmi.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_recognition.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_registry.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_strip_layout.py +0 -0
- {draftwright-0.2.5 → draftwright-0.2.7}/tests/test_turned_steps.py +0 -0
|
@@ -1,15 +1,96 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## v0.2.7 — 2026-07-05
|
|
4
|
+
|
|
5
|
+
**The three authoring modes (ADR 0011 Amendment 1).** Naming how you drive draftwright —
|
|
6
|
+
*just do it* / *auto then tweak* / *generate an editable script* — and filling in the third:
|
|
7
|
+
a declarative `Sheet`-DSL generator, and number-free authoring against the build123d objects
|
|
8
|
+
you built.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`draftwright part.step --script --style sheet`** — generate an **editable declarative
|
|
13
|
+
`Sheet` script** from a detected part: one commentable line per feature (hole / diameter /
|
|
14
|
+
step / slot / pattern / envelope), views noted, the auto-section flagged. Comment a line out
|
|
15
|
+
to drop that feature; the script re-runs to the drawing. The existing `--script` (imperative
|
|
16
|
+
edit-verb reconstruction) stays the default (#461).
|
|
17
|
+
- **Object-reading `.cbore(tool)` / `.spotface(tool)`** on a declared hole — read the
|
|
18
|
+
counterbore/spotface's ⌀ **and** depth off the tool object + part (depth measured from the
|
|
19
|
+
local open face, so a rib/wall elsewhere doesn't skew it). A `Sheet`-declared drawing can now
|
|
20
|
+
carry **zero magic numbers** and track the geometry parametrically (#462).
|
|
21
|
+
- **`Sheet.of(feature)`** — a fluent handle onto an **existing / detection-seeded** feature (by
|
|
22
|
+
the build123d object, an index, or the `Feature` itself), so you can `.fit(...)` /
|
|
23
|
+
`.tolerance(...)` a feature from `Sheet.from_part()` without re-declaring it (#463).
|
|
24
|
+
|
|
25
|
+
### Docs
|
|
26
|
+
|
|
27
|
+
- **ADR 0011 Amendment 1** — the three authoring modes and the mode-3 generation surface;
|
|
28
|
+
records the decision to emit **honest detected numbers** for a detected part rather than
|
|
29
|
+
fabricate a build123d reconstruction that misrepresents the geometry (#464).
|
|
30
|
+
|
|
31
|
+
## v0.2.6 — 2026-07-05
|
|
32
|
+
|
|
33
|
+
**ADR 0011 — the IR as a public input: declare features, don't only detect them.**
|
|
34
|
+
Detection stays the default, but the caller can now *supply* the feature model, so a
|
|
35
|
+
part you built parametrically reads as its own drawing and misdetection becomes
|
|
36
|
+
recoverable by construction. Plus the first Phase-2 aspect layer (tolerances + fits).
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`build_drawing(part, model=…)`** accepts a caller-supplied `PartModel` (or a
|
|
41
|
+
`Sequence[Feature]`); when given, **detection is skipped** and the auto-pass dimensions
|
|
42
|
+
the declared features. Detection and declaration are two producers of the same IR (#447,
|
|
43
|
+
ADR 0011 Phase 0).
|
|
44
|
+
- **Object → feature constructors** `draftwright.model.hole` / `boss` / `step` / `slot` /
|
|
45
|
+
`pattern` / `envelope` — read a feature's geometry off the build123d object you built (⌀
|
|
46
|
+
from the cylindrical face, axis/location from the bbox), or supply explicit values (#447).
|
|
47
|
+
- **The fluent `Sheet` façade** (`draftwright.Sheet`) — reference the objects you built,
|
|
48
|
+
declare their drawing aspects, `.build()` / `.export()`. `Sheet.from_part()` seeds the
|
|
49
|
+
hybrid override mode from detection (#447, Phase 1).
|
|
50
|
+
- **Toleranced dimensions** — a `±` / limit tolerance on a diameter, step, or hole bore,
|
|
51
|
+
rendered on both the linear and ⌀-callout paths (`Sheet.tolerance(...)`, or a
|
|
52
|
+
`decorations=` side-layer) (#28, Phase 2 P2a).
|
|
53
|
+
- **Fit-class deviation** — `Sheet.fit("H7")` resolves an ISO 286 fit class to its limit
|
|
54
|
+
deviation for the feature's nominal ⌀, rendered as the class code (`ø20 H7`, default) or
|
|
55
|
+
the signed deviations (`show="deviation"` → `ø20 +0.021/0`). Common classes; fails loud
|
|
56
|
+
outside its table (#29, Phase 2 P2a.2).
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **Object constructors honour explicit overrides** — a passed object supplies *defaults*;
|
|
61
|
+
each explicit keyword overrides that field independently, and invalid public input fails
|
|
62
|
+
at declaration with a clear `ValueError` (#451, #452).
|
|
63
|
+
- **A declared hole/pattern renders at its declared position** even where detection missed
|
|
64
|
+
it — the callout membership is sourced from the declared model, not only detection (#448).
|
|
65
|
+
- **`Sheet.model()` / `Sheet.from_part()`** no longer build a full drawing just to return
|
|
66
|
+
the IR — feature inspection/seeding is now a cheap, no-render path (#453).
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- A narrow diameter band hidden under a larger OD silhouette is no longer silently
|
|
71
|
+
undimensioned (the two feature inventories agreed) (#298).
|
|
72
|
+
|
|
73
|
+
## v0.2.5 — 2026-07-04
|
|
74
|
+
|
|
75
|
+
**The editable write API and record-then-finalize.** A detected drawing became an editable
|
|
76
|
+
object, and a generated `--script` became a runnable reconstruction that reaches auto-pass
|
|
77
|
+
quality.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- **`dwg.model()`** exposes the detected `PartModel` as a read surface, plus feature-
|
|
82
|
+
referenced add verbs `dimension()` / `callout()` / `locate()` / `furniture()` /
|
|
83
|
+
`section()` and `drop(feature)`, with a machine-checked completeness audit (#400).
|
|
84
|
+
- **Record-then-finalize** (#426): the verbs record intents in deferred mode and
|
|
85
|
+
`dwg.finalize()` (auto-run by export) drains them through the auto-pass's own solvers, so
|
|
86
|
+
a reconstruction reaches auto-pass quality; `--script` now emits a runnable detect-only
|
|
87
|
+
reconstruction.
|
|
4
88
|
|
|
5
89
|
### Changed
|
|
6
90
|
|
|
7
|
-
- **Dimension-line spacing now follows ISO 129-1 / ASME Y14.5 convention** (#347)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
clear gap 4 → 2.5 mm). The inter-view corridor widens in step (`_DIM_PAD`
|
|
11
|
-
18 → 20 mm) so the wider first-line gap does not crowd the between-view
|
|
12
|
-
dimensions. Re-drifts the layout of every drawing.
|
|
91
|
+
- **Dimension-line spacing now follows ISO 129-1 / ASME Y14.5 convention** (#347): a wider
|
|
92
|
+
first-line gap (8 → 10 mm) and tighter, uniform parallel stacking (between-line clear gap
|
|
93
|
+
4 → 2.5 mm), with the inter-view corridor widened in step. Re-drifts every drawing.
|
|
13
94
|
|
|
14
95
|
## v0.2.4 — 2026-07-03
|
|
15
96
|
|
|
@@ -30,6 +30,7 @@ _LAZY = {
|
|
|
30
30
|
"make_drawing": "draftwright.builder",
|
|
31
31
|
"Drawing": "draftwright.drawing",
|
|
32
32
|
"FeatureInfo": "draftwright.drawing",
|
|
33
|
+
"Sheet": "draftwright.sheet_dsl",
|
|
33
34
|
"fix_svg_page_size": "draftwright.export",
|
|
34
35
|
"lint_feature_coverage": "draftwright.linting",
|
|
35
36
|
"PmiRecord": "draftwright.pmi",
|
|
@@ -78,6 +79,7 @@ if TYPE_CHECKING: # static analysers / IDEs — no runtime import, no kernel co
|
|
|
78
79
|
from draftwright.linting import lint_feature_coverage
|
|
79
80
|
from draftwright.pmi import PmiRecord, extract_pmi
|
|
80
81
|
from draftwright.sheet import choose_scale
|
|
82
|
+
from draftwright.sheet_dsl import Sheet
|
|
81
83
|
|
|
82
84
|
|
|
83
85
|
def __dir__():
|
|
@@ -91,6 +93,7 @@ __all__ = [
|
|
|
91
93
|
"Drawing",
|
|
92
94
|
"FeatureInfo",
|
|
93
95
|
"PmiRecord",
|
|
96
|
+
"Sheet",
|
|
94
97
|
"analyse_face_levels",
|
|
95
98
|
"build_drawing",
|
|
96
99
|
"choose_scale",
|
|
@@ -30,6 +30,7 @@ from build123d_drafting.helpers import (
|
|
|
30
30
|
format_drawing_scale,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
+
from draftwright.fits import FitClass
|
|
33
34
|
from draftwright.fonts import PLEX_MONO, PLEX_SANS_CONDENSED
|
|
34
35
|
from draftwright.layout import _greedy_strip_1d, _solve_strip_1d
|
|
35
36
|
|
|
@@ -86,6 +87,30 @@ def _fmt(v: float) -> str:
|
|
|
86
87
|
return str(r) if abs(v - r) < 1e-6 else f"{v:.1f}"
|
|
87
88
|
|
|
88
89
|
|
|
90
|
+
def _tol_suffix(tolerance, draft) -> str:
|
|
91
|
+
"""The ``±`` / limit tolerance suffix to append to a **callout** label (a ø leader
|
|
92
|
+
or a hole callout), matching byte-for-byte what ``Dimension(tolerance=…)`` renders
|
|
93
|
+
on a linear dim (helpers ``_format_label``): a symmetric ``float`` → ``" ±t"``; an
|
|
94
|
+
``(lower, upper)`` pair → ``" +upper -lower"`` — all rounded to the draft precision.
|
|
95
|
+
|
|
96
|
+
draftwright owns this suffix ONLY because the pinned helpers' ``Leader`` /
|
|
97
|
+
``HoleCallout`` take no ``tolerance=`` yet, so we bake it into the label string
|
|
98
|
+
ourselves. Delete this once helpers grows a first-class tolerance parameter for
|
|
99
|
+
those two (extraction tracked as #449) — ``Dimension`` formats its own (#28 / P2a).
|
|
100
|
+
|
|
101
|
+
A resolved fit (:class:`~draftwright.fits.FitClass`, P2a.2) renders its own class-code
|
|
102
|
+
or deviation suffix — it rides the same ``tolerance`` field as an aspect marker."""
|
|
103
|
+
if tolerance is None:
|
|
104
|
+
return ""
|
|
105
|
+
if isinstance(tolerance, FitClass):
|
|
106
|
+
return tolerance.suffix()
|
|
107
|
+
prec = draft.decimal_precision
|
|
108
|
+
if isinstance(tolerance, (int, float)):
|
|
109
|
+
return f" ±{round(tolerance, prec):.{prec}f}"
|
|
110
|
+
lo, hi = tolerance
|
|
111
|
+
return f" +{round(hi, prec):.{prec}f} -{round(lo, prec):.{prec}f}"
|
|
112
|
+
|
|
113
|
+
|
|
89
114
|
def _tag_sequence(n):
|
|
90
115
|
"""``A, B, …, Z, AA, AB, …`` — deterministic hole-table tags for *n* rows."""
|
|
91
116
|
tags = []
|
|
@@ -179,6 +179,26 @@ _STEP_MIN_AREA_FRAC = 0.01
|
|
|
179
179
|
# ---------------------------------------------------------------------------
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
def _solids_body(part, src: str = "part"):
|
|
183
|
+
"""The part reduced to just its solids — the geometry the drawing is *of*.
|
|
184
|
+
|
|
185
|
+
AP242 STEP files (and hand-built Compounds) can carry non-solid geometry beside
|
|
186
|
+
the solid — PMI presentation wires, leader curves, construction edges/sketches —
|
|
187
|
+
which, left in, draw as phantom rectangles in every view and inflate the bounding
|
|
188
|
+
box, corrupting the scale choice and the envelope dimensions. Shared by
|
|
189
|
+
:func:`_analyse` and :meth:`draftwright.Sheet.model` (#453) so the model a caller
|
|
190
|
+
*inspects* is wrapped from the exact same body the engine *draws*."""
|
|
191
|
+
solids = part.solids()
|
|
192
|
+
if not solids:
|
|
193
|
+
return part
|
|
194
|
+
body = solids[0] if len(solids) == 1 else Compound(children=list(solids))
|
|
195
|
+
if body.bounding_box().size != part.bounding_box().size or len(part.edges()) != len(
|
|
196
|
+
body.edges()
|
|
197
|
+
):
|
|
198
|
+
_log.info("Dropping non-solid geometry from %s (PMI presentation data)", src)
|
|
199
|
+
return body
|
|
200
|
+
|
|
201
|
+
|
|
182
202
|
def _analyse(
|
|
183
203
|
step_file, title, number, tolerance, drawn_by, out, scale=None, page=None, pmi="off"
|
|
184
204
|
) -> Analysis:
|
|
@@ -192,22 +212,7 @@ def _analyse(
|
|
|
192
212
|
else:
|
|
193
213
|
part = _import_step(step_file)
|
|
194
214
|
src = str(step_file)
|
|
195
|
-
|
|
196
|
-
# border wires, leader curves) beside the solid; left in, it draws as
|
|
197
|
-
# phantom rectangles in every view and inflates the bounding box —
|
|
198
|
-
# corrupting the scale choice and the envelope dimensions. The drawing
|
|
199
|
-
# is of the solids.
|
|
200
|
-
solids = part.solids()
|
|
201
|
-
if solids:
|
|
202
|
-
body = solids[0] if len(solids) == 1 else Compound(children=list(solids))
|
|
203
|
-
if body.bounding_box().size != part.bounding_box().size or len(part.edges()) != len(
|
|
204
|
-
body.edges()
|
|
205
|
-
):
|
|
206
|
-
_log.info(
|
|
207
|
-
"Dropping non-solid geometry from %s (PMI presentation data)",
|
|
208
|
-
src,
|
|
209
|
-
)
|
|
210
|
-
part = body
|
|
215
|
+
part = _solids_body(part, src)
|
|
211
216
|
|
|
212
217
|
# Semantic PMI extraction (AP242 only; separate read-only pass).
|
|
213
218
|
pmi_records: list = []
|
|
@@ -41,6 +41,7 @@ from draftwright._core import (
|
|
|
41
41
|
_legible_steps,
|
|
42
42
|
_log,
|
|
43
43
|
_solve_strip_ys,
|
|
44
|
+
_tol_suffix,
|
|
44
45
|
)
|
|
45
46
|
from draftwright.annotations._common import (
|
|
46
47
|
CROSSABLE_TYPES,
|
|
@@ -82,6 +83,14 @@ def hole_callout_spec(group: DimensionGroup) -> dict | None:
|
|
|
82
83
|
bore = _first(group, "diameter", "bore")
|
|
83
84
|
if bore is None:
|
|
84
85
|
return None
|
|
86
|
+
bore_tol = next(
|
|
87
|
+
(
|
|
88
|
+
pd.param.tolerance
|
|
89
|
+
for pd in group.dims
|
|
90
|
+
if pd.param.kind == "diameter" and pd.param.role == "bore"
|
|
91
|
+
),
|
|
92
|
+
None,
|
|
93
|
+
)
|
|
85
94
|
depth = _first(group, "depth", "bore")
|
|
86
95
|
count = feat.count
|
|
87
96
|
suffix = None
|
|
@@ -99,6 +108,7 @@ def hole_callout_spec(group: DimensionGroup) -> dict | None:
|
|
|
99
108
|
"cbore_dia": _first(group, "diameter", "counterbore", "spotface"),
|
|
100
109
|
"cbore_depth": _first(group, "depth", "counterbore", "spotface"),
|
|
101
110
|
"suffix": suffix,
|
|
111
|
+
"tolerance": bore_tol, # P2a: ± on the bore ⌀, baked into the callout string below
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
|
|
@@ -120,8 +130,14 @@ def callout_from_spec(spec, draft, count) -> HoleCallout | None:
|
|
|
120
130
|
def f(v): # see the #261 note above — every value crosses as a formatted string
|
|
121
131
|
return _fmt(v) if v is not None else None
|
|
122
132
|
|
|
133
|
+
dia = f(spec["diameter"])
|
|
134
|
+
if dia is not None:
|
|
135
|
+
# P2a: bake the ± tolerance into the bore string (helpers' HoleCallout accepts a
|
|
136
|
+
# diameter carrying tolerance/fit text, "8 ±0.05"); no tolerance → empty suffix.
|
|
137
|
+
dia += _tol_suffix(spec.get("tolerance"), draft)
|
|
138
|
+
|
|
123
139
|
return HoleCallout(
|
|
124
|
-
|
|
140
|
+
dia,
|
|
125
141
|
count=count,
|
|
126
142
|
through=spec["through"],
|
|
127
143
|
depth=f(spec["depth"]),
|
|
@@ -578,6 +594,28 @@ def _mentioned_diameters(dwg) -> set[float]:
|
|
|
578
594
|
return diams
|
|
579
595
|
|
|
580
596
|
|
|
597
|
+
def _place_what_fits(specs, axis: int, min_gap: float, lo: float, hi: float):
|
|
598
|
+
"""Fit as many ø specs as the strip ``[lo, hi]`` holds at ``min_gap`` spacing,
|
|
599
|
+
dropping the SMALLEST-diameter spec first when the full set overflows — so the
|
|
600
|
+
significant ODs survive and only the finest bands fall to ``feature_not_dimensioned``,
|
|
601
|
+
never the whole row/column (#298). ``specs`` = ``[(tip, dia, label, feat), ...]``;
|
|
602
|
+
``axis`` selects the strip coordinate of ``tip`` (0 = page-x for the row-below, 1 =
|
|
603
|
+
page-y for the column-left). Returns ``(survivors_in_strip_order, positions)`` —
|
|
604
|
+
``([], [])`` if not even one fits. A part whose full row already fits keeps every
|
|
605
|
+
spec in strip order, so existing output is unchanged."""
|
|
606
|
+
survivors = sorted(specs, key=lambda s: s[0][axis])
|
|
607
|
+
while survivors:
|
|
608
|
+
naturals = [s[0][axis] for s in survivors]
|
|
609
|
+
pos = _solve_strip_ys(naturals, min_gap, lo, hi) or _greedy_strip_ys(
|
|
610
|
+
naturals, min_gap, lo, hi
|
|
611
|
+
)
|
|
612
|
+
if pos is not None:
|
|
613
|
+
return survivors, pos
|
|
614
|
+
drop = min(range(len(survivors)), key=lambda i: survivors[i][1])
|
|
615
|
+
survivors.pop(drop)
|
|
616
|
+
return [], []
|
|
617
|
+
|
|
618
|
+
|
|
581
619
|
def _diameter_row_below(dwg, items, start: int = 0) -> int:
|
|
582
620
|
"""ø-callout row BELOW the front view for X-turned step/boss diameters (#77).
|
|
583
621
|
*items* is ``[(anchor, diameter), ...]``. The row is dropped clear of anything
|
|
@@ -599,28 +637,23 @@ def _diameter_row_below(dwg, items, start: int = 0) -> int:
|
|
|
599
637
|
label_y = obstacle_bottom - (draft.font_size + 4 * draft.pad_around_text)
|
|
600
638
|
if label_y < _MARGIN + draft.font_size:
|
|
601
639
|
return 0
|
|
602
|
-
specs = [] # (tip_page, label, feature), tip on the step's bottom silhouette
|
|
603
|
-
for anchor, dia, feat in items:
|
|
640
|
+
specs = [] # (tip_page, dia, label, feature), tip on the step's bottom silhouette
|
|
641
|
+
for anchor, dia, feat, dtol in items:
|
|
604
642
|
ax, ay, az = anchor
|
|
605
643
|
tip = dwg.at("front", ax, ay, az - dia / 2)
|
|
606
|
-
specs.append((tip, f"ø{_fmt(dia)}", feat))
|
|
607
|
-
specs.
|
|
608
|
-
half_w = max(len(label) for _, label, _ in specs) * draft.font_size * 0.62 / 2
|
|
644
|
+
specs.append((tip, dia, f"ø{_fmt(dia)}{_tol_suffix(dtol, draft)}", feat))
|
|
645
|
+
half_w = max(len(label) for _, _, label, _ in specs) * draft.font_size * 0.62 / 2
|
|
609
646
|
min_gap = 2 * half_w + 2 * draft.pad_around_text
|
|
610
|
-
|
|
611
|
-
xs =
|
|
612
|
-
|
|
613
|
-
)
|
|
614
|
-
if xs is None:
|
|
615
|
-
return 0
|
|
616
|
-
for i, ((tip, label, feat), lx) in enumerate(zip(specs, xs, strict=True)):
|
|
647
|
+
# Place what fits; drop the smallest ø first, never the whole row (#298).
|
|
648
|
+
survivors, xs = _place_what_fits(specs, 0, min_gap, fx0 + half_w, fx1 - half_w)
|
|
649
|
+
for i, ((tip, dia, label, feat), lx) in enumerate(zip(survivors, xs, strict=True)):
|
|
617
650
|
dwg.add(
|
|
618
651
|
Leader(tip=(tip[0], tip[1], 0), elbow=(lx, label_y, 0), label=label, draft=draft),
|
|
619
652
|
f"m_dia_x{start + i}",
|
|
620
653
|
view="front",
|
|
621
654
|
feature=feat,
|
|
622
655
|
)
|
|
623
|
-
return len(
|
|
656
|
+
return len(survivors)
|
|
624
657
|
|
|
625
658
|
|
|
626
659
|
def _diameter_column_left(dwg, items, start: int = 0) -> int:
|
|
@@ -632,31 +665,30 @@ def _diameter_column_left(dwg, items, start: int = 0) -> int:
|
|
|
632
665
|
return 0
|
|
633
666
|
draft = dwg.draft
|
|
634
667
|
fx0, fy0, _, fy1 = dwg.view_bounds("front")
|
|
635
|
-
label_w =
|
|
668
|
+
label_w = (
|
|
669
|
+
max(len(f"ø{_fmt(dia)}{_tol_suffix(dtol, draft)}") for _, dia, _, dtol in items)
|
|
670
|
+
* draft.font_size
|
|
671
|
+
* 0.62
|
|
672
|
+
)
|
|
636
673
|
elbow_x = fx0 - (draft.font_size + 2 * draft.pad_around_text)
|
|
637
674
|
if elbow_x - label_w < _MARGIN:
|
|
638
675
|
return 0
|
|
639
|
-
specs = [] # (tip_page, label, feature), tip on the step's left silhouette
|
|
640
|
-
for anchor, dia, feat in items:
|
|
676
|
+
specs = [] # (tip_page, dia, label, feature), tip on the step's left silhouette
|
|
677
|
+
for anchor, dia, feat, dtol in items:
|
|
641
678
|
ax, ay, az = anchor
|
|
642
679
|
tip = dwg.at("front", ax - dia / 2, ay, az)
|
|
643
|
-
specs.append((tip, f"ø{_fmt(dia)}", feat))
|
|
644
|
-
specs.sort(key=lambda s: s[0][1])
|
|
680
|
+
specs.append((tip, dia, f"ø{_fmt(dia)}{_tol_suffix(dtol, draft)}", feat))
|
|
645
681
|
half_h = draft.font_size / 2 + draft.pad_around_text
|
|
646
682
|
min_gap = 2 * half_h
|
|
647
|
-
|
|
648
|
-
ys =
|
|
649
|
-
naturals, min_gap, fy0 + half_h, fy1 - half_h
|
|
650
|
-
)
|
|
651
|
-
if ys is None:
|
|
652
|
-
return 0
|
|
683
|
+
# Place what fits; drop the smallest ø first, never the whole column (#298).
|
|
684
|
+
survivors, ys = _place_what_fits(specs, 1, min_gap, fy0 + half_h, fy1 - half_h)
|
|
653
685
|
# Full-footprint occupancy (leader shafts, witness/extension lines, hatch) — NOT
|
|
654
686
|
# the label-box-only `_occupied_boxes`, which is blind to a bore callout's leader
|
|
655
687
|
# SHAFT, so a ø label could silently overprint it (the #133/#225/#305 invisible-
|
|
656
688
|
# occupant class, #358). Centre lines stay crossable (a diameter dim may cross one).
|
|
657
689
|
occupied = strip_obstacles(dwg, view="front", crossable=CROSSABLE_TYPES)
|
|
658
690
|
placed = 0
|
|
659
|
-
for i, ((tip, label, feat), ly) in enumerate(zip(
|
|
691
|
+
for i, ((tip, dia, label, feat), ly) in enumerate(zip(survivors, ys, strict=True)):
|
|
660
692
|
ldr = Leader(tip=(tip[0], tip[1], 0), elbow=(elbow_x, ly, 0), label=label, draft=draft)
|
|
661
693
|
if _box_hits(_anno_box(ldr), occupied):
|
|
662
694
|
continue # would overprint a bore leader / existing callout — drop just this one
|
|
@@ -682,24 +714,35 @@ def render_diameters(dwg, groups, tol: float = 0.15, *, only=None) -> int:
|
|
|
682
714
|
# diameter (insertion-ordered), so provenance (#412) can tag the callout with its
|
|
683
715
|
# single owner — or leave it unowned when two distinct features share the diameter
|
|
684
716
|
# (the #398c/#406 shared-value rule, so drop can't over-strip a sibling).
|
|
685
|
-
row_buckets: dict = {} # round(dia,2) -> [anchor, dia, {features}] (X-turned)
|
|
717
|
+
row_buckets: dict = {} # round(dia,2) -> [anchor, dia, {features}, tolerance] (X-turned)
|
|
686
718
|
col_buckets: dict = {} # Z-turned
|
|
687
719
|
for g in groups:
|
|
688
720
|
if g.feature_kind not in ("step", "boss"):
|
|
689
721
|
continue
|
|
690
722
|
if only is not None and g.feature not in only: # #426 finalize: recorded subset
|
|
691
723
|
continue
|
|
692
|
-
|
|
693
|
-
if
|
|
724
|
+
dpd = next((pd for pd in g.dims if pd.param.kind == "diameter"), None)
|
|
725
|
+
if dpd is None:
|
|
726
|
+
continue
|
|
727
|
+
dia = dpd.param.value
|
|
728
|
+
if any(abs(dia - m) <= tol for m in mentioned):
|
|
694
729
|
continue
|
|
695
730
|
bucket = {"x": row_buckets, "z": col_buckets}.get(g.feature.frame.axis)
|
|
696
731
|
if bucket is None:
|
|
697
732
|
continue
|
|
698
733
|
dkey = round(dia, 2)
|
|
699
|
-
|
|
734
|
+
dtol = dpd.param.tolerance
|
|
735
|
+
# entry = [anchor, dia, {features}, ± tolerance]. A callout is per (axis, ⌀); the
|
|
736
|
+
# first authored tolerance on a shared ⌀ wins (P2a — a single callout, one label).
|
|
737
|
+
entry = bucket.setdefault(dkey, [g.anchor, dia, set(), dtol])
|
|
738
|
+
entry[2].add(g.feature)
|
|
739
|
+
if entry[3] is None:
|
|
740
|
+
entry[3] = dtol
|
|
700
741
|
|
|
701
742
|
def _items(buckets):
|
|
702
|
-
return [
|
|
743
|
+
return [
|
|
744
|
+
(a, d, next(iter(fs)) if len(fs) == 1 else None, t) for a, d, fs, t in buckets.values()
|
|
745
|
+
]
|
|
703
746
|
|
|
704
747
|
# The placers name leaders m_dia_{x,z}{start+i} CONTIGUOUSLY from one start. The auto-pass
|
|
705
748
|
# (only None) uses start=0 — byte-identical. The finalize path (only set) may run after
|
|
@@ -867,12 +910,14 @@ def _draw_step_chain(
|
|
|
867
910
|
draft = dwg.draft
|
|
868
911
|
gap = draft.font_size + 4 * draft.pad_around_text
|
|
869
912
|
horizontal = abs(segs[0][1][0] - segs[0][0][0]) >= abs(segs[0][1][1] - segs[0][0][1])
|
|
870
|
-
vals = [
|
|
913
|
+
vals = [s[2] for s in segs] # value is index 2 (segs are 4-tuples: pa, pb, value, tol)
|
|
871
914
|
mean_v = sum(vals) / len(vals)
|
|
872
915
|
if allow_collapse and len(segs) >= 3 and (max(vals) - min(vals)) <= 0.10 * mean_v:
|
|
916
|
+
# A uniform run collapses to one "N× v" dim; a per-step ± would be a false claim on
|
|
917
|
+
# N equal steps, so the collapse carries NO tolerance (#28 / P2a).
|
|
873
918
|
label = f"{len(segs)}× {_fmt(mean_v)}"
|
|
874
|
-
xs = [p[0] for pa, pb, _ in segs for p in (pa, pb)]
|
|
875
|
-
ys = [p[1] for pa, pb, _ in segs for p in (pa, pb)]
|
|
919
|
+
xs = [p[0] for pa, pb, *_ in segs for p in (pa, pb)]
|
|
920
|
+
ys = [p[1] for pa, pb, *_ in segs for p in (pa, pb)]
|
|
876
921
|
if horizontal:
|
|
877
922
|
dim = _dim((min(xs), y1, 0), (max(xs), y1, 0), "above", gap, draft, label=label)
|
|
878
923
|
else:
|
|
@@ -884,7 +929,8 @@ def _draw_step_chain(
|
|
|
884
929
|
tiers = [0] * len(segs)
|
|
885
930
|
if horizontal:
|
|
886
931
|
cw = [
|
|
887
|
-
((pa[0] + pb[0]) / 2, len(_fmt(v)) * draft.font_size * 0.62)
|
|
932
|
+
((pa[0] + pb[0]) / 2, len(_fmt(v)) * draft.font_size * 0.62)
|
|
933
|
+
for pa, pb, v, *_ in segs
|
|
888
934
|
]
|
|
889
935
|
|
|
890
936
|
def _clear(items): # (center, width) pairs in x order — labels don't overlap
|
|
@@ -904,14 +950,14 @@ def _draw_step_chain(
|
|
|
904
950
|
)
|
|
905
951
|
return 0
|
|
906
952
|
else:
|
|
907
|
-
shoulder_ys = sorted({c for pa, pb, _ in segs for c in (pa[1], pb[1])})
|
|
953
|
+
shoulder_ys = sorted({c for pa, pb, *_ in segs for c in (pa[1], pb[1])})
|
|
908
954
|
if any(b - a < tier_step for a, b in zip(shoulder_ys, shoulder_ys[1:])):
|
|
909
955
|
_log.info("step-length chain skipped: shoulders too close to dimension")
|
|
910
956
|
_record_step_chain_drop(dwg, "turned shoulders too closely spaced to dimension")
|
|
911
957
|
return 0
|
|
912
958
|
|
|
913
959
|
candidates = []
|
|
914
|
-
for i, (pa, pb, value) in enumerate(segs):
|
|
960
|
+
for i, (pa, pb, value, seg_tol) in enumerate(segs):
|
|
915
961
|
if horizontal:
|
|
916
962
|
p1, p2, side = (pa[0], y1, 0), (pb[0], y1, 0), "above"
|
|
917
963
|
dist = gap + tiers[i] * tier_step
|
|
@@ -919,7 +965,10 @@ def _draw_step_chain(
|
|
|
919
965
|
p1, p2, side = (x1, pa[1], 0), (x1, pb[1], 0), "right"
|
|
920
966
|
dist = gap
|
|
921
967
|
candidates.append(
|
|
922
|
-
(
|
|
968
|
+
(
|
|
969
|
+
f"{name_prefix}{start + i}",
|
|
970
|
+
_dim(p1, p2, side, dist, draft, label=_fmt(value), tolerance=seg_tol),
|
|
971
|
+
)
|
|
923
972
|
)
|
|
924
973
|
|
|
925
974
|
# Room guard: if any dim would fall off the drawable page, place NONE.
|
|
@@ -980,14 +1029,14 @@ def render_step_lengths(dwg, groups, *, only=None) -> int:
|
|
|
980
1029
|
)
|
|
981
1030
|
if length is None or length.span is None:
|
|
982
1031
|
continue
|
|
983
|
-
rows.append((length.span[0], length.span[1], length.value))
|
|
1032
|
+
rows.append((length.span[0], length.span[1], length.value, length.tolerance))
|
|
984
1033
|
if not rows:
|
|
985
1034
|
return 0
|
|
986
1035
|
draft = dwg.draft
|
|
987
1036
|
# only=None (auto-pass) → start=0, historical m_steplen naming, byte-identical. The
|
|
988
1037
|
# finalize path (only set) starts past existing m_steplen names (#426 naming seam).
|
|
989
1038
|
start = _next_steplen_start(dwg) if only is not None else 0
|
|
990
|
-
fsegs = [(dwg.at("front", *a), dwg.at("front", *b), v) for a, b, v in rows]
|
|
1039
|
+
fsegs = [(dwg.at("front", *a), dwg.at("front", *b), v, t) for a, b, v, t in rows]
|
|
991
1040
|
horizontal = abs(fsegs[0][1][0] - fsegs[0][0][0]) >= abs(fsegs[0][1][1] - fsegs[0][0][1])
|
|
992
1041
|
|
|
993
1042
|
# X-turned crowded-head detour (#307): split off each contiguous *run of ≥2*
|
|
@@ -997,7 +1046,7 @@ def render_step_lengths(dwg, groups, *, only=None) -> int:
|
|
|
997
1046
|
# (#307 review). The legible steps + blocks stay as the main chain.
|
|
998
1047
|
if horizontal:
|
|
999
1048
|
floor_pg = 2 * draft.arrow_length
|
|
1000
|
-
sub = [i for i, (pa, pb, _) in enumerate(fsegs) if abs(pb[0] - pa[0]) < floor_pg]
|
|
1049
|
+
sub = [i for i, (pa, pb, *_) in enumerate(fsegs) if abs(pb[0] - pa[0]) < floor_pg]
|
|
1001
1050
|
runs: list[list[int]] = []
|
|
1002
1051
|
for j in sub:
|
|
1003
1052
|
(runs[-1].append(j) if runs and j == runs[-1][-1] + 1 else runs.append([j]))
|
|
@@ -1006,17 +1055,20 @@ def render_step_lengths(dwg, groups, *, only=None) -> int:
|
|
|
1006
1055
|
blocks = []
|
|
1007
1056
|
for run in heads:
|
|
1008
1057
|
ra = [rows[i] for i in run]
|
|
1009
|
-
hlo = min(min(a[0], b[0]) for a, b, _ in ra)
|
|
1010
|
-
hhi = max(max(a[0], b[0]) for a, b, _ in ra)
|
|
1011
|
-
minlen = min(
|
|
1058
|
+
hlo = min(min(a[0], b[0]) for a, b, *_ in ra)
|
|
1059
|
+
hhi = max(max(a[0], b[0]) for a, b, *_ in ra)
|
|
1060
|
+
minlen = min(r[2] for r in ra) # value is index 2 (rows are 4-tuples: a,b,v,tol)
|
|
1012
1061
|
# World→page scale for the detail (no sheet factor — detail_scale is an
|
|
1013
1062
|
# absolute world→page scale). (#307 review)
|
|
1014
1063
|
scale_needed = _MIN_STEP_SEP_MM / minlen if minlen > 0 else float("inf")
|
|
1015
|
-
|
|
1064
|
+
# A head *block* is a synthetic span, not one toleranced step — carry no ± (None).
|
|
1065
|
+
blocks.append(
|
|
1066
|
+
(dwg.at("front", hlo, 0, 0), dwg.at("front", hhi, 0, 0), hhi - hlo, None)
|
|
1067
|
+
)
|
|
1016
1068
|
|
|
1017
1069
|
def _redraw(dwg, view, detail_scale, _hw=ra):
|
|
1018
1070
|
# View-scoped name prefix so two detail views never collide (#307 review).
|
|
1019
|
-
hsegs = [(dwg.at(view, *a), dwg.at(view, *b), v) for a, b, v in _hw]
|
|
1071
|
+
hsegs = [(dwg.at(view, *a), dwg.at(view, *b), v, t) for a, b, v, t in _hw]
|
|
1020
1072
|
return _draw_step_chain(dwg, view, hsegs, f"dim_{view}_steplen", detail_scale)
|
|
1021
1073
|
|
|
1022
1074
|
dwg._detail_requests.append(
|
|
@@ -388,12 +388,13 @@ def add_feature_diameter(dwg, feature) -> str:
|
|
|
388
388
|
"pass one from dwg.model().features"
|
|
389
389
|
)
|
|
390
390
|
group = next((g for g in plan_dimensions(model) if g.feature is feature), None)
|
|
391
|
-
|
|
392
|
-
next((pd
|
|
391
|
+
dpd = (
|
|
392
|
+
next((pd for pd in group.dims if pd.param.kind == "diameter"), None)
|
|
393
393
|
if group is not None
|
|
394
394
|
else None
|
|
395
395
|
)
|
|
396
|
-
if
|
|
396
|
+
dia = dpd.param.value if dpd is not None else None
|
|
397
|
+
if group is None or dpd is None or dia is None:
|
|
397
398
|
raise ValueError(
|
|
398
399
|
f"callout(): {type(feature).__name__} exposes no step/boss diameter callout"
|
|
399
400
|
)
|
|
@@ -403,7 +404,9 @@ def add_feature_diameter(dwg, feature) -> str:
|
|
|
403
404
|
f"callout(): a {axis!r}-turned step/boss diameter is not placeable "
|
|
404
405
|
"(only X- and Z-turned parts)"
|
|
405
406
|
)
|
|
406
|
-
|
|
407
|
+
# 4-tuple (anchor, dia, feature, tolerance): a manual callout honours a declared ±
|
|
408
|
+
# tolerance too, like the auto-pass (P2a, #28).
|
|
409
|
+
items = [(group.anchor, dia, feature, dpd.param.tolerance)]
|
|
407
410
|
# The row/column placers name leaders m_dia_{x,z}{start+i} — pass the first FREE
|
|
408
411
|
# index so a second callout() (or a call on an already-annotated turned part) never
|
|
409
412
|
# collides on m_dia_x0/z0 and clobbers an existing leader (#419 review F1).
|
|
@@ -698,7 +701,7 @@ def _add_furniture(dwg, a: Analysis, view, j, feat: PatternFeature | None, to_pa
|
|
|
698
701
|
here) so it survives finalize's ``place_furniture=False`` (#426 Ph4c)."""
|
|
699
702
|
if feat is None:
|
|
700
703
|
return
|
|
701
|
-
members = feat.members
|
|
704
|
+
members = feat.members or (feat.frame.origin,) # guard a declared pattern's empty members
|
|
702
705
|
# Remember the bore-callout name AND the holes it documents (by position), so a
|
|
703
706
|
# later hole-table escalation leaves the grouped pattern callout standing and
|
|
704
707
|
# tabulates only the holes no *placed* pattern callout covers (#92).
|