draftwright 0.3.8__tar.gz → 0.3.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.3.8 → draftwright-0.3.9}/CHANGELOG.md +68 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/PKG-INFO +10 -2
- {draftwright-0.3.8 → draftwright-0.3.9}/README.md +9 -1
- {draftwright-0.3.8 → draftwright-0.3.9}/pyproject.toml +1 -1
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/_core.py +8 -7
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/analysis.py +4 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/annotations/from_model.py +93 -36
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/annotations/holes.py +219 -5
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/annotations/orchestrator.py +27 -6
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/annotations/sections.py +31 -20
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/builder.py +17 -2
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/drawing.py +97 -36
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/model/__init__.py +8 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/model/declare.py +258 -5
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/model/detect.py +152 -3
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/model/ir.py +79 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/model/planner.py +8 -1
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/projection.py +39 -2
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/__init__.py +18 -1
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/_features.py +42 -21
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/slots.py +405 -35
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/sheet.py +114 -1
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/sheet_emit.py +71 -2
- draftwright-0.3.9/tests/fixtures/tuner_jig_blind_obround_pockets.step +4109 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_declare.py +111 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_drawing_encapsulation.py +51 -20
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_gdt_placement.py +38 -9
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_make_drawing.py +59 -0
- draftwright-0.3.9/tests/test_pocket_pattern.py +259 -0
- draftwright-0.3.9/tests/test_pocket_pattern_recognition.py +191 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_recogniser_contract.py +56 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_render_seam.py +4 -4
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_sheet_notes.py +35 -0
- draftwright-0.3.9/tests/test_sheet_section.py +103 -0
- draftwright-0.3.9/tests/test_slot_pattern.py +151 -0
- draftwright-0.3.9/tests/test_slot_pattern_recognition.py +181 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/.gitignore +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/LICENSE +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/docs/adr/README.md +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/skills/SKILL.md +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/__init__.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/_geometry.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/annotate.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/annotations/_common.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/annotations/balloons.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/cli.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/compose.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/export.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/fits.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/intents.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/layout.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/pmi.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/_record.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/chamfers.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/countersinks.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/fillets.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/flats.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/grooves.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/plates.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/registry.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/repair.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/score.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/src/draftwright/sheet_dsl.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/_kernel.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/_layout_sig.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/grm03_thumbwheel_drive_screw.step +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/bracket_section.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/centered_rebate.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/chamfered.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/filleted.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/flange_dense.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/grid_plate.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/grooved_shaft.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/hex_bar.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/holed_slot.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/pocketed.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/prismatic_ladder.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/scattered_plate.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/side_drilled.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/refactor_golden/turned_stepped.json +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_agents_guide.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_architecture_docs.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_carve_free_position_callers.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_dense_sheet_canary.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_detect_once.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_detect_registry.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_export_dxf_zoom.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_fillets_adjacency.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_fits.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_import_boundaries.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_layout.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_layout_hypothesis.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_layout_property.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_lint_box_cache.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_lint_reconciliation.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_lint_structural.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_linting.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_note_verb.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_object_aspects.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_occupancy_boxes.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_part_model.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_pitch_dim_footprint.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_pmi.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_private_test_attr_reads.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_private_test_imports.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_recognition.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_refactor_golden.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_registry.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_score.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_script_detail_parity.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_sheet_emit.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_sheet_of.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_sheet_tables.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_solve_trace.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_strip_layout.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_tolerances.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_turned_steps.py +0 -0
- {draftwright-0.3.8 → draftwright-0.3.9}/tests/test_witness_label_reconciliation.py +0 -0
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.9 — 2026-07-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Repeated recesses are dimensioned as one pattern, not N competing callouts**
|
|
8
|
+
(#841): identical blind pockets and identical through-slots now collapse to a single
|
|
9
|
+
grouped `5× 7.9 × 13.6 × 19 DEEP` / `4× SLOT 8 × 30` leader plus `(n-1)× pitch`
|
|
10
|
+
dim(s). Previously each member competed for its own size dims and some silently
|
|
11
|
+
dropped for want of strip room — five declared slots rendered only three length dims.
|
|
12
|
+
Both new kinds (`PocketPatternFeature`, `SlotPatternFeature`) round-trip every
|
|
13
|
+
surface: declare (`sheet.pocket_pattern(...)` / `sheet.slot_pattern(...)`),
|
|
14
|
+
recognition from a real solid (coplanar, same-facing members only), the emitted
|
|
15
|
+
Sheet script, and the editable surface (`dwg.callout(feature)` + `finalize()`).
|
|
16
|
+
- **`Sheet.section()` and `Sheet.detail()` — ask for the view you need** (#841/#847):
|
|
17
|
+
a section A–A auto-fires only for a Z-axis hole with a counterbore, spotface, or
|
|
18
|
+
blind bottom, so a blind pocket's floor and depth stayed hidden-line-only with no
|
|
19
|
+
supported way to request a cut. `sheet.section(feature)` / `section(at=y)` /
|
|
20
|
+
bare `section()` force one (the cut plane is validated to lie strictly inside the
|
|
21
|
+
part), and `sheet.detail()` exposes the existing `detail_view=True` opt-in as a
|
|
22
|
+
chainable verb.
|
|
23
|
+
- **External threads are a first-class declared aspect** (#859): the turned analog of
|
|
24
|
+
the #764 internal thread — `sheet.step(shaft).thread("M3x0.5")`,
|
|
25
|
+
`sheet.diameter(...).thread(...)`, and `sheet.boss(...).thread(...)` append the
|
|
26
|
+
spec to the ⌀ callout (`ø3 M3x0.5`) instead of needing a free-text `.note()`. It
|
|
27
|
+
composes with `.finish()` for Ra-on-thread, keys the callout bucket on `(⌀, thread)`
|
|
28
|
+
so a threaded and a plain ø6 stay distinct, and now round-trips through the Sheet
|
|
29
|
+
emitter — including pattern-member holes, closing the symmetric latent #764 gap.
|
|
30
|
+
Declaration-only: a plain cylinder is geometrically indistinguishable from a
|
|
31
|
+
threaded one, so there is no recogniser. *(Known limitation: the incremental
|
|
32
|
+
`callout`/`finalize(only=…)` paths still dedup thread-blind — #863.)*
|
|
33
|
+
- **Blind obround pocket recognition, including imported STEP** (#837): the blind
|
|
34
|
+
counterpart of the #816 through-slot work. A stubby floored obround has side walls
|
|
35
|
+
too short to pair, so it is recovered from its semicircular end caps — clustering
|
|
36
|
+
faces by axis proximity so the quarter-cylinder split a STEP importer commonly
|
|
37
|
+
produces recognises as well as build123d's half-cylinder. A sealed internal void
|
|
38
|
+
(capped at both ends) is correctly *not* a pocket. A real tuner-jig STEP with five
|
|
39
|
+
blind pockets ships as a fixture.
|
|
40
|
+
- **`sheet.slot(...).note(...)` / `sheet.pocket(...).note(...)`** (#841/#845): the
|
|
41
|
+
slot/pocket handle now anchors a note to its own feature, with no `ref=` needed
|
|
42
|
+
(an explicit `ref` still forwards, as before).
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **An anchored note, GD&T frame, or finish relaxes its side instead of silently
|
|
47
|
+
vanishing** (#841/#855): an annotation declared with an explicit `view=`/`side=`
|
|
48
|
+
used to drop with only a warning when that strip was full, while the export still
|
|
49
|
+
reported success. The requested side is now a *preference* — when the strip has no
|
|
50
|
+
room the placer tries the opposite side, then the two perpendicular sides, taking
|
|
51
|
+
the first with room (via the same bounds and title-block checks, so it never
|
|
52
|
+
overshoots). A relaxed placement records an INFO `gdt_side_relaxed` issue naming
|
|
53
|
+
requested versus actual. Only when no strip fits does it drop.
|
|
54
|
+
|
|
55
|
+
### Internal
|
|
56
|
+
|
|
57
|
+
- **The state-bus endgame is closed to its permanent seam** (#830/#840): the
|
|
58
|
+
solve trace is filled into `BuildState` at the one construction site, the redundant
|
|
59
|
+
part-model re-attach is gone, and the detail view is now *transactional* — geometry
|
|
60
|
+
is projected at the detail scale and committed only if dimensions land, so
|
|
61
|
+
`_drop_view_coordinates` has zero engine callers. With those removed, the #817
|
|
62
|
+
decision-D method-call exemption narrows from a blanket pass to a named
|
|
63
|
+
`_LAYOUT_SEAM` allowlist (`_add_view`, `_set_view_coordinates`), accepted as the
|
|
64
|
+
permanent interactive-layout seam; every other private-method call on the drawing
|
|
65
|
+
is now flagged.
|
|
66
|
+
- **A draftwright logo set** (#843) — drafting-idiom mark, all text outlined so the
|
|
67
|
+
SVGs render font-independently; the README now leads with the lockup.
|
|
68
|
+
- **A worked multi-feature object-reference example** (#853) in
|
|
69
|
+
`docs/multi-feature-object-reference-workflow.md`.
|
|
70
|
+
|
|
3
71
|
## v0.3.8 — 2026-07-23
|
|
4
72
|
|
|
5
73
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -694,7 +694,9 @@ Requires-Dist: typer>=0.12
|
|
|
694
694
|
Requires-Dist: typing-extensions>=4.5; python_full_version < '3.13'
|
|
695
695
|
Description-Content-Type: text/markdown
|
|
696
696
|
|
|
697
|
-
|
|
697
|
+
<h1 align="center">
|
|
698
|
+
<img src="docs/images/draftwright-logo.png" alt="draftwright" width="460">
|
|
699
|
+
</h1>
|
|
698
700
|
|
|
699
701
|
[](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml)
|
|
700
702
|
[](https://codecov.io/gh/pzfreo/draftwright)
|
|
@@ -832,6 +834,12 @@ sheet.hole(features.m3_bore).thread("M3x0.5").finish("1.6") # tapped + Ra on t
|
|
|
832
834
|
An object-sourced script carries an inline tip pointing at exactly this edit; a STEP-sourced
|
|
833
835
|
script keeps the detected numbers (there's no object to reference).
|
|
834
836
|
|
|
837
|
+
See [`docs/multi-feature-object-reference-workflow.md`](docs/multi-feature-object-reference-workflow.md)
|
|
838
|
+
for a complete, runnable walkthrough of this pattern on a multi-feature turned part —
|
|
839
|
+
including the backward-compatible refactor, external threads (no `.thread()` on a step —
|
|
840
|
+
use `.note(...)`), and why an object-sourced script should point at the cheap geometry
|
|
841
|
+
variant of your source.
|
|
842
|
+
|
|
835
843
|
## What it produces
|
|
836
844
|
|
|
837
845
|
- **Three orthographic views** (front, plan, side) sized and scaled automatically to the
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img src="docs/images/draftwright-logo.png" alt="draftwright" width="460">
|
|
3
|
+
</h1>
|
|
2
4
|
|
|
3
5
|
[](https://github.com/pzfreo/draftwright/actions/workflows/ci.yml)
|
|
4
6
|
[](https://codecov.io/gh/pzfreo/draftwright)
|
|
@@ -136,6 +138,12 @@ sheet.hole(features.m3_bore).thread("M3x0.5").finish("1.6") # tapped + Ra on t
|
|
|
136
138
|
An object-sourced script carries an inline tip pointing at exactly this edit; a STEP-sourced
|
|
137
139
|
script keeps the detected numbers (there's no object to reference).
|
|
138
140
|
|
|
141
|
+
See [`docs/multi-feature-object-reference-workflow.md`](docs/multi-feature-object-reference-workflow.md)
|
|
142
|
+
for a complete, runnable walkthrough of this pattern on a multi-feature turned part —
|
|
143
|
+
including the backward-compatible refactor, external threads (no `.thread()` on a step —
|
|
144
|
+
use `.note(...)`), and why an object-sourced script should point at the cheap geometry
|
|
145
|
+
variant of your source.
|
|
146
|
+
|
|
139
147
|
## What it produces
|
|
140
148
|
|
|
141
149
|
- **Three orthographic views** (front, plan, side) sized and scaled automatically to the
|
|
@@ -604,13 +604,14 @@ class DetailRequest:
|
|
|
604
604
|
axis: part axis the band spans / is cropped along ("x"/"y"/"z").
|
|
605
605
|
lo, hi: band bounds along ``axis`` (world mm).
|
|
606
606
|
scale_needed: detail world→page scale that makes the region legible.
|
|
607
|
-
redraw: ``redraw(dwg, view_name, detail_scale) -> int`` — draws the
|
|
608
|
-
detail's dimensions
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
607
|
+
redraw: ``redraw(dwg, view_name, coords, detail_scale) -> int`` — draws the
|
|
608
|
+
detail's dimensions against *coords* (the detail's
|
|
609
|
+
:class:`ViewCoordinates`, mapped via ``coords.pp``) and returns the count
|
|
610
|
+
placed (0 → the detailer drops the detail without committing a view, #840).
|
|
611
|
+
Called on the SCRATCH-projected coords before the view is committed, so a
|
|
612
|
+
zero result needs no rollback; the main view always carries the located
|
|
613
|
+
head/block inline regardless, so a placement failure loses no coverage
|
|
614
|
+
(lint reports the un-located interior instead).
|
|
614
615
|
pad_top: page-mm band reserved above the detail view (a horizontal
|
|
615
616
|
chain); reserved in the fit + placement.
|
|
616
617
|
pads: optional ``pads(detail_scale) -> (pad_right, pad_top)`` for a
|
|
@@ -93,6 +93,10 @@ def _will_section(model, *, is_rotational=False, cx=0.0, cy=0.0) -> bool:
|
|
|
93
93
|
|
|
94
94
|
if model is None:
|
|
95
95
|
return False
|
|
96
|
+
# An explicit Sheet.section() request (ADR 0011, #841) reserves the row even when no
|
|
97
|
+
# hole gate qualifies — a blind pocket's floor/depth section has no driving Z hole.
|
|
98
|
+
if getattr(model, "decorations", {}).get("section") is not None:
|
|
99
|
+
return True
|
|
96
100
|
features = getattr(model, "features", model)
|
|
97
101
|
|
|
98
102
|
def feature_member(pt) -> bool:
|
|
@@ -778,14 +778,15 @@ def _diameter_row_below(dwg, items, start: int = 0, trace=None, *, ctx) -> int:
|
|
|
778
778
|
if ev is not None:
|
|
779
779
|
ev["items"].extend(
|
|
780
780
|
{"label": f"ø{_fmt(d)}", "outcome": "dropped", "reason": "no_room_below"}
|
|
781
|
-
for _, d, _, _ in items
|
|
781
|
+
for _, d, _, _, _ in items
|
|
782
782
|
)
|
|
783
783
|
return 0
|
|
784
784
|
specs = [] # (tip_page, dia, label, feature), tip on the step's bottom silhouette,
|
|
785
|
-
for anchor, dia, feat, dtol in items: # centred along the feature's length (not a corner)
|
|
785
|
+
for anchor, dia, feat, dtol, thr in items: # centred along the feature's length (not a corner)
|
|
786
786
|
ax, ay, az = anchor
|
|
787
787
|
tip = dwg.at("front", ax, ay, az - dia / 2)
|
|
788
|
-
|
|
788
|
+
label = f"ø{_fmt(dia)}{_tol_suffix(dtol, draft)}" + (f" {thr}" if thr else "") # #859
|
|
789
|
+
specs.append((tip, dia, label, feat))
|
|
789
790
|
# Real measured width, not the per-char estimate: helpers >=0.14 label boxes are
|
|
790
791
|
# honest about the rendered string, so an underestimated min_gap here surfaces as a
|
|
791
792
|
# visible annotation_overlap between adjacent labels (hypothesis tier).
|
|
@@ -811,7 +812,7 @@ def _diameter_row_below(dwg, items, start: int = 0, trace=None, *, ctx) -> int:
|
|
|
811
812
|
# when direction-aware label intervals actually collide, enforce elbow ≥ tip with
|
|
812
813
|
# a left-to-right min_gap cascade; overflow drops the smallest ø (#298) and
|
|
813
814
|
# re-solves. No-op for ordinary rows.
|
|
814
|
-
_SHELF =
|
|
815
|
+
_SHELF = draft.pad_around_text # helpers Leader shelf_len = gap = draft.pad_around_text
|
|
815
816
|
|
|
816
817
|
def _label_ivals(svs, positions):
|
|
817
818
|
out = []
|
|
@@ -882,24 +883,36 @@ def _diameter_column_left(dwg, items, start: int = 0, trace=None, *, ctx) -> int
|
|
|
882
883
|
ev = trace.pass_event("diameter_column_left", view="front") if trace is not None else None
|
|
883
884
|
draft = dwg.draft
|
|
884
885
|
fx0, fy0, _, fy1 = dwg.view_bounds("front")
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
886
|
+
# Real measured width, not the per-char estimate (#859): a thread spec is arbitrary text,
|
|
887
|
+
# so `len * 0.62 em` can underestimate a wide label and let it cross the margin
|
|
888
|
+
# (annotation_out_of_bounds). Measure the completed label like the row-below path does.
|
|
889
|
+
label_w = max(
|
|
890
|
+
_text_size(
|
|
891
|
+
f"ø{_fmt(dia)}{_tol_suffix(dtol, draft)}" + (f" {thr}" if thr else ""),
|
|
892
|
+
draft.font_size,
|
|
893
|
+
getattr(draft, "font_path", DEFAULT_FONT_PATH),
|
|
894
|
+
getattr(draft, "font", "Arial"),
|
|
895
|
+
)[0]
|
|
896
|
+
for _, dia, _, dtol, thr in items
|
|
889
897
|
)
|
|
890
898
|
elbow_x = fx0 - (draft.font_size + 2 * draft.pad_around_text)
|
|
891
|
-
|
|
899
|
+
# A left-directed leader hangs its label a shelf-length PAST the elbow, so the label's left
|
|
900
|
+
# edge sits at elbow_x - shelf - label_w; the guard must reserve the shelf or a near-boundary
|
|
901
|
+
# label overshoots the margin. The shelf is the helpers Leader's gap = draft.pad_around_text,
|
|
902
|
+
# not a fixed 2.0 (#859, Codex #862 r4/r5).
|
|
903
|
+
if elbow_x - draft.pad_around_text - label_w < _MARGIN:
|
|
892
904
|
if ev is not None:
|
|
893
905
|
ev["items"].extend(
|
|
894
906
|
{"label": f"ø{_fmt(d)}", "outcome": "dropped", "reason": "no_room_left"}
|
|
895
|
-
for _, d, _, _ in items
|
|
907
|
+
for _, d, _, _, _ in items
|
|
896
908
|
)
|
|
897
909
|
return 0
|
|
898
910
|
specs = [] # (tip_page, dia, label, feature), tip on the step's left silhouette,
|
|
899
|
-
for anchor, dia, feat, dtol in items: # centred along the feature's length (not a corner)
|
|
911
|
+
for anchor, dia, feat, dtol, thr in items: # centred along the feature's length (not a corner)
|
|
900
912
|
ax, ay, az = anchor
|
|
901
913
|
tip = dwg.at("front", ax - dia / 2, ay, az)
|
|
902
|
-
|
|
914
|
+
label = f"ø{_fmt(dia)}{_tol_suffix(dtol, draft)}" + (f" {thr}" if thr else "") # #859
|
|
915
|
+
specs.append((tip, dia, label, feat))
|
|
903
916
|
half_h = draft.font_size / 2 + draft.pad_around_text
|
|
904
917
|
min_gap = 2 * half_h
|
|
905
918
|
# Place what fits; drop the smallest ø first, never the whole column (#298).
|
|
@@ -1006,24 +1019,30 @@ def render_diameters(dwg, groups, tol: float = 0.15, *, ctx, only=None) -> int:
|
|
|
1006
1019
|
if dpd is None:
|
|
1007
1020
|
continue
|
|
1008
1021
|
dia = dpd.param.value
|
|
1009
|
-
|
|
1022
|
+
# An EXTERNAL thread (#859) makes a distinct callout: a threaded ⌀6 ("ø6 M6x1") and a
|
|
1023
|
+
# plain ⌀6 are NOT the same label, so the bucket keys on (⌀, thread) — this never drops a
|
|
1024
|
+
# thread on a shared ⌀, and a threadless model keys every entry on (⌀, None) exactly as
|
|
1025
|
+
# before (byte-identical). entry = [anchor, dia, {features}, ± tolerance, thread]. A
|
|
1026
|
+
# callout is per (axis, ⌀, thread); the first authored tolerance on a shared ⌀ wins.
|
|
1027
|
+
thr = getattr(g.feature, "thread", None)
|
|
1028
|
+
# A coincident plain ⌀ already drawn (a bore, another step) dedups only an UNTHREADED ⌀;
|
|
1029
|
+
# a threaded ⌀ is a distinct callout, so a bare ⌀8 mention must not suppress ø8 M8x1.25.
|
|
1030
|
+
if thr is None and any(abs(dia - m) <= tol for m in mentioned):
|
|
1010
1031
|
continue
|
|
1011
1032
|
bucket = {"x": row_buckets, "z": col_buckets}.get(g.feature.frame.axis)
|
|
1012
1033
|
if bucket is None:
|
|
1013
1034
|
continue
|
|
1014
|
-
dkey = round(dia, 2)
|
|
1015
1035
|
dtol = dpd.param.tolerance
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
entry = bucket.setdefault(dkey, [g.anchor, dia, set(), dtol])
|
|
1036
|
+
dkey = (round(dia, 2), thr)
|
|
1037
|
+
entry = bucket.setdefault(dkey, [g.anchor, dia, set(), dtol, thr])
|
|
1019
1038
|
entry[2].add(g.feature)
|
|
1020
1039
|
if entry[3] is None:
|
|
1021
1040
|
entry[3] = dtol
|
|
1022
1041
|
|
|
1023
1042
|
def _items(buckets):
|
|
1024
1043
|
return [
|
|
1025
|
-
(_diameter_step_anchor(a, fs), d, next(iter(fs)) if len(fs) == 1 else None, t)
|
|
1026
|
-
for a, d, fs, t in buckets.values()
|
|
1044
|
+
(_diameter_step_anchor(a, fs), d, next(iter(fs)) if len(fs) == 1 else None, t, thr)
|
|
1045
|
+
for a, d, fs, t, thr in buckets.values()
|
|
1027
1046
|
]
|
|
1028
1047
|
|
|
1029
1048
|
# The placers name leaders m_dia_{x,z}{start+i} CONTIGUOUSLY from one start. The auto-pass
|
|
@@ -1541,6 +1560,13 @@ def _pocket_label(width, length, depth, wsfx="", lsfx="", dsfx="") -> str:
|
|
|
1541
1560
|
return f"{_fmt(width)}{wsfx} × {_fmt(length)}{lsfx} × {_fmt(depth)}{dsfx} DEEP"
|
|
1542
1561
|
|
|
1543
1562
|
|
|
1563
|
+
def _slot_label(width, length, wsfx="", lsfx="") -> str:
|
|
1564
|
+
"""The grouped slot-array callout string: ``SLOT {width} × {length}`` (#841). A slot has no
|
|
1565
|
+
depth, so — unlike :func:`_pocket_label` — there is no ``× depth DEEP``; the ``SLOT`` prefix
|
|
1566
|
+
names the feature the way a lone slot's linear dims otherwise would."""
|
|
1567
|
+
return f"SLOT {_fmt(width)}{wsfx} × {_fmt(length)}{lsfx}"
|
|
1568
|
+
|
|
1569
|
+
|
|
1544
1570
|
# Unit lead directions tried (nearest-clear wins), diagonals first so a central pocket's
|
|
1545
1571
|
# leader exits toward a corner (usually the emptiest margin) before an edge.
|
|
1546
1572
|
_POCKET_LEAD_DIRS = (
|
|
@@ -1730,8 +1756,11 @@ def render_boss_diameters(dwg, groups, a, *, ctx) -> int:
|
|
|
1730
1756
|
continue
|
|
1731
1757
|
dia = dpd.param.value
|
|
1732
1758
|
dtol = dpd.param.tolerance
|
|
1733
|
-
|
|
1734
|
-
|
|
1759
|
+
thr = getattr(b, "thread", None) # external thread appends to the OD callout (#859)
|
|
1760
|
+
# A coincident plain ⌀ dedups only an UNTHREADED boss; a threaded ⌀ is a distinct callout,
|
|
1761
|
+
# so a bare ⌀8 mention (a bore, a step) must not suppress ø8 M8x1.25 (#859).
|
|
1762
|
+
if thr is None and any(abs(dia - m) <= 0.15 for m in mentioned):
|
|
1763
|
+
continue
|
|
1735
1764
|
view = view_of.get(b.frame.axis)
|
|
1736
1765
|
if view is None:
|
|
1737
1766
|
continue
|
|
@@ -1743,7 +1772,7 @@ def render_boss_diameters(dwg, groups, a, *, ctx) -> int:
|
|
|
1743
1772
|
f"m_bossdia_{b.frame.axis}{bi}",
|
|
1744
1773
|
view,
|
|
1745
1774
|
vb,
|
|
1746
|
-
f"ø{_fmt(dia)}{_tol_suffix(dtol, draft)}",
|
|
1775
|
+
f"ø{_fmt(dia)}{_tol_suffix(dtol, draft)}" + (f" {thr}" if thr else ""),
|
|
1747
1776
|
# arrowhead on the boss circle's rim, not its centre
|
|
1748
1777
|
_radial_candidates(dwg, view, vb, b, reach, rim=dia / 2 * a.SCALE),
|
|
1749
1778
|
)
|
|
@@ -2298,9 +2327,15 @@ def render_step_lengths(dwg, groups, *, ctx, only=None) -> int:
|
|
|
2298
2327
|
(dwg.at("front", hlo, 0, 0), dwg.at("front", hhi, 0, 0), hhi - hlo, None)
|
|
2299
2328
|
)
|
|
2300
2329
|
|
|
2301
|
-
def _redraw(dwg, view, detail_scale, _hw=ra):
|
|
2330
|
+
def _redraw(dwg, view, coords, detail_scale, _hw=ra):
|
|
2302
2331
|
# View-scoped name prefix so two detail views never collide (#307 review).
|
|
2303
|
-
|
|
2332
|
+
# Map world→page against the detail coords (not a live dwg.at) so the view can be
|
|
2333
|
+
# committed only after these dims land — no place-then-roll-back (#840).
|
|
2334
|
+
def _at(x, y, z):
|
|
2335
|
+
px, py = coords.pp(x, y, z)
|
|
2336
|
+
return (px, py, 0.0)
|
|
2337
|
+
|
|
2338
|
+
hsegs = [(_at(*a), _at(*b), v, t) for a, b, v, t in _hw]
|
|
2304
2339
|
return _draw_step_chain(
|
|
2305
2340
|
dwg, view, hsegs, f"dim_{view}_steplen", detail_scale, ctx=ctx
|
|
2306
2341
|
)
|
|
@@ -3435,28 +3470,50 @@ def render_gdt(dwg, model, a, *, ctx) -> int:
|
|
|
3435
3470
|
_zones=_zones,
|
|
3436
3471
|
_px=_px,
|
|
3437
3472
|
_py=_py,
|
|
3438
|
-
_hz=_hz,
|
|
3439
3473
|
_sz=_sz,
|
|
3440
3474
|
_bld=_bld,
|
|
3441
3475
|
_feat=_feat,
|
|
3442
3476
|
_tb=_tb,
|
|
3443
3477
|
):
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3478
|
+
# Auto-relax the requested side (#841 outcome C): the requested strip is full, so
|
|
3479
|
+
# try the OPPOSITE side, then the two PERPENDICULAR sides, placing on the first
|
|
3480
|
+
# with room. A note the caller asked to see should appear somewhere legible
|
|
3481
|
+
# rather than vanish; when the requested strip has no room, an explicit `side=`
|
|
3482
|
+
# is a preference, not a hard constraint. A perpendicular side flips the leader
|
|
3483
|
+
# orientation (`_bld(pos, _hz=hz)`). If the placement lands on a side other than
|
|
3484
|
+
# requested, record an INFO issue so the relaxation is visible, not silent — the
|
|
3485
|
+
# #841 goal is that a requested annotation is never *silently* lost.
|
|
3486
|
+
relax_order = {
|
|
3487
|
+
"above": ("below", "right", "left"),
|
|
3488
|
+
"below": ("above", "right", "left"),
|
|
3489
|
+
"left": ("right", "above", "below"),
|
|
3490
|
+
"right": ("left", "above", "below"),
|
|
3491
|
+
}[_s]
|
|
3492
|
+
for alt in relax_order:
|
|
3493
|
+
alt_strip = getattr(_zones, alt, None)
|
|
3494
|
+
if alt_strip is None:
|
|
3495
|
+
continue
|
|
3496
|
+
hz = alt in ("above", "below") # perpendicular sides flip the leader axis
|
|
3497
|
+
axis2 = "y" if hz else "x"
|
|
3448
3498
|
extent = _sz[1] if axis2 == "y" else _sz[0] # the glyph's stacking-axis size
|
|
3449
|
-
perp = (_px, _px + _sz[0]) if
|
|
3499
|
+
perp = (_px, _px + _sz[0]) if hz else (_py - _sz[1] / 2, _py + _sz[1] / 2)
|
|
3450
3500
|
pos = carve_free_position(dwg, alt_strip, _v, axis2, max(tier, extent), perp)
|
|
3451
|
-
if pos is
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3501
|
+
if pos is None:
|
|
3502
|
+
continue
|
|
3503
|
+
dim = _bld(pos, _hz=hz)
|
|
3504
|
+
if _box_hits(_anno_box(dim), (_tb,)): # would overlap the title block — skip
|
|
3505
|
+
continue
|
|
3506
|
+
ctx.place(dim, nm, view=_v, feature=_feat) # relaxed side
|
|
3507
|
+
ctx.record_issue(
|
|
3508
|
+
"info",
|
|
3509
|
+
"gdt_side_relaxed",
|
|
3510
|
+
f"{nm}: the {_v} {_s} strip was full — placed on {alt} instead",
|
|
3511
|
+
)
|
|
3512
|
+
return
|
|
3456
3513
|
ctx.record_issue(
|
|
3457
3514
|
"warning",
|
|
3458
3515
|
"gdt_dropped",
|
|
3459
|
-
f"{nm} not placed (no room in
|
|
3516
|
+
f"{nm} not placed (no room in any {_v} strip)",
|
|
3460
3517
|
)
|
|
3461
3518
|
|
|
3462
3519
|
ctx.post_drain.append(_retry)
|