draftwright 0.2.8__tar.gz → 0.2.9__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.8 → draftwright-0.2.9}/CHANGELOG.md +39 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/PKG-INFO +31 -1
- {draftwright-0.2.8 → draftwright-0.2.9}/README.md +30 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/pyproject.toml +1 -1
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/_core.py +19 -2
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/annotate.py +1 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/annotations/_common.py +69 -13
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/annotations/from_model.py +181 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/annotations/orchestrator.py +22 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/builder.py +12 -1
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/model/__init__.py +14 -1
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/model/declare.py +130 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/model/ir.py +70 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/sheet_dsl.py +208 -5
- draftwright-0.2.9/tests/test_gdt_placement.py +230 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_sheet_emit.py +71 -1
- draftwright-0.2.9/tests/test_sheet_gdt.py +188 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/.gitignore +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/LICENSE +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/skills/SKILL.md +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/analysis.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/annotations/holes.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/annotations/sections.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/cli.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/drawing.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/export.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/features.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/fits.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/src/draftwright/sheet_emit.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/_kernel.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/bracket.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/dshape.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/side_drilled.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_declare.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_fits.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_layout.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_linting.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_make_drawing.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_object_aspects.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_part_model.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_pmi.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_recognition.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_registry.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_sheet_of.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_strip_layout.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_tolerances.py +0 -0
- {draftwright-0.2.8 → draftwright-0.2.9}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.9 — 2026-07-06
|
|
4
|
+
|
|
5
|
+
**Declarative GD&T, datums, and surface finish (ADR 0011 Phase 2b/2c).** The `Sheet` DSL can
|
|
6
|
+
now author the drawing information geometry can't carry — geometric tolerances, datum feature
|
|
7
|
+
symbols, and surface-finish marks — by pointing at a build123d feature or face. They render
|
|
8
|
+
through the auto-layout engine as first-class ADR 0009 corridor candidates, placed and spaced
|
|
9
|
+
crossing-free alongside the automatic dimensions (not a post-hoc overlay).
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
sheet.datum("A", base_face)
|
|
13
|
+
sheet.control(bore).position(0.1, to="A", diameter=True).perpendicularity(0.05, to="A")
|
|
14
|
+
sheet.diameter(journal).finish("0.8")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Feature control frames (ISO 1101)** — `sheet.control(ref)` returns a chainable builder with
|
|
20
|
+
one method per **all 14** characteristics (`position`, `flatness`, `perpendicularity`,
|
|
21
|
+
`cylindricity`, `circular_runout`, `total_runout`, …); `to=` names the referenced datum(s),
|
|
22
|
+
`diameter=` prefixes the ⌀ tolerance zone, `modifier=` adds a material-condition symbol (Ⓜ/Ⓛ/Ⓟ).
|
|
23
|
+
- **Datum feature symbols (ISO 5459)** — `sheet.datum("A", face_or_feature)`.
|
|
24
|
+
- **Surface finish (ISO 1302, Ra)** — `.finish("1.6")` on a hole / diameter / step handle, or
|
|
25
|
+
`sheet.finish(ra, face)`.
|
|
26
|
+
- The target view + strip side are **derived from the referenced geometry** (a feature's axis →
|
|
27
|
+
face-on view, a planar face's normal → edge-on view); `view=`/`side=` override.
|
|
28
|
+
- Render core: `ControlFrame` / `DatumRef` / `Finish` IR features placed by `render_gdt` as
|
|
29
|
+
first-class corridor candidates, with the real-footprint plumbing needed to space wide frames.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- A control frame referencing an **undeclared datum letter** now warns at build.
|
|
34
|
+
- GD&T placement is **title-block aware** and **falls through to the opposite strip side** before
|
|
35
|
+
dropping a frame, so stacked control frames place robustly rather than vanishing or overlapping
|
|
36
|
+
the title block.
|
|
37
|
+
- An invalid glyph spec / degenerate leader in a caller-supplied model **drops the one item with a
|
|
38
|
+
warning** instead of crashing the whole build (the IR is public input).
|
|
39
|
+
- Turned-shaft rotational furniture now reproduces correctly through the declared-model path
|
|
40
|
+
(`model=` / `Sheet`) (#476).
|
|
41
|
+
|
|
3
42
|
## v0.2.8 — 2026-07-06
|
|
4
43
|
|
|
5
44
|
**`--script` now defaults to the declarative `Sheet` DSL.** The editable script the CLI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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
|
|
@@ -769,6 +769,36 @@ issues = dwg.lint() # list[LintIssue] — coverage, page b
|
|
|
769
769
|
svg_path, dxf_path = dwg.export("my_part")
|
|
770
770
|
```
|
|
771
771
|
|
|
772
|
+
### Declarative drawings — reference features, declare intent
|
|
773
|
+
|
|
774
|
+
Instead of relying on detection, **reference the build123d objects you built** and declare
|
|
775
|
+
only the drawing intent — geometry supplies the sizes (⌀ read off the object), you supply
|
|
776
|
+
tolerances, fits, datums, GD&T, and surface finish. The fluent `Sheet` skips detection and
|
|
777
|
+
dimensions exactly what you declare (ADR 0011):
|
|
778
|
+
|
|
779
|
+
```python
|
|
780
|
+
from build123d import Box, Cylinder, Pos
|
|
781
|
+
from draftwright import Sheet
|
|
782
|
+
|
|
783
|
+
plate = Box(120, 80, 20)
|
|
784
|
+
bore = Pos(0, 0, 0) * Cylinder(4, 20)
|
|
785
|
+
|
|
786
|
+
sheet = Sheet(plate - bore, title="Plate", number="DWG-002")
|
|
787
|
+
sheet.envelope()
|
|
788
|
+
sheet.datum("A", plate.faces().sort_by()[-1]) # datum A on the top face
|
|
789
|
+
hole = sheet.hole(bore).finish("1.6") # ⌀8 bore, Ra 1.6
|
|
790
|
+
sheet.control(hole).position(0.1, to="A", diameter=True) # ⌀0.1 position wrt A
|
|
791
|
+
sheet.export("plate") # writes plate.svg + plate.dxf
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
Every aspect the geometry can't carry is a declared verb: `.tolerance(±)` / `.fit("H7")`
|
|
795
|
+
(ISO 286), `.finish("Ra")` (ISO 1302), `sheet.datum(letter, ref)` (ISO 5459), and
|
|
796
|
+
`sheet.control(ref)` with all 14 ISO 1101 characteristics
|
|
797
|
+
(`.position`/`.flatness`/`.perpendicularity`/`.circular_runout`/…). A feature verb returns
|
|
798
|
+
a chainable handle (`sheet.hole(bore)`) that the aspect and `control(...)` verbs decorate;
|
|
799
|
+
targets are placed automatically — the view and strip are derived from the referenced
|
|
800
|
+
feature or face, with `view=`/`side=` overrides.
|
|
801
|
+
|
|
772
802
|
## What it produces
|
|
773
803
|
|
|
774
804
|
- **Three orthographic views** (front, plan, side) sized and scaled automatically to the
|
|
@@ -75,6 +75,36 @@ issues = dwg.lint() # list[LintIssue] — coverage, page b
|
|
|
75
75
|
svg_path, dxf_path = dwg.export("my_part")
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### Declarative drawings — reference features, declare intent
|
|
79
|
+
|
|
80
|
+
Instead of relying on detection, **reference the build123d objects you built** and declare
|
|
81
|
+
only the drawing intent — geometry supplies the sizes (⌀ read off the object), you supply
|
|
82
|
+
tolerances, fits, datums, GD&T, and surface finish. The fluent `Sheet` skips detection and
|
|
83
|
+
dimensions exactly what you declare (ADR 0011):
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from build123d import Box, Cylinder, Pos
|
|
87
|
+
from draftwright import Sheet
|
|
88
|
+
|
|
89
|
+
plate = Box(120, 80, 20)
|
|
90
|
+
bore = Pos(0, 0, 0) * Cylinder(4, 20)
|
|
91
|
+
|
|
92
|
+
sheet = Sheet(plate - bore, title="Plate", number="DWG-002")
|
|
93
|
+
sheet.envelope()
|
|
94
|
+
sheet.datum("A", plate.faces().sort_by()[-1]) # datum A on the top face
|
|
95
|
+
hole = sheet.hole(bore).finish("1.6") # ⌀8 bore, Ra 1.6
|
|
96
|
+
sheet.control(hole).position(0.1, to="A", diameter=True) # ⌀0.1 position wrt A
|
|
97
|
+
sheet.export("plate") # writes plate.svg + plate.dxf
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Every aspect the geometry can't carry is a declared verb: `.tolerance(±)` / `.fit("H7")`
|
|
101
|
+
(ISO 286), `.finish("Ra")` (ISO 1302), `sheet.datum(letter, ref)` (ISO 5459), and
|
|
102
|
+
`sheet.control(ref)` with all 14 ISO 1101 characteristics
|
|
103
|
+
(`.position`/`.flatness`/`.perpendicularity`/`.circular_runout`/…). A feature verb returns
|
|
104
|
+
a chainable handle (`sheet.hole(bore)`) that the aspect and `control(...)` verbs decorate;
|
|
105
|
+
targets are placed automatically — the view and strip are derived from the referenced
|
|
106
|
+
feature or face, with `view=`/`side=` overrides.
|
|
107
|
+
|
|
78
108
|
## What it produces
|
|
79
109
|
|
|
80
110
|
- **Three orthographic views** (front, plan, side) sized and scaled automatically to the
|
|
@@ -557,8 +557,11 @@ def _attribution_author(drawn_by: str | None) -> str:
|
|
|
557
557
|
return f"{author} / draftwright" if author else "draftwright"
|
|
558
558
|
|
|
559
559
|
|
|
560
|
-
def
|
|
561
|
-
"""
|
|
560
|
+
def _make_title_block(dwg, a: Analysis):
|
|
561
|
+
"""Construct + page-locate the title block, returning ``(tb, cell)`` where *cell* is its
|
|
562
|
+
drawn-by cell bbox (for the hyperlink rect). Shared by :func:`_add_title_block` (which adds
|
|
563
|
+
it, last) and :func:`_title_block_box` (which measures its footprint for GD&T avoidance, #481)
|
|
564
|
+
so the two never drift."""
|
|
562
565
|
tb = TitleBlock(
|
|
563
566
|
a.title,
|
|
564
567
|
a.number,
|
|
@@ -582,6 +585,20 @@ def _add_title_block(dwg, a: Analysis):
|
|
|
582
585
|
# TitleBlock layout change. Build-frame bbox; translated to page space below.
|
|
583
586
|
cell = tb.drawn_by_cell_bbox()
|
|
584
587
|
tb = tb.locate(Location((a.PAGE_W - a.TB_W - _TB_CLEAR, _TB_CLEAR, 0)))
|
|
588
|
+
return tb, cell
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _title_block_box(dwg, a: Analysis):
|
|
592
|
+
"""The title block's real page-space bbox ``(x0, y0, x1, y1)``. GD&T placement avoids it
|
|
593
|
+
(#481): the block is added last, so strip placement can't see it, but it's deterministic."""
|
|
594
|
+
tb, _ = _make_title_block(dwg, a)
|
|
595
|
+
b = tb.bounding_box()
|
|
596
|
+
return (b.min.X, b.min.Y, b.max.X, b.max.Y)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _add_title_block(dwg, a: Analysis):
|
|
600
|
+
"""Add the title block annotation."""
|
|
601
|
+
tb, cell = _make_title_block(dwg, a)
|
|
585
602
|
dwg.add(tb, "title_block")
|
|
586
603
|
|
|
587
604
|
# Record that cell's page-space rectangle so export() can place a clickable
|
|
@@ -305,6 +305,16 @@ class CorridorCandidate:
|
|
|
305
305
|
# The source IR feature this dim was rendered for — recorded as provenance when the
|
|
306
306
|
# dim is placed at drain (ADR 0010). ``None`` leaves the annotation feature-less.
|
|
307
307
|
feature: object | None = None
|
|
308
|
+
# Real stacking-axis + perpendicular footprint ``(w, h)`` in page-mm, or ``None`` to
|
|
309
|
+
# use the dimension default ``(tier, tier)``. Wide/tall occupants (a GD&T feature
|
|
310
|
+
# control frame is ~24×6 mm) set this so the strip solve reserves their true extent
|
|
311
|
+
# instead of one label-height (ADR 0009 real-footprint plumbing, #61). A dim leaves
|
|
312
|
+
# it ``None`` — byte-identical to the pre-plumbing placement.
|
|
313
|
+
size: tuple | None = None
|
|
314
|
+
# An ``(x0, y0, x1, y1)`` page-box this candidate must NOT overlap even when force-kept —
|
|
315
|
+
# the title block, which is placed after the corridor drain so the strip carve can't see
|
|
316
|
+
# it (#481). ``None`` (every dim) skips the check → byte-identical.
|
|
317
|
+
forbid: object | None = None
|
|
308
318
|
|
|
309
319
|
|
|
310
320
|
def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
@@ -357,15 +367,29 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
357
367
|
return
|
|
358
368
|
pairs = [(c.name, c.build) for c in kept]
|
|
359
369
|
feats = {c.name: c.feature for c in kept if c.feature is not None} # provenance (ADR 0010)
|
|
370
|
+
sizes = {c.name: c.size for c in kept if c.size is not None} # real footprint (#61)
|
|
371
|
+
forbid = {c.name: c.forbid for c in kept if c.forbid is not None} # title-block box (#481)
|
|
360
372
|
left = {
|
|
361
|
-
n
|
|
373
|
+
n
|
|
374
|
+
for n, _ in place_strip_candidates(
|
|
375
|
+
dwg, strip, view, axis, pairs, tier, features=feats, sizes=sizes, forbid=forbid
|
|
376
|
+
)
|
|
362
377
|
}
|
|
363
378
|
force_pairs = [(c.name, c.build) for c in kept if c.name in left and c.force]
|
|
364
379
|
still = (
|
|
365
380
|
{
|
|
366
381
|
n
|
|
367
382
|
for n, _ in place_strip_candidates(
|
|
368
|
-
dwg,
|
|
383
|
+
dwg,
|
|
384
|
+
strip,
|
|
385
|
+
view,
|
|
386
|
+
axis,
|
|
387
|
+
force_pairs,
|
|
388
|
+
tier,
|
|
389
|
+
force=True,
|
|
390
|
+
features=feats,
|
|
391
|
+
sizes=sizes,
|
|
392
|
+
forbid=forbid,
|
|
369
393
|
)
|
|
370
394
|
}
|
|
371
395
|
if force_pairs
|
|
@@ -399,7 +423,9 @@ def drain_corridors(dwg):
|
|
|
399
423
|
dwg._corridor_batch = {}
|
|
400
424
|
|
|
401
425
|
|
|
402
|
-
def place_strip_candidates(
|
|
426
|
+
def place_strip_candidates(
|
|
427
|
+
dwg, strip, view, axis, cands, tier, *, force=False, features=None, sizes=None, forbid=None
|
|
428
|
+
):
|
|
403
429
|
"""Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
|
|
404
430
|
The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
|
|
405
431
|
P3): each candidate in *cands* — an ``(name, build(pos)->dim)`` pair — is spaced by
|
|
@@ -418,6 +444,12 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False,
|
|
|
418
444
|
1-D strip carve cannot represent: a leader in that corridor is crossed no matter how
|
|
419
445
|
far out the dim line lands. By default such a placement is rejected so the caller can
|
|
420
446
|
route the dim to the other view (its disjoint block cannot cross this leader).
|
|
447
|
+
|
|
448
|
+
*sizes* maps a candidate's name to its real page-mm footprint ``(w, h)``; absent
|
|
449
|
+
names use the dimension default ``(tier, tier)``. A wide/tall occupant (a GD&T
|
|
450
|
+
frame, #61) sets it so :func:`plan_strip` enforces its true stacking gap — over
|
|
451
|
+
capacity it is relocated to the next segment or dropped, never overlapped.
|
|
452
|
+
|
|
421
453
|
``force=True`` skips that corridor check — the caller's last resort when no view took
|
|
422
454
|
the dim cleanly: keep it on its natural view and accept the (same-feature) leader
|
|
423
455
|
crossing rather than drop a real dimension (policy B). Candidates that find no strip
|
|
@@ -426,17 +458,31 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False,
|
|
|
426
458
|
if strip is None or not cands:
|
|
427
459
|
return list(cands)
|
|
428
460
|
lo, hi, inner = strip_free_span(strip)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
# the
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
#
|
|
461
|
+
idx = 1 if axis == "y" else 0
|
|
462
|
+
|
|
463
|
+
# Reserve the outermost label's OUTWARD extent at the strip boundary. plan_strip bounds
|
|
464
|
+
# the dim-LINE position, but the label extends outward from it — so without this the last
|
|
465
|
+
# tier's label overshoots outer_limit (into the iso view / page margin), unlike the old
|
|
466
|
+
# Strip.allocate which checked `start + tier <= outer_limit` (#338 review). A plain dim's
|
|
467
|
+
# label extends one `tier` outward (one-sided). A GD&T glyph (#61) hangs off a Leader that
|
|
468
|
+
# CENTRES it on the elbow for an above/below strip (real outward extent = height/2) but
|
|
469
|
+
# places it one-sided for a left/right strip (extent = full width). Reserve the MAX real
|
|
470
|
+
# outward extent among these candidates — else a glyph wider than `tier` renders off the
|
|
471
|
+
# sheet (annotation_out_of_bounds) instead of dropping when the strip is too narrow (ADR
|
|
472
|
+
# 0009 Amdt 7 fixed inter-candidate gaps but not this edge). With no `sizes` (every dim)
|
|
473
|
+
# this is `tier`, byte-identical. The strip edge is not an obstacle (obstacles carry their
|
|
474
|
+
# own footprint + pad), so only the boundary needs it.
|
|
475
|
+
def _outward(name):
|
|
476
|
+
sz = (sizes or {}).get(name)
|
|
477
|
+
if sz is None:
|
|
478
|
+
return tier # a dim: one-sided tier reservation (unchanged)
|
|
479
|
+
return sz[idx] if axis == "x" else sz[idx] / 2 # GD&T: one-sided (L/R) vs centred (A/B)
|
|
480
|
+
|
|
481
|
+
reserve = max([tier, *(_outward(n) for n, _ in cands)])
|
|
435
482
|
if inner == lo:
|
|
436
|
-
hi -=
|
|
483
|
+
hi -= reserve
|
|
437
484
|
else:
|
|
438
|
-
lo +=
|
|
439
|
-
idx = 1 if axis == "y" else 0
|
|
485
|
+
lo += reserve
|
|
440
486
|
perp = 0 if axis == "y" else 1 # the axis the dims do NOT stack along
|
|
441
487
|
pad = tier + strip.spacing # min separation between stacked dim lines
|
|
442
488
|
# Perpendicular band of these candidates. The 1-D carve projects obstacles onto the
|
|
@@ -471,7 +517,9 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False,
|
|
|
471
517
|
triples = [
|
|
472
518
|
(
|
|
473
519
|
StripCandidate(
|
|
474
|
-
f"{(k if inner == lo else len(take) - 1 - k):04d}",
|
|
520
|
+
f"{(k if inner == lo else len(take) - 1 - k):04d}",
|
|
521
|
+
anch,
|
|
522
|
+
(sizes or {}).get(nb[0], (tier, tier)),
|
|
475
523
|
),
|
|
476
524
|
nb,
|
|
477
525
|
)
|
|
@@ -487,6 +535,14 @@ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False,
|
|
|
487
535
|
if not force and _box_hits(_geom_box(dim), blockers): # corridor crosses a leader
|
|
488
536
|
todo.append((name, build))
|
|
489
537
|
continue
|
|
538
|
+
# A forbidden box (the title block, #481) is rejected even under force — it is
|
|
539
|
+
# placed after the drain, so the strip carve can't see it; a force-kept GD&T frame
|
|
540
|
+
# must still not stack onto it. `forbid` maps names to their box (only GD&T sets it,
|
|
541
|
+
# so dims are byte-identical). Returned unplaced → the caller's on_drop fallthrough.
|
|
542
|
+
fb = (forbid or {}).get(name)
|
|
543
|
+
if fb is not None and _box_hits(_geom_box(dim), (fb,)):
|
|
544
|
+
todo.append((name, build))
|
|
545
|
+
continue
|
|
490
546
|
# Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
|
|
491
547
|
# dims — `features` maps this batch's names to their source IR feature.
|
|
492
548
|
dwg.add(dim, name, view=view, feature=(features or {}).get(name))
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import math
|
|
22
22
|
|
|
23
|
+
from build123d_drafting import DatumFeature, FeatureControlFrame, SurfaceFinish
|
|
23
24
|
from build123d_drafting.helpers import Centerline, CenterMark, HoleCallout, Leader, TitleBlock
|
|
24
25
|
|
|
25
26
|
from draftwright._core import (
|
|
@@ -41,6 +42,7 @@ from draftwright._core import (
|
|
|
41
42
|
_legible_steps,
|
|
42
43
|
_log,
|
|
43
44
|
_solve_strip_ys,
|
|
45
|
+
_title_block_box,
|
|
44
46
|
_tol_suffix,
|
|
45
47
|
)
|
|
46
48
|
from draftwright.annotations._common import (
|
|
@@ -1775,3 +1777,182 @@ def render_pmi(dwg, model, a) -> int:
|
|
|
1775
1777
|
|
|
1776
1778
|
_log.info("PMI annotate: %d/%d dims placed", emitted, len(usable))
|
|
1777
1779
|
return emitted
|
|
1780
|
+
|
|
1781
|
+
|
|
1782
|
+
# GD&T aspect side-layer (ADR 0011 §4, #61) — declared feature control frames / datum
|
|
1783
|
+
# feature symbols / surface finishes. Placed as first-class ADR 0009 corridor candidates,
|
|
1784
|
+
# NOT through the dimension planner (their IR items carry no DimParameters).
|
|
1785
|
+
_GDT_KINDS = ("control_frame", "datum_ref", "finish")
|
|
1786
|
+
# Outer run of the shared corridor ladder: GD&T frames tier BEYOND the feature-size
|
|
1787
|
+
# (_SIZE_SUBCHAIN=0) and datum-location (_LOC_SUBCHAIN=1) dim runs, so a frame never lands
|
|
1788
|
+
# mid-ladder among the dimensions it annotates.
|
|
1789
|
+
_GDT_SUBCHAIN = 2
|
|
1790
|
+
# Minimum GD&T leader shaft length (page-mm). A zero-length Leader (site == solved tier)
|
|
1791
|
+
# makes OCC's edge builder raise; nudging to this keeps `_build` total (#61 review).
|
|
1792
|
+
_MIN_LEADER = 0.05
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
def _gdt_glyph(item, draft):
|
|
1796
|
+
"""Build the ISO 1101/5459/1302 glyph sketch for one GD&T IR item at the origin
|
|
1797
|
+
(the :class:`Leader` repositions it). A fresh sketch per call — the leader translate
|
|
1798
|
+
must not alias a shared object across the strip solve's repeated probe builds."""
|
|
1799
|
+
if item.kind == "control_frame":
|
|
1800
|
+
return FeatureControlFrame(
|
|
1801
|
+
item.characteristic,
|
|
1802
|
+
item.tolerance,
|
|
1803
|
+
datums=item.datums,
|
|
1804
|
+
draft=draft,
|
|
1805
|
+
diameter=item.diameter,
|
|
1806
|
+
modifier=item.modifier,
|
|
1807
|
+
)
|
|
1808
|
+
if item.kind == "datum_ref":
|
|
1809
|
+
return DatumFeature(item.letter, draft=draft)
|
|
1810
|
+
return SurfaceFinish(item.ra, position=(0.0, 0.0), draft=draft)
|
|
1811
|
+
|
|
1812
|
+
|
|
1813
|
+
def render_gdt(dwg, model, a) -> int:
|
|
1814
|
+
"""Place declared GD&T frames / datum symbols / surface finishes (#61) as first-class
|
|
1815
|
+
ADR 0009 corridor candidates — registered into the SAME strip the feature's dimensions
|
|
1816
|
+
use, BEFORE ``drain_corridors``, so one solve orders and spaces them crossing-free with
|
|
1817
|
+
the dims (not a leftover first-fit like :func:`render_pmi`). Each item carries its target
|
|
1818
|
+
``(view, side)`` strip + model-space site; the leader hangs the glyph off the site into
|
|
1819
|
+
that strip. The strip footprint is the GLYPH's own box — NOT the leader+glyph box, whose
|
|
1820
|
+
shaft back to the feature would inflate the stacking extent (the same reason dims reserve
|
|
1821
|
+
one label-height). Cross-view separation is the compose-then-pack repack's job (ADR 0004):
|
|
1822
|
+
every placed frame is ``view=``-tagged, so ``_measure_blocks`` folds it into the block.
|
|
1823
|
+
Returns the count registered."""
|
|
1824
|
+
items = [f for f in model.features if f.kind in _GDT_KINDS]
|
|
1825
|
+
if not items:
|
|
1826
|
+
return 0
|
|
1827
|
+
draft = dwg.draft
|
|
1828
|
+
tier = draft.font_size + 2 * draft.pad_around_text
|
|
1829
|
+
# (zones, h-projector, v-projector, h-model-index, v-model-index) per view.
|
|
1830
|
+
views = {
|
|
1831
|
+
"plan": (a.pv_zones, a.proj.plan_x, a.proj.plan_y, 0, 1),
|
|
1832
|
+
"front": (a.fv_zones, a.proj.front_x, a.proj.front_z, 0, 2),
|
|
1833
|
+
"side": (a.sv_zones, a.proj.side_x, a.proj.side_z, 1, 2),
|
|
1834
|
+
}
|
|
1835
|
+
# The title block (bottom-right) is added AFTER drain_corridors, so strip placement can't
|
|
1836
|
+
# see it — a below/right strip runs down into its region. Its box is deterministic, so
|
|
1837
|
+
# reject any GD&T placement that would land on it (BOTH the primary corridor path, via the
|
|
1838
|
+
# candidate's `forbid`, AND the fallthrough) else the frame overlaps 'DRAWING' (#481 review).
|
|
1839
|
+
tb_box = _title_block_box(dwg, a)
|
|
1840
|
+
n = 0
|
|
1841
|
+
for i, item in enumerate(items):
|
|
1842
|
+
name = f"m_gdt{i}"
|
|
1843
|
+
vk = views.get(item.view)
|
|
1844
|
+
if vk is None or item.side not in ("above", "below", "left", "right"):
|
|
1845
|
+
dwg._record_build_issue(
|
|
1846
|
+
"warning", "gdt_dropped", f"{name}: bad target {item.view!r}/{item.side!r}"
|
|
1847
|
+
)
|
|
1848
|
+
continue
|
|
1849
|
+
zones, hproj, vproj, hi, vi = vk
|
|
1850
|
+
strip = getattr(zones, item.side)
|
|
1851
|
+
o = item.frame.origin
|
|
1852
|
+
px, py = hproj(o[hi]), vproj(o[vi])
|
|
1853
|
+
horizontal = item.side in ("above", "below") # frame stacks along y
|
|
1854
|
+
axis = "y" if horizontal else "x"
|
|
1855
|
+
# The IR is public input (ADR 0011), so an invalid glyph spec (a mistyped
|
|
1856
|
+
# characteristic, a bad tolerance) must drop THIS item with a warning — never crash
|
|
1857
|
+
# the whole drawing build. The helper raises on a bad spec; catch it at the measure
|
|
1858
|
+
# (the first build) and drop. `_build` below re-runs `_gdt_glyph` with the same args
|
|
1859
|
+
# (so a spec error can't reappear there) AND is made total against the OTHER raise
|
|
1860
|
+
# source — a zero-length Leader shaft (see the min-leader guard in `_build`).
|
|
1861
|
+
try:
|
|
1862
|
+
gb = _gdt_glyph(item, draft).bounding_box().size
|
|
1863
|
+
except Exception as e: # noqa: BLE001 — any glyph-spec error drops one item, not the build
|
|
1864
|
+
dwg._record_build_issue(
|
|
1865
|
+
"warning", "gdt_dropped", f"{name}: cannot render ({type(e).__name__}: {e})"
|
|
1866
|
+
)
|
|
1867
|
+
continue
|
|
1868
|
+
size = (gb.X, gb.Y)
|
|
1869
|
+
|
|
1870
|
+
def _build(pos, _px=px, _py=py, _hz=horizontal, _it=item):
|
|
1871
|
+
g = _gdt_glyph(_it, draft)
|
|
1872
|
+
tip = (_px, _py)
|
|
1873
|
+
# A zero-length leader shaft (the projected site coincides with the solved tier —
|
|
1874
|
+
# `pos == py` above/below, `pos == px` left/right) makes OCC's edge builder raise,
|
|
1875
|
+
# which would crash the whole build on a public-IR declaration. Guarantee a
|
|
1876
|
+
# minimum shaft along the stacking axis (nudge outward; 0.05 mm is invisible) so
|
|
1877
|
+
# `_build` is total — the drop-don't-crash invariant holds for every build() call.
|
|
1878
|
+
if _hz:
|
|
1879
|
+
dy = pos - _py
|
|
1880
|
+
pos = (
|
|
1881
|
+
pos if abs(dy) >= _MIN_LEADER else _py + math.copysign(_MIN_LEADER, dy or 1.0)
|
|
1882
|
+
)
|
|
1883
|
+
elbow = (_px, pos)
|
|
1884
|
+
else:
|
|
1885
|
+
dx = pos - _px
|
|
1886
|
+
pos = (
|
|
1887
|
+
pos if abs(dx) >= _MIN_LEADER else _px + math.copysign(_MIN_LEADER, dx or 1.0)
|
|
1888
|
+
)
|
|
1889
|
+
elbow = (pos, _py)
|
|
1890
|
+
return Leader(tip=tip, elbow=elbow, label="", draft=draft, callout=g)
|
|
1891
|
+
|
|
1892
|
+
def _drop(
|
|
1893
|
+
nm,
|
|
1894
|
+
_v=item.view,
|
|
1895
|
+
_s=item.side,
|
|
1896
|
+
_zones=zones,
|
|
1897
|
+
_px=px,
|
|
1898
|
+
_py=py,
|
|
1899
|
+
_hz=horizontal,
|
|
1900
|
+
_sz=size,
|
|
1901
|
+
_bld=_build,
|
|
1902
|
+
_feat=item.origin,
|
|
1903
|
+
_tb=tb_box,
|
|
1904
|
+
):
|
|
1905
|
+
# Fallthrough (#481): the declared/derived side is full — try the OPPOSITE side of
|
|
1906
|
+
# the same view before dropping, so a congested default still places somewhere
|
|
1907
|
+
# legible rather than vanishing. Best-effort (mirrors render_slots' below-fallthrough):
|
|
1908
|
+
# carve a free tier on the alternate strip and place there; carve_free_position only
|
|
1909
|
+
# ever sees already-placed annotations, so it can't collide with a not-yet-drained
|
|
1910
|
+
# sibling corridor. Force semantics (no corridor-cross check) match the primary path,
|
|
1911
|
+
# BUT reject a spot over the (not-yet-placed) title block — a below/right strip runs
|
|
1912
|
+
# into it, and carve can't see it (#481 review).
|
|
1913
|
+
alt = {"above": "below", "below": "above", "left": "right", "right": "left"}[_s]
|
|
1914
|
+
alt_strip = getattr(_zones, alt, None)
|
|
1915
|
+
if alt_strip is not None:
|
|
1916
|
+
axis2 = "y" if alt in ("above", "below") else "x"
|
|
1917
|
+
extent = _sz[1] if axis2 == "y" else _sz[0] # the glyph's stacking-axis size
|
|
1918
|
+
perp = (_px, _px + _sz[0]) if _hz else (_py - _sz[1] / 2, _py + _sz[1] / 2)
|
|
1919
|
+
pos = carve_free_position(dwg, alt_strip, _v, axis2, max(tier, extent), perp)
|
|
1920
|
+
if pos is not None:
|
|
1921
|
+
dim = _bld(pos)
|
|
1922
|
+
if not _box_hits(_anno_box(dim), (_tb,)): # clear of the title block
|
|
1923
|
+
dwg.add(dim, nm, view=_v, feature=_feat) # placed on the alternate side
|
|
1924
|
+
return
|
|
1925
|
+
dwg._record_build_issue(
|
|
1926
|
+
"warning",
|
|
1927
|
+
"gdt_dropped",
|
|
1928
|
+
f"{nm} not placed (no room in the {_v} {_s} strip or its opposite)",
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
register_corridor(
|
|
1932
|
+
dwg,
|
|
1933
|
+
(item.view, item.side),
|
|
1934
|
+
strip,
|
|
1935
|
+
item.view,
|
|
1936
|
+
axis,
|
|
1937
|
+
tier,
|
|
1938
|
+
CorridorCandidate(
|
|
1939
|
+
name=name,
|
|
1940
|
+
build=_build,
|
|
1941
|
+
order=(_GDT_SUBCHAIN, px if horizontal else py, name),
|
|
1942
|
+
on_place=lambda nm: None,
|
|
1943
|
+
on_drop=_drop,
|
|
1944
|
+
dedup=None,
|
|
1945
|
+
precedence=0,
|
|
1946
|
+
# A declared frame has no alternate view — force-keep (policy B) rather than
|
|
1947
|
+
# drop a user-authored annotation; only a physically full strip drops.
|
|
1948
|
+
force=True,
|
|
1949
|
+
feature=item.origin, # provenance (ADR 0010): the decorated feature
|
|
1950
|
+
size=size,
|
|
1951
|
+
# Even a force-kept frame must not stack into the title block (#481 review) —
|
|
1952
|
+
# place_strip_candidates rejects a placement hitting this box, then on_drop's
|
|
1953
|
+
# fallthrough tries the other side.
|
|
1954
|
+
forbid=tb_box,
|
|
1955
|
+
),
|
|
1956
|
+
)
|
|
1957
|
+
n += 1
|
|
1958
|
+
return n
|
|
@@ -39,6 +39,7 @@ from draftwright.annotations.from_model import (
|
|
|
39
39
|
render_centermarks,
|
|
40
40
|
render_diameters,
|
|
41
41
|
render_envelope,
|
|
42
|
+
render_gdt,
|
|
42
43
|
render_height_ladder,
|
|
43
44
|
render_locations,
|
|
44
45
|
render_pmi,
|
|
@@ -59,8 +60,10 @@ from draftwright.annotations.sections import (
|
|
|
59
60
|
feature_holes_of,
|
|
60
61
|
)
|
|
61
62
|
from draftwright.model import (
|
|
63
|
+
Frame,
|
|
62
64
|
HoleFeature,
|
|
63
65
|
PatternFeature,
|
|
66
|
+
RotationalFeature,
|
|
64
67
|
build_part_model,
|
|
65
68
|
plan_dimensions,
|
|
66
69
|
plan_sections,
|
|
@@ -130,6 +133,20 @@ def build_model(a: Analysis):
|
|
|
130
133
|
)
|
|
131
134
|
|
|
132
135
|
|
|
136
|
+
def build_rotational_feature(a: Analysis):
|
|
137
|
+
"""The rotational furniture feature (OD + centrelines + concentric bores) for *a*,
|
|
138
|
+
or ``None`` when the part isn't rotational. Mirrors the ``rotational`` branch of
|
|
139
|
+
:func:`build_model` / ``detect.build_part_model`` (detect.py) so the declared-model
|
|
140
|
+
path can synthesise the same furniture detection produces: a declared turned shaft
|
|
141
|
+
otherwise renders with no centrelines and its OD as a leader, not a dimension (#472).
|
|
142
|
+
Concentric bores are a Z-axis construction (as in :func:`build_model`)."""
|
|
143
|
+
if not a.is_rotational or a.od_diam is None:
|
|
144
|
+
return None
|
|
145
|
+
bores = tuple(_concentric_bore_diams(a)) if a.od_axis == "z" else ()
|
|
146
|
+
c = a.part.bounding_box().center()
|
|
147
|
+
return RotationalFeature(frame=Frame((c.X, c.Y, c.Z), a.od_axis), od=a.od_diam, bores=bores)
|
|
148
|
+
|
|
149
|
+
|
|
133
150
|
def _declared_feature_keys(groups, a: Analysis) -> set:
|
|
134
151
|
"""The :class:`HoleRef` position keys of every DECLARED hole/pattern member (ADR 0011
|
|
135
152
|
#448), so a caller-declared hole/pattern renders at its declared position even where
|
|
@@ -333,6 +350,11 @@ def _auto_annotate(dwg, a: Analysis, *, detail_view: bool = False):
|
|
|
333
350
|
# after every hole/diameter pass so it claims strip space last.
|
|
334
351
|
render_slots(dwg, _model, a)
|
|
335
352
|
|
|
353
|
+
# Declared GD&T frames / datum symbols / surface finishes (ADR 0011 §4, #61) — register
|
|
354
|
+
# into the same strips as first-class candidates BEFORE the drain, so the one solve orders
|
|
355
|
+
# and spaces them crossing-free with the dims (not a leftover first-fit like render_pmi).
|
|
356
|
+
render_gdt(dwg, _model, a)
|
|
357
|
+
|
|
336
358
|
# Now every feeder pass (locations + slots) has registered; solve each shared above
|
|
337
359
|
# corridor once (ADR 0009 end state, #345/#346) — dedup coincident spans, order the
|
|
338
360
|
# ladder — BEFORE detail views and PMI, so they see the placed ladder as an obstacle.
|
|
@@ -36,7 +36,7 @@ from draftwright._core import (
|
|
|
36
36
|
_tb_width,
|
|
37
37
|
)
|
|
38
38
|
from draftwright.analysis import _analyse
|
|
39
|
-
from draftwright.annotate import _auto_annotate, build_model
|
|
39
|
+
from draftwright.annotate import _auto_annotate, build_model, build_rotational_feature
|
|
40
40
|
from draftwright.annotations.sections import feature_hole_keys
|
|
41
41
|
from draftwright.drawing import Drawing
|
|
42
42
|
from draftwright.fonts import PLEX_MONO
|
|
@@ -263,6 +263,17 @@ def _assemble(a, out, assembly, detail_view, auto_dims, model=None, decorations=
|
|
|
263
263
|
dwg._part_model = (
|
|
264
264
|
_coerce_model(model, a.part, decorations) if model is not None else build_model(a)
|
|
265
265
|
)
|
|
266
|
+
if model is not None:
|
|
267
|
+
# A declared model skips detection, so a turned shaft carries no RotationalFeature —
|
|
268
|
+
# and that feature is the sole driver of the turned-axis centrelines + the OD dimension
|
|
269
|
+
# (rot furniture). Synthesise it from the (unconditional) analysis so a declared /
|
|
270
|
+
# emitted-script turned part reproduces the detected drawing (#472). Gated on the
|
|
271
|
+
# caller not having declared one, so an explicit choice wins.
|
|
272
|
+
pm = dwg._part_model
|
|
273
|
+
if not any(f.kind == "rotational" for f in pm.features):
|
|
274
|
+
rot = build_rotational_feature(a)
|
|
275
|
+
if rot is not None:
|
|
276
|
+
dwg._part_model = replace(pm, features=[*pm.features, rot])
|
|
266
277
|
dwg._model_declared = model is not None # ADR 0011 #448: gate model-driven hole render
|
|
267
278
|
|
|
268
279
|
part_s = a.part.scale(a.SCALE)
|
|
@@ -21,7 +21,17 @@ width/depth, slots) and by the lint coverage checks. Remaining engine passes
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
-
from draftwright.model.declare import
|
|
24
|
+
from draftwright.model.declare import (
|
|
25
|
+
boss,
|
|
26
|
+
control_frame,
|
|
27
|
+
datum,
|
|
28
|
+
envelope,
|
|
29
|
+
finish,
|
|
30
|
+
hole,
|
|
31
|
+
pattern,
|
|
32
|
+
slot,
|
|
33
|
+
step,
|
|
34
|
+
)
|
|
25
35
|
from draftwright.model.detect import build_part_model
|
|
26
36
|
from draftwright.model.ir import (
|
|
27
37
|
BossFeature,
|
|
@@ -67,7 +77,10 @@ __all__ = [
|
|
|
67
77
|
"StepLevelFeature",
|
|
68
78
|
"build_part_model",
|
|
69
79
|
"boss",
|
|
80
|
+
"control_frame",
|
|
81
|
+
"datum",
|
|
70
82
|
"envelope",
|
|
83
|
+
"finish",
|
|
71
84
|
"hole",
|
|
72
85
|
"pattern",
|
|
73
86
|
"slot",
|