draftwright 0.3.1__tar.gz → 0.3.3__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.1 → draftwright-0.3.3}/CHANGELOG.md +89 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/PKG-INFO +2 -2
- {draftwright-0.3.1 → draftwright-0.3.3}/pyproject.toml +2 -2
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/_core.py +29 -16
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/analysis.py +5 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/annotations/_common.py +278 -20
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/annotations/from_model.py +306 -117
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/annotations/holes.py +23 -5
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/annotations/orchestrator.py +6 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/annotations/sections.py +20 -1
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/builder.py +15 -9
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/drawing.py +89 -15
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/export.py +27 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/linting/structural.py +102 -35
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/fillets.py +23 -11
- draftwright-0.3.3/src/draftwright/repair.py +198 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/centered_rebate.json +3 -3
- draftwright-0.3.3/tests/refactor_golden/grid_plate.json +474 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/holed_slot.json +2 -2
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/prismatic_ladder.json +3 -3
- draftwright-0.3.3/tests/refactor_golden/scattered_plate.json +649 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_carve_free_position_callers.py +14 -5
- draftwright-0.3.3/tests/test_detect_once.py +81 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_drawing_encapsulation.py +65 -0
- draftwright-0.3.3/tests/test_export_dxf_zoom.py +65 -0
- draftwright-0.3.3/tests/test_fillets_adjacency.py +53 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_layout_cleanliness.py +84 -53
- draftwright-0.3.3/tests/test_lint_box_cache.py +117 -0
- draftwright-0.3.3/tests/test_occupancy_boxes.py +39 -0
- draftwright-0.3.3/tests/test_pitch_dim_footprint.py +157 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_refactor_golden.py +40 -1
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_sheet_emit.py +7 -2
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_strip_layout.py +45 -10
- draftwright-0.3.3/tests/test_witness_label_reconciliation.py +56 -0
- draftwright-0.3.1/src/draftwright/repair.py +0 -91
- {draftwright-0.3.1 → draftwright-0.3.3}/.gitignore +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/LICENSE +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/README.md +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/skills/SKILL.md +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/__init__.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/_geometry.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/annotate.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/cli.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/compose.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/fits.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/intents.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/layout.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/model/declare.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/pmi.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/projection.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/_record.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/chamfers.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/countersinks.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/flats.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/grooves.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/plates.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/registry.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/score.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/sheet.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/sheet_dsl.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/src/draftwright/sheet_emit.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/_kernel.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/_layout_sig.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/bracket_section.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/chamfered.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/filleted.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/flange_dense.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/grooved_shaft.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/hex_bar.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/pocketed.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/side_drilled.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/refactor_golden/turned_stepped.json +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_declare.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_fits.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_gdt_placement.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_import_boundaries.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_layout.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_layout_hypothesis.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_layout_property.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_lint_reconciliation.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_lint_structural.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_linting.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_make_drawing.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_object_aspects.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_part_model.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_pmi.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_private_test_imports.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_recogniser_contract.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_recognition.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_registry.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_render_seam.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_score.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_sheet_notes.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_sheet_of.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_sheet_tables.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_tolerances.py +0 -0
- {draftwright-0.3.1 → draftwright-0.3.3}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.3 — 2026-07-18
|
|
4
|
+
|
|
5
|
+
The **layout-quality release**: the occupancy model gets honest about ink shape,
|
|
6
|
+
the last solver-invisible pass joins the corridor solve, and crossed labels shift
|
|
7
|
+
themselves clear. Also completes the #635 consolidation epic (ADR 0005 fully
|
|
8
|
+
executed; ADR 0009's no-invisible-occupant guarantee now holds by construction
|
|
9
|
+
for every auto-pass).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Witness-crossing label shifts** (#690): a dimension label crossed by another
|
|
14
|
+
dimension's witness stroke now shifts along its own line to clear it — a
|
|
15
|
+
deterministic post-drain pass using the repair machinery (destination-aware,
|
|
16
|
+
axis-aligned only, pinned dims exempt).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **L-shaped occupancy** (#685): placement obstacles decompose into per-stroke
|
|
21
|
+
boxes + label box (helpers ≥0.14 `segments`) instead of one hull, with
|
|
22
|
+
preset-aware arrowhead pads. View-corner contention between perpendicular
|
|
23
|
+
strips disappears by construction (a plate-thickness dim returns to its
|
|
24
|
+
natural strip); packing tightens. The cleanliness ratchet mirrors the model —
|
|
25
|
+
only transverse stroke crossings are exempt, so parallel overprints and label
|
|
26
|
+
contacts are detected honestly (twelve hull-artifact allowlist entries burned
|
|
27
|
+
down; the legitimate shared-datum running-dimension pairs are documented).
|
|
28
|
+
- **Height ladder joins the corridor solve** (#636): step heights + overall
|
|
29
|
+
height are corridor candidates (the leapfrog witness cursor survives as a
|
|
30
|
+
deterministic build-time chain; the overall height stacks outermost by
|
|
31
|
+
construction and packs tighter). An explicitly-requested crowded-step detail
|
|
32
|
+
view now *deliberately* demotes the overall-height dim when the sheet cannot
|
|
33
|
+
hold both (previously the carve dropped it silently); a Y-drilled hole's Z
|
|
34
|
+
location joins the front running ladder per the documented routing.
|
|
35
|
+
- **One typed `BuildState`** (#639): the drawing's build context (analysis,
|
|
36
|
+
part model, lint geometry caches) consolidates into a single object filled at
|
|
37
|
+
one builder site, single-writer-guarded; the render passes make zero private
|
|
38
|
+
`Drawing` reads (fail-closed empty allowlist).
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- A prismatic detail view no longer loses both the detail *and* the height dim
|
|
43
|
+
when space is tight (transactional demotion).
|
|
44
|
+
- The GD&T alternate-strip fallback defers until every corridor has drained, so
|
|
45
|
+
it can never preempt a sibling strip's reserved corner.
|
|
46
|
+
|
|
47
|
+
## v0.3.2 — 2026-07-18
|
|
48
|
+
|
|
49
|
+
The **performance release**: the #602 perf loop (six PRs) plus the
|
|
50
|
+
build123d-drafting-helpers 0.14 upgrade. A dimension-dense drawing now builds
|
|
51
|
+
roughly **14× faster** than v0.3.1 (grid-plate benchmark 40 s → 2.8 s; NIST
|
|
52
|
+
CTC-02 ~107 s → well under half); the full test tier dropped ~14 min → ~3.5 min.
|
|
53
|
+
|
|
54
|
+
### Performance
|
|
55
|
+
|
|
56
|
+
- **Placement measures, then builds** (#678, #679): corridor/strip candidates are
|
|
57
|
+
evaluated on analytical footprints (`dim_footprint`) or a single probe box; an
|
|
58
|
+
accepted candidate builds its OCC geometry exactly once and re-validates the
|
|
59
|
+
real box. Dimension constructions on the scattered-plate benchmark: 59 → 21
|
|
60
|
+
(= the placed count).
|
|
61
|
+
- **Detect once** (#682): the sizing model `_analyse` builds pre-scale is stored
|
|
62
|
+
on `Analysis.model` and reused as the render model — restoring ADR 0008
|
|
63
|
+
Amendment 5 ("one inventory, detected once"). ~13 s off CTC-02.
|
|
64
|
+
- **Fillet recognition de-quadratised** (#683): edge→faces adjacency map replaces
|
|
65
|
+
the O(faces²×edges²) `IsSame` sweep (3.7 M calls on CTC-02); 10.8 s → 4.6 s,
|
|
66
|
+
byte-identical records.
|
|
67
|
+
- **Lint bounding boxes memoised** (#681): `Drawing.lint()` 0.88 s → 0.07 s per
|
|
68
|
+
call (identity+location-token-checked cache beside the #143 view-edge cache).
|
|
69
|
+
- **DXF viewport from the known page window** (#680): skips `ezdxf.zoom.extents`'
|
|
70
|
+
per-entity walk; ~2.4 s per DXF export.
|
|
71
|
+
- **helpers 0.14** (#684): boolean-free dimension ink (upstream #177, filed and
|
|
72
|
+
fixed same-day) — ~25 ms per `Dimension`, was ~234 ms.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- **Tight-span dimensions now render honestly** (helpers 0.14): ISO outside
|
|
77
|
+
arrows where 0.13 silently degraded to a bare line. Ink extents grow on such
|
|
78
|
+
dims; placement models the flip analytically. Two placement-golden fixtures
|
|
79
|
+
re-blessed (ink extent only).
|
|
80
|
+
- Diameter-row leader labels space by **measured text width** and repair
|
|
81
|
+
helpers' shelf-flip collisions (crowded rows only; ordinary rows unchanged).
|
|
82
|
+
- A plate-thickness dim whose home strip corner is structurally contested falls
|
|
83
|
+
through to the opposite strip / side view instead of dropping (#684; the
|
|
84
|
+
underlying one-box-occupancy artifact is tracked as #685).
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- `recognise_fillets` neighbour tie-break is order-independent (#683 review).
|
|
89
|
+
- Lint's annotation-box cache prunes departed objects and detects in-place
|
|
90
|
+
relocation (#681 review).
|
|
91
|
+
|
|
3
92
|
## v0.3.1 — 2026-07-17
|
|
4
93
|
|
|
5
94
|
Also lands the **#635 consolidation epic** (P1–P6, all six children closed): the render
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -682,7 +682,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
682
682
|
Classifier: Programming Language :: Python :: 3.14
|
|
683
683
|
Classifier: Topic :: Scientific/Engineering
|
|
684
684
|
Requires-Python: <3.15,>=3.10
|
|
685
|
-
Requires-Dist: build123d-drafting-helpers>=0.
|
|
685
|
+
Requires-Dist: build123d-drafting-helpers>=0.14.0
|
|
686
686
|
Requires-Dist: build123d<0.11,>=0.9.0; python_full_version < '3.13'
|
|
687
687
|
Requires-Dist: build123d<0.12,>=0.11; python_full_version >= '3.13'
|
|
688
688
|
Requires-Dist: numpy>=1.24
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "draftwright"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Automated technical-drawing generation for build123d"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
# 3.13+. Mirrors pzfreo/build123d-mcp's dependency handling.
|
|
18
18
|
"build123d>=0.9.0,<0.11 ; python_full_version < '3.13'",
|
|
19
19
|
"build123d>=0.11,<0.12 ; python_full_version >= '3.13'",
|
|
20
|
-
"build123d-drafting-helpers>=0.
|
|
20
|
+
"build123d-drafting-helpers>=0.14.0",
|
|
21
21
|
"numpy>=1.24",
|
|
22
22
|
# PNG raster export (SVG→PDF→PNG). Both permissively licensed (Pillow HPND; pypdfium2
|
|
23
23
|
# Apache-2.0 wrapping Google PDFium BSD-3) and ship pre-built wheels with NO native system
|
|
@@ -107,8 +107,10 @@ _QUOTED_RE = re.compile(r"'([^']*)'")
|
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
@functools.lru_cache(maxsize=512)
|
|
110
|
-
def
|
|
111
|
-
|
|
110
|
+
def _text_size(
|
|
111
|
+
text: str, font_size: float, font_path: str | None = PLEX_MONO, font: str = "Arial"
|
|
112
|
+
) -> tuple[float, float]:
|
|
113
|
+
"""Measured rendered (width, height) (page-mm) of *text* at *font_size*.
|
|
112
114
|
|
|
113
115
|
Uses build123d's ``Text`` — the same primitive ``Dimension``/``HoleCallout``
|
|
114
116
|
stroke their labels with — so callout-width estimates use real glyph metrics
|
|
@@ -116,22 +118,27 @@ def _text_width(text: str, font_size: float, font_path: str = PLEX_MONO) -> floa
|
|
|
116
118
|
(``font_path``), not a system font *name*: name resolution substitutes a
|
|
117
119
|
different font on Linux, which makes this estimate — and the layout it feeds —
|
|
118
120
|
platform-variant (#149). The default is the same face the annotations render
|
|
119
|
-
with, so estimate and render agree.
|
|
120
|
-
|
|
121
|
+
with, so estimate and render agree. *font* only matters when a caller opts out
|
|
122
|
+
of path-pinning (``font_path=None``) — then the *name* resolves through the OS
|
|
123
|
+
font stack, exactly as the renderer would. Cached because the same numeric
|
|
124
|
+
labels recur across holes and the rasterisation is the costly part.
|
|
121
125
|
"""
|
|
122
126
|
if not text:
|
|
123
|
-
return 0.0
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
return (0.0, 0.0)
|
|
128
|
+
bb = Text(
|
|
129
|
+
txt=text,
|
|
130
|
+
font_size=font_size,
|
|
131
|
+
font=font,
|
|
132
|
+
font_path=font_path,
|
|
133
|
+
align=(Align.CENTER, Align.CENTER),
|
|
134
|
+
mode=Mode.PRIVATE,
|
|
135
|
+
).bounding_box()
|
|
136
|
+
return (bb.size.X, bb.size.Y)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _text_width(text: str, font_size: float, font_path: str = PLEX_MONO) -> float:
|
|
140
|
+
"""Measured rendered width (page-mm) of *text* — see :func:`_text_size`."""
|
|
141
|
+
return _text_size(text, font_size, font_path)[0]
|
|
135
142
|
|
|
136
143
|
|
|
137
144
|
# Cheap mean glyph advance as a fraction of the em (font size), for label-width ESTIMATES in
|
|
@@ -556,6 +563,12 @@ class Analysis:
|
|
|
556
563
|
out: str
|
|
557
564
|
pmi: list
|
|
558
565
|
pmi_mode: str
|
|
566
|
+
# The PartModel built by _analyse's pre-scale sizing pass (#584 WP1 A) — stored so
|
|
567
|
+
# the render path reuses it instead of re-running the detectors (ADR 0008 Amdt 5:
|
|
568
|
+
# one inventory, detected once; #602). Typed `object` because model/ sits above
|
|
569
|
+
# _core in the DAG. None when the caller declared a model (ADR 0011) or on a
|
|
570
|
+
# manually-built Analysis — consumers fall back to build_model(a).
|
|
571
|
+
model: object | None = None
|
|
559
572
|
|
|
560
573
|
|
|
561
574
|
_greedy_strip_ys = _greedy_strip_1d
|
|
@@ -772,4 +772,9 @@ def _analyse(
|
|
|
772
772
|
out=out,
|
|
773
773
|
pmi=pmi_records,
|
|
774
774
|
pmi_mode=pmi,
|
|
775
|
+
# The sizing model IS the render model when detection ran (identical inputs by
|
|
776
|
+
# construction — #584 WP1 A); store it so the pipeline never detects twice
|
|
777
|
+
# (ADR 0008 Amdt 5, #602). A declared model (layout_model) is NOT stored: the
|
|
778
|
+
# builder coerces + decorates the caller's model itself.
|
|
779
|
+
model=sizing_model if layout_model is None else None,
|
|
775
780
|
)
|
|
@@ -8,11 +8,13 @@ and an AABB overlap test (`_box_hits`). Bottom of the annotations DAG.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
+
import math
|
|
11
12
|
from dataclasses import dataclass, field
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
14
|
-
from build123d_drafting.helpers import Dimension, SafeDimension
|
|
15
|
+
from build123d_drafting.helpers import DEFAULT_FONT_PATH, Dimension, SafeDimension
|
|
15
16
|
|
|
17
|
+
from draftwright._core import _text_size
|
|
16
18
|
from draftwright.layout import StripCandidate, plan_strip
|
|
17
19
|
from draftwright.linting.issues import LintIssue
|
|
18
20
|
from draftwright.linting.structural import _centerline_extent
|
|
@@ -100,6 +102,84 @@ def _geom_box(o):
|
|
|
100
102
|
return None
|
|
101
103
|
|
|
102
104
|
|
|
105
|
+
def dim_footprint(p1, p2, side, distance, draft, label):
|
|
106
|
+
"""Analytical page-mm AABB ``(x0, y0, x1, y1)`` of the :class:`Dimension` that
|
|
107
|
+
``_dim(p1, p2, side, distance, draft, label=label)`` would build — WITHOUT
|
|
108
|
+
constructing any OCC geometry (#602: a rejected candidate must not pay the
|
|
109
|
+
rendering cost).
|
|
110
|
+
|
|
111
|
+
Mirrors ``helpers.Dimension`` / build123d ``ExtensionLine``: each extension line
|
|
112
|
+
is the object→dimension-line segment *translated* ``extension_gap`` along itself,
|
|
113
|
+
so it spans ``p + side·gap`` → ``p + side·(distance + gap)`` — starting ``gap``
|
|
114
|
+
clear of the object and overshooting the dimension line by the same ``gap``. The
|
|
115
|
+
measured label is centred on the line's midpoint with its extents swapped for a
|
|
116
|
+
vertical measured segment (the label is rotated); everything strokes at
|
|
117
|
+
``line_width``. With inside arrows the heads lie within the extension-line
|
|
118
|
+
overshoot (at preset sizes), so they add nothing to the hull.
|
|
119
|
+
|
|
120
|
+
Tight spans mirror helpers ≥0.14's outside-arrows flip (``_dim_line_ink``):
|
|
121
|
+
when the label and both heads don't fit — ``w + 2·al ≥ length`` or the shaft
|
|
122
|
+
piece beside a head would vanish — the ink extends ``2·arrow_length`` past
|
|
123
|
+
each end along the line. The label itself stays centred regardless of width
|
|
124
|
+
(``Dimension`` always passes an explicit ``label_t``, so the hang branch
|
|
125
|
+
never runs), and when its keep-clear reaches a witness end the witness's
|
|
126
|
+
overshoot past the dimension line is cut away with it. Without this model
|
|
127
|
+
the estimate under-covers exactly the dims v0.14 widens, and the
|
|
128
|
+
accept-time rebuild fails validation until the candidate is dropped (the
|
|
129
|
+
declared-plate 8 mm thickness dim was the first casualty). Assumes the
|
|
130
|
+
centred label (``label_offset_x=0``), like the rest of the estimate.
|
|
131
|
+
Callers accepting a candidate off this footprint must still build the real
|
|
132
|
+
geometry once and re-validate its box (the #602 validation fallback) so any
|
|
133
|
+
residual mismatch degrades to a wasted probe, never a collision.
|
|
134
|
+
"""
|
|
135
|
+
if isinstance(side, str): # mirror helpers._SIDE_VECTORS ("above"/"below"/"left"/"right")
|
|
136
|
+
side = {
|
|
137
|
+
"above": (0.0, 1.0, 0.0),
|
|
138
|
+
"below": (0.0, -1.0, 0.0),
|
|
139
|
+
"left": (-1.0, 0.0, 0.0),
|
|
140
|
+
"right": (1.0, 0.0, 0.0),
|
|
141
|
+
}[side]
|
|
142
|
+
sx, sy = side[0], side[1]
|
|
143
|
+
off = abs(distance)
|
|
144
|
+
gap = draft.extension_gap
|
|
145
|
+
dxp, dyp = p2[0] - p1[0], p2[1] - p1[1]
|
|
146
|
+
# Mirror the renderer's font resolution exactly (helpers._font_path): the pinned
|
|
147
|
+
# font *file* when set, else the helpers default; an explicit font_path=None means
|
|
148
|
+
# name-based resolution of draft.font — so pass the name through too.
|
|
149
|
+
w, h = _text_size(
|
|
150
|
+
label,
|
|
151
|
+
draft.font_size,
|
|
152
|
+
getattr(draft, "font_path", DEFAULT_FONT_PATH),
|
|
153
|
+
getattr(draft, "font", "Arial"),
|
|
154
|
+
)
|
|
155
|
+
hx, hy = (h / 2.0, w / 2.0) if abs(dyp) > abs(dxp) else (w / 2.0, h / 2.0)
|
|
156
|
+
lcx = (p1[0] + p2[0]) / 2.0 + sx * off
|
|
157
|
+
lcy = (p1[1] + p2[1]) / 2.0 + sy * off
|
|
158
|
+
far = off + gap
|
|
159
|
+
xs = [p1[0] + sx * gap, p2[0] + sx * gap, lcx - hx, lcx + hx]
|
|
160
|
+
ys = [p1[1] + sy * gap, p2[1] + sy * gap, lcy - hy, lcy + hy]
|
|
161
|
+
# Helpers ≥0.14 tight-span behaviour. Along the line: the label's half-extent
|
|
162
|
+
# is w/2 regardless of orientation (the text rotates with the line); outside
|
|
163
|
+
# arrows extend the ink 2·al past each end when label+heads don't fit. Along
|
|
164
|
+
# the witness: the label keep-clear (h/2 + pad either side of the line) is
|
|
165
|
+
# cut out of a witness the centred label reaches, removing its overshoot
|
|
166
|
+
# unless a stub past the keep-clear survives (gap > h/2 + pad).
|
|
167
|
+
length = math.hypot(dxp, dyp)
|
|
168
|
+
al = getattr(draft, "arrow_length", 0.9 * draft.font_size)
|
|
169
|
+
tpad = getattr(draft, "pad_around_text", 0.0)
|
|
170
|
+
fits = length > 0 and (w + 2.0 * al < length) and (length / 2.0 - w / 2.0 - tpad > al / 2.0)
|
|
171
|
+
if not fits and length > 0:
|
|
172
|
+
ux, uy = dxp / length, dyp / length
|
|
173
|
+
xs += [p1[0] + sx * off - ux * 2.0 * al, p2[0] + sx * off + ux * 2.0 * al]
|
|
174
|
+
ys += [p1[1] + sy * off - uy * 2.0 * al, p2[1] + sy * off + uy * 2.0 * al]
|
|
175
|
+
label_covers_witness = length > 0 and length / 2.0 < w / 2.0 + tpad
|
|
176
|
+
if not label_covers_witness or gap > h / 2.0 + tpad:
|
|
177
|
+
xs += [p1[0] + sx * far, p2[0] + sx * far]
|
|
178
|
+
ys += [p1[1] + sy * far, p2[1] + sy * far]
|
|
179
|
+
pad = draft.line_width / 2.0
|
|
180
|
+
return (min(xs) - pad, min(ys) - pad, max(xs) + pad, max(ys) + pad)
|
|
181
|
+
|
|
182
|
+
|
|
103
183
|
CROSSABLE_TYPES = frozenset({"Centerline", "CenterlineCircle", "CenterMark"})
|
|
104
184
|
"""Annotation types a *dimension* may legitimately cross (ISO 128): centre lines
|
|
105
185
|
and centre marks. A **leader**, by contrast, must avoid them (#305) — so this is a
|
|
@@ -175,6 +255,52 @@ def clear_label_of_centerlines(label_bbox, centerlines, gap):
|
|
|
175
255
|
return target_x - lmin_x
|
|
176
256
|
|
|
177
257
|
|
|
258
|
+
def occupancy_boxes(o, stroke_pad=None):
|
|
259
|
+
"""*o*'s occupancy as a list of AABBs — decomposed, not one hull (#685).
|
|
260
|
+
|
|
261
|
+
An annotation's rendered hull includes large EMPTY corner regions (a dimension's
|
|
262
|
+
ink is L-shaped: witness lines + a dim-line band), and helpers ≥0.14's honest
|
|
263
|
+
tight-span rendering made those hulls big enough to collide structurally at view
|
|
264
|
+
corners while the inks stay disjoint. For an annotation exposing ``.segments``
|
|
265
|
+
(helpers ≥0.14 reports the drawn line pieces: witness lines, shafts, box
|
|
266
|
+
strokes), return one box per stroke — inflated by ``_STROKE_PAD`` to cover line
|
|
267
|
+
width plus the arrowheads at stroke junctions — plus its ``label_bbox``.
|
|
268
|
+
Anything else (leaders, hatch, title block) keeps its single hull box.
|
|
269
|
+
|
|
270
|
+
Consumers treat the result exactly like a list of hull boxes; the
|
|
271
|
+
perpendicular-band filter in the carve then does the rest — a witness sliver
|
|
272
|
+
carves only its own sliver, and an out-of-band dim-line band stops blocking a
|
|
273
|
+
sibling strip's corner entirely.
|
|
274
|
+
"""
|
|
275
|
+
segs = getattr(o, "segments", None)
|
|
276
|
+
if not segs:
|
|
277
|
+
b = _geom_box(o)
|
|
278
|
+
return [b] if b is not None else []
|
|
279
|
+
pad = _STROKE_PAD if stroke_pad is None else stroke_pad
|
|
280
|
+
out = []
|
|
281
|
+
for (x0, y0), (x1, y1) in segs:
|
|
282
|
+
out.append(
|
|
283
|
+
(
|
|
284
|
+
min(x0, x1) - pad,
|
|
285
|
+
min(y0, y1) - pad,
|
|
286
|
+
max(x0, x1) + pad,
|
|
287
|
+
max(y0, y1) + pad,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
lb = getattr(o, "label_bbox", None)
|
|
291
|
+
if lb is not None:
|
|
292
|
+
out.append((lb[0], lb[1], lb[2], lb[3]))
|
|
293
|
+
return out
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Fallback stroke inflation for decomposed occupancy: line_width/2 (~0.08) plus the
|
|
297
|
+
# arrowhead half-width at stroke junctions at DEFAULT presets. Arrow geometry scales
|
|
298
|
+
# with font_size (#688 review), so callers that know the draft derive the pad as
|
|
299
|
+
# max(_STROKE_PAD, draft.arrow_length / 2) — arrow half-LENGTH bounds the head's
|
|
300
|
+
# half-width (aspect < 1) AND its protrusion past an inside-arrow shaft trim (al/2).
|
|
301
|
+
_STROKE_PAD = 1.2
|
|
302
|
+
|
|
303
|
+
|
|
178
304
|
def strip_obstacles(dwg, view=None, *, crossable=()):
|
|
179
305
|
"""The COMPLETE occupancy for strip placement (ADR 0009): every placed
|
|
180
306
|
annotation's full rendered footprint, optionally restricted to *view*, minus
|
|
@@ -204,6 +330,9 @@ def strip_obstacles(dwg, view=None, *, crossable=()):
|
|
|
204
330
|
|
|
205
331
|
The occupancy source for the collect-then-solve carve — every migrated renderer's
|
|
206
332
|
``place_strip_candidates`` call wires this in (#321/#150/P3)."""
|
|
333
|
+
# Preset-aware stroke pad (#688 review): arrowheads scale with font_size.
|
|
334
|
+
al = getattr(getattr(dwg, "draft", None), "arrow_length", None)
|
|
335
|
+
pad = max(_STROKE_PAD, al / 2) if al else _STROKE_PAD
|
|
207
336
|
boxes = []
|
|
208
337
|
for name, o in dwg.iter_annotations():
|
|
209
338
|
if view is not None:
|
|
@@ -212,9 +341,7 @@ def strip_obstacles(dwg, view=None, *, crossable=()):
|
|
|
212
341
|
continue # owned by a different ortho view → its own (disjoint) block
|
|
213
342
|
if type(o).__name__ in crossable:
|
|
214
343
|
continue # this consumer may cross it (centre lines/marks for a dim)
|
|
215
|
-
|
|
216
|
-
if bb is not None:
|
|
217
|
-
boxes.append(bb)
|
|
344
|
+
boxes.extend(occupancy_boxes(o, stroke_pad=pad)) # decomposed, not one hull (#685)
|
|
218
345
|
return boxes
|
|
219
346
|
|
|
220
347
|
|
|
@@ -415,9 +542,20 @@ class CorridorCandidate:
|
|
|
415
542
|
# the title block, which is placed after the corridor drain so the strip carve can't see
|
|
416
543
|
# it (#481). ``None`` (every dim) skips the check → byte-identical.
|
|
417
544
|
forbid: object | None = None
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
545
|
+
# Analytical ``pos -> (x0, y0, x1, y1)`` footprint of the geometry ``build(pos)``
|
|
546
|
+
# would produce (#602): lets the strip solve measure and evaluate this candidate
|
|
547
|
+
# without constructing OCC geometry at all (see :func:`dim_footprint`). ``None``
|
|
548
|
+
# falls back to one probe build at the strip edge + the box-shift model. CONTRACT:
|
|
549
|
+
# the footprint must be accurate — its PERPENDICULAR extent feeds the obstacle
|
|
550
|
+
# band filter, so an underestimate hides obstacles from the carve. The solve
|
|
551
|
+
# re-validates each built survivor against the blockers, the forbid box and the
|
|
552
|
+
# band-filtered-out obstacles, so a miss costs a wasted build and a retry, but
|
|
553
|
+
# keeping footprints truthful (``dim_footprint``, ±0.05 mm) is what keeps that
|
|
554
|
+
# fallback rare and the placement identical to the probe path.
|
|
555
|
+
footprint: object | None = None
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def solve_corridor(dwg, strip, view, axis, cands, tier, corner_reserves=()):
|
|
421
559
|
"""One collect-then-solve over every :class:`CorridorCandidate` a shared strip
|
|
422
560
|
accumulated across passes (ADR 0009 end state). Dedup → order → one non-force
|
|
423
561
|
:func:`place_strip_candidates` pass → a force pass for the force-eligible leftovers →
|
|
@@ -472,6 +610,7 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
472
610
|
prio = {c.name: c.priority for c in kept if c.priority} # over-capacity survival rank (#357)
|
|
473
611
|
anchored = {c.name: c.anchored for c in kept if c.anchored}
|
|
474
612
|
naturals = {c.name: c.natural for c in kept if c.natural is not None}
|
|
613
|
+
foots = {c.name: c.footprint for c in kept if c.footprint is not None} # analytical (#602)
|
|
475
614
|
left = {
|
|
476
615
|
n
|
|
477
616
|
for n, _ in place_strip_candidates(
|
|
@@ -487,6 +626,8 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
487
626
|
priorities=prio,
|
|
488
627
|
anchored=anchored,
|
|
489
628
|
naturals=naturals,
|
|
629
|
+
footprints=foots,
|
|
630
|
+
corner_reserves=corner_reserves,
|
|
490
631
|
)
|
|
491
632
|
}
|
|
492
633
|
force_pairs = [(c.name, c.build) for c in kept if c.name in left and c.force]
|
|
@@ -501,6 +642,8 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
501
642
|
force_pairs,
|
|
502
643
|
tier,
|
|
503
644
|
force=True,
|
|
645
|
+
footprints=foots,
|
|
646
|
+
corner_reserves=corner_reserves,
|
|
504
647
|
features=feats,
|
|
505
648
|
sizes=sizes,
|
|
506
649
|
forbid=forbid,
|
|
@@ -540,6 +683,10 @@ class PlacementContext:
|
|
|
540
683
|
corridor_batch: dict = field(default_factory=dict)
|
|
541
684
|
escalations: list = field(default_factory=list)
|
|
542
685
|
detail_requests: list = field(default_factory=list)
|
|
686
|
+
# Fallthrough callbacks a pass's on_drop queues to run AFTER every corridor has
|
|
687
|
+
# drained (#684 review): a mid-drain carve could occupy space a later sibling
|
|
688
|
+
# corridor's force candidate needs; deferral makes "post-drain" literally true.
|
|
689
|
+
post_drain: list = field(default_factory=list)
|
|
543
690
|
# The drawing's build-state stores, referenced (not owned) by the run's passes (#639).
|
|
544
691
|
# Duck-typed as ``Any`` — matching the untyped ``Drawing._record_build_issue`` they replace —
|
|
545
692
|
# so mypy does not reject the delegating calls below.
|
|
@@ -601,10 +748,43 @@ def register_corridor(ctx, key, strip, view, axis, tier, cand):
|
|
|
601
748
|
def drain_corridors(ctx, dwg):
|
|
602
749
|
"""Solve every registered corridor (one :func:`solve_corridor` per strip), then clear
|
|
603
750
|
the batch. Called once, after all corridor-feeding passes have registered. Takes both the
|
|
604
|
-
scratch *ctx* (the batch) and *dwg* (the drawing :func:`solve_corridor` places onto).
|
|
605
|
-
|
|
606
|
-
|
|
751
|
+
scratch *ctx* (the batch) and *dwg* (the drawing :func:`solve_corridor` places onto).
|
|
752
|
+
|
|
753
|
+
Corner coordination (helpers ≥0.14): perpendicular strips of one view contest the
|
|
754
|
+
view corners — a tight-span dim's outside-arrow tails overhang past the view edge
|
|
755
|
+
into the sibling strip's band (an 8 mm plate-thickness dim on the left strip dips
|
|
756
|
+
below the view bottom; the below strip's own tight dim pokes left of the view edge).
|
|
757
|
+
Solved sequentially and blind, the first drain fills the corner and the second's
|
|
758
|
+
force-kept candidate hard-drops. So each solve receives the *innermost-tier
|
|
759
|
+
footprint boxes* of every not-yet-drained same-view sibling's **force** candidates
|
|
760
|
+
as extra obstacles: the earlier drain places clear of the corner the later one
|
|
761
|
+
provably needs. Reservation is exact (the candidate's own analytical footprint, at
|
|
762
|
+
the innermost position it would take), restricted to force candidates — principal
|
|
763
|
+
dims that would otherwise drop rather than relocate — so best-effort occupants
|
|
764
|
+
never lose capacity to it. Already-drained siblings need nothing: their dims are
|
|
765
|
+
real obstacles via :func:`strip_obstacles`."""
|
|
766
|
+
batches = list(ctx.corridor_batch.values())
|
|
767
|
+
for i, b in enumerate(batches):
|
|
768
|
+
reserves = []
|
|
769
|
+
for sib in batches[i + 1 :]:
|
|
770
|
+
if sib["view"] != b["view"] or sib["strip"] is None:
|
|
771
|
+
continue
|
|
772
|
+
s = sib["strip"]
|
|
773
|
+
inner_pos = s.anchor + s.direction * s.gap
|
|
774
|
+
for c in sib["cands"]:
|
|
775
|
+
if c.force and c.footprint is not None:
|
|
776
|
+
box = c.footprint(inner_pos)
|
|
777
|
+
if box is not None:
|
|
778
|
+
reserves.append(box)
|
|
779
|
+
solve_corridor(
|
|
780
|
+
dwg, b["strip"], b["view"], b["axis"], b["cands"], b["tier"], corner_reserves=reserves
|
|
781
|
+
)
|
|
607
782
|
ctx.corridor_batch = {}
|
|
783
|
+
# Deferred fallthroughs (opposite-strip retries) run once every strip has drained,
|
|
784
|
+
# so a retry can never preempt a corner a later sibling's force candidate needs.
|
|
785
|
+
pending, ctx.post_drain = ctx.post_drain, []
|
|
786
|
+
for cb in pending:
|
|
787
|
+
cb()
|
|
608
788
|
|
|
609
789
|
|
|
610
790
|
def place_strip_candidates(
|
|
@@ -622,6 +802,8 @@ def place_strip_candidates(
|
|
|
622
802
|
priorities=None,
|
|
623
803
|
anchored=None,
|
|
624
804
|
naturals=None,
|
|
805
|
+
footprints=None,
|
|
806
|
+
corner_reserves=(),
|
|
625
807
|
):
|
|
626
808
|
"""Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
|
|
627
809
|
The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
|
|
@@ -699,13 +881,59 @@ def place_strip_candidates(
|
|
|
699
881
|
# obstacles out first. The perpendicular extent is independent of the tier position,
|
|
700
882
|
# so a single probe build per candidate suffices; the corridor check below already
|
|
701
883
|
# uses the full 2-D box, so it needs no such filter.
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
884
|
+
#
|
|
885
|
+
# That one probe build is also this call's entire MEASUREMENT step (#602): every
|
|
886
|
+
# candidate is a fixed feature-side anchor (witness origin / leader shaft end)
|
|
887
|
+
# plus a dim line that translates with the tier position, so its box at position
|
|
888
|
+
# ``pos`` is the probe box with the OUTWARD stacking-axis edge shifted by
|
|
889
|
+
# ``pos - lo`` — no further geometry is built to evaluate a position. The
|
|
890
|
+
# segment loop below re-solves and re-checks on these predicted boxes only;
|
|
891
|
+
# each finally-accepted candidate is built once and its real box re-validated
|
|
892
|
+
# (a prediction miss degrades to a later-segment retry, never a collision).
|
|
893
|
+
# A candidate with an analytical *footprints* entry (#602) needs no probe build at
|
|
894
|
+
# all — its box at any position is computed, not measured.
|
|
895
|
+
probe_boxes = {
|
|
896
|
+
name: (footprints[name](lo) if name in (footprints or {}) else _geom_box(build(lo)))
|
|
897
|
+
for name, build in cands
|
|
898
|
+
}
|
|
899
|
+
pbands = [(b[perp], b[perp + 2]) for b in probe_boxes.values() if b is not None]
|
|
900
|
+
|
|
901
|
+
def _predicted_box(name, pos):
|
|
902
|
+
fp = (footprints or {}).get(name)
|
|
903
|
+
if fp is not None:
|
|
904
|
+
return fp(pos)
|
|
905
|
+
pb = probe_boxes.get(name)
|
|
906
|
+
if pb is None:
|
|
907
|
+
return None
|
|
908
|
+
box = list(pb)
|
|
909
|
+
# The moving edge is the one AWAY from the view (`inner`); the feature-side
|
|
910
|
+
# edge is anchored geometry and stays put.
|
|
911
|
+
box[idx + 2 if inner == lo else idx] += pos - lo
|
|
912
|
+
return tuple(box)
|
|
913
|
+
|
|
705
914
|
occupied = strip_obstacles(dwg, view=view, crossable=CROSSABLE_TYPES)
|
|
915
|
+
# Obstacles OUTSIDE the batch's predicted perpendicular band are invisible to the
|
|
916
|
+
# carve below by design — but that makes the band prediction itself load-bearing: a
|
|
917
|
+
# candidate whose real geometry exceeds its predicted band could land on one with no
|
|
918
|
+
# check ever seeing it (review #679). Keep the filtered-out set: the post-build
|
|
919
|
+
# validation re-checks each survivor's REAL box against it. In-band overlaps are NOT
|
|
920
|
+
# validated — witness lines legitimately cross the boxes of dims stacked further in
|
|
921
|
+
# (ISO 129-1), which is exactly why the carve projects onto the stacking axis only.
|
|
922
|
+
out_of_band: list = []
|
|
706
923
|
if pbands:
|
|
707
924
|
band_lo, band_hi = min(p[0] for p in pbands), max(p[1] for p in pbands)
|
|
708
|
-
|
|
925
|
+
in_band = [b for b in occupied if b[perp] < band_hi and b[perp + 2] > band_lo]
|
|
926
|
+
out_of_band = [b for b in occupied if not (b[perp] < band_hi and b[perp + 2] > band_lo)]
|
|
927
|
+
occupied = in_band
|
|
928
|
+
# Corner reserves (drain_corridors): projected boxes of not-yet-drained sibling
|
|
929
|
+
# corridors' force candidates. Same band relevance filter as real obstacles, but
|
|
930
|
+
# NEVER in out_of_band — a reserve is a projection, not geometry, so a survivor's
|
|
931
|
+
# real box must not be failed against it.
|
|
932
|
+
occupied += [
|
|
933
|
+
r
|
|
934
|
+
for r in corner_reserves
|
|
935
|
+
if r is not None and r[perp] < band_hi and r[perp + 2] > band_lo
|
|
936
|
+
]
|
|
709
937
|
blockers = () if force else corridor_blockers(dwg, view)
|
|
710
938
|
segs = carve_free_segments(lo, hi, [(b[idx], b[idx + 2]) for b in occupied], pad)
|
|
711
939
|
# Fill innermost-first (nearest the view), matching the old cursor's stack order.
|
|
@@ -764,8 +992,15 @@ def place_strip_candidates(
|
|
|
764
992
|
if pos is None: # segment over its estimated capacity (shouldn't occur)
|
|
765
993
|
rejected.append((name, build))
|
|
766
994
|
continue
|
|
767
|
-
|
|
768
|
-
|
|
995
|
+
# Predicted box, not built geometry (#602): the refill loop re-evaluates
|
|
996
|
+
# every already-accepted candidate each iteration, so building here made
|
|
997
|
+
# the drain quadratic in OCC builds.
|
|
998
|
+
box = _predicted_box(name, pos)
|
|
999
|
+
if box is None: # probe didn't bbox — measure the old way, once per check
|
|
1000
|
+
box = _geom_box(build(pos))
|
|
1001
|
+
if (
|
|
1002
|
+
not force and box is not None and _box_hits(box, blockers)
|
|
1003
|
+
): # corridor crosses a leader
|
|
769
1004
|
rejected.append((name, build))
|
|
770
1005
|
continue
|
|
771
1006
|
# A forbidden box (the title block, #481) is rejected even under force — it is
|
|
@@ -773,10 +1008,10 @@ def place_strip_candidates(
|
|
|
773
1008
|
# must still not stack onto it. `forbid` maps names to their box (only GD&T sets it,
|
|
774
1009
|
# so dims are byte-identical). Returned unplaced → the caller's on_drop fallthrough.
|
|
775
1010
|
fb = (forbid or {}).get(name)
|
|
776
|
-
if fb is not None and _box_hits(
|
|
1011
|
+
if fb is not None and box is not None and _box_hits(box, (fb,)):
|
|
777
1012
|
rejected.append((name, build))
|
|
778
1013
|
continue
|
|
779
|
-
accepted.append(((name, build),
|
|
1014
|
+
accepted.append(((name, build), pos))
|
|
780
1015
|
return accepted, rejected
|
|
781
1016
|
|
|
782
1017
|
for seg_lo, seg_hi in segs:
|
|
@@ -792,9 +1027,32 @@ def place_strip_candidates(
|
|
|
792
1027
|
if vacancies <= 0 or not todo:
|
|
793
1028
|
break
|
|
794
1029
|
fill, todo = _take_for_segment(todo, vacancies)
|
|
795
|
-
take = [nb for nb,
|
|
1030
|
+
take = [nb for nb, _pos in accepted] + fill
|
|
1031
|
+
# Build each survivor ONCE at its solved position and re-validate the real box
|
|
1032
|
+
# (the #602 validation fallback): a prediction miss is returned to the pool for
|
|
1033
|
+
# the next segment — exactly where a same-segment rejection would have sent it.
|
|
1034
|
+
placed = []
|
|
1035
|
+
for (name, build), pos in accepted:
|
|
1036
|
+
dim = build(pos)
|
|
1037
|
+
real = _geom_box(dim)
|
|
1038
|
+
if real is not None:
|
|
1039
|
+
if not force and _box_hits(real, blockers):
|
|
1040
|
+
rejected_total.append((name, build))
|
|
1041
|
+
continue
|
|
1042
|
+
fb = (forbid or {}).get(name)
|
|
1043
|
+
if fb is not None and _box_hits(real, (fb,)):
|
|
1044
|
+
rejected_total.append((name, build))
|
|
1045
|
+
continue
|
|
1046
|
+
# A survivor whose real geometry escaped the batch's predicted
|
|
1047
|
+
# perpendicular band could overlap an obstacle the carve was never
|
|
1048
|
+
# shown (review #679) — the one collision class the stacking-axis
|
|
1049
|
+
# model cannot tolerate. Never fires while predictions are accurate.
|
|
1050
|
+
if _box_hits(real, out_of_band):
|
|
1051
|
+
rejected_total.append((name, build))
|
|
1052
|
+
continue
|
|
1053
|
+
placed.append((name, dim))
|
|
796
1054
|
todo = todo + rejected_total
|
|
797
|
-
for
|
|
1055
|
+
for name, dim in placed:
|
|
798
1056
|
# Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
|
|
799
1057
|
# dims — `features` maps this batch's names to their source IR feature.
|
|
800
1058
|
dwg.add(dim, name, view=view, feature=(features or {}).get(name))
|