draftwright 0.2.10__tar.gz → 0.2.12__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.10 → draftwright-0.2.12}/CHANGELOG.md +64 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/PKG-INFO +5 -5
- {draftwright-0.2.10 → draftwright-0.2.12}/README.md +4 -4
- {draftwright-0.2.10 → draftwright-0.2.12}/pyproject.toml +1 -1
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/_core.py +11 -5
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/analysis.py +159 -14
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/annotations/_common.py +77 -11
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/annotations/from_model.py +406 -247
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/annotations/holes.py +232 -83
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/annotations/orchestrator.py +30 -49
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/annotations/sections.py +11 -13
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/builder.py +59 -35
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/drawing.py +439 -113
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/linting/suggest.py +13 -9
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/model/__init__.py +4 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/model/detect.py +28 -9
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/model/ir.py +53 -9
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/repair.py +8 -25
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/sheet.py +239 -82
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/sheet_dsl.py +73 -1
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/sheet_emit.py +90 -13
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/bracket.json +12 -12
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/dshape.json +3 -3
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/side_drilled.json +3 -3
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_make_drawing.py +562 -60
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_pmi.py +37 -13
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_render_seam.py +34 -11
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_sheet_emit.py +160 -15
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_strip_layout.py +119 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/.gitignore +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/LICENSE +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/skills/SKILL.md +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/cli.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/export.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/features.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/fits.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/model/declare.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/_kernel.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_declare.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_fits.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_gdt_placement.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_layout.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_lint_reconciliation.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_linting.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_object_aspects.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_part_model.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_recognition.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_registry.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_sheet_notes.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_sheet_of.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_sheet_tables.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_tolerances.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.12}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.12 — 2026-07-09
|
|
4
|
+
|
|
5
|
+
**Sheet scripts now round-trip authored layout intent.** This patch finishes the next
|
|
6
|
+
slice of the layout-authority work: below/right ladders, pinned edits, height ladders,
|
|
7
|
+
detected envelopes, and AP242 authored dimensions now survive the declarative Sheet path
|
|
8
|
+
without being invalidated by post-hoc placement or raw imported metadata.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Pinned edit intents route through the corridor solve.** `locate(..., pin=True)` and
|
|
13
|
+
pinned dimensions now become priority-ranked candidates in the shared corridor instead
|
|
14
|
+
of fixed post-hoc edits, so user intent participates in the same ordering/spacing model
|
|
15
|
+
as automatic dimensions. (#511)
|
|
16
|
+
- **AP242 dimensional PMI lowers to authored drafting dimensions.** Imported AP242
|
|
17
|
+
size/location dimensions now become `AuthoredDimension` IR and generated Sheet scripts
|
|
18
|
+
emit `sheet.dimension(...)`; unsupported PMI records remain explicit raw fallbacks.
|
|
19
|
+
(#503, #422, #62)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`place_dim` is deprecated.** Manual dimensions should use pinned candidate/dimension
|
|
24
|
+
intent instead of the old incremental edit path.
|
|
25
|
+
- **Prismatic height ladders and detected envelopes round-trip through Sheet scripts.**
|
|
26
|
+
Generated scripts now preserve `StepLevelFeature` and detected `EnvelopeFeature`
|
|
27
|
+
fallbacks, preventing CTC01-style ladder swaps and raw STEP-envelope remeasurement
|
|
28
|
+
drift.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Below/right corridors now share one solve.** Side-hole locations, height ladders,
|
|
33
|
+
PMI/GD&T, envelope dims, and related below/right strip occupants negotiate in the shared
|
|
34
|
+
corridor rather than competing through separate late passes. (#477)
|
|
35
|
+
- **Sheet script parity is tighter.** Generated scripts preserve member positions, step
|
|
36
|
+
levels, AP242 authored dimensions, raw PMI fallbacks, and detected envelopes so
|
|
37
|
+
direct-vs-script CTC01 output has matching annotation names.
|
|
38
|
+
|
|
39
|
+
## v0.2.11 — 2026-07-08
|
|
40
|
+
|
|
41
|
+
**Automatic layout now has one authority.** Page/scale selection, section placement,
|
|
42
|
+
furniture footprints, and table/balloon escalation now negotiate through the same layout
|
|
43
|
+
fitness model instead of using separate fixed offsets or first-fit policies after the main
|
|
44
|
+
solve.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **Solver placement paths are more consistent.** STEP PMI dimensions now queue into
|
|
49
|
+
the shared ADR 0009 corridor solve instead of carving after the drain; front-view
|
|
50
|
+
hole callouts use the strip solver instead of fixed row stepping; pitch-dimension
|
|
51
|
+
fallback searches bounded, obstacle-aware positions; repair no longer hides
|
|
52
|
+
`annotation_overlap` with a fixed-step nudge; and step-count sizing now handles
|
|
53
|
+
non-convergence conservatively. (#524)
|
|
54
|
+
- **Page/scale fitness is shared across initial selection and repack.** Later layout
|
|
55
|
+
passes now compare candidates with the same model as the page chooser, preventing a
|
|
56
|
+
nominally "better" repack from invalidating the original layout decision. (#519)
|
|
57
|
+
- **Section A-A participates in layout selection.** Section placement is measured as
|
|
58
|
+
part of page/scale fitness, so a section view is no longer a fixed-offset afterthought
|
|
59
|
+
that can disappear on dense sheets. (#515)
|
|
60
|
+
- **Furniture placement reserves full rendered footprints.** Section arrows, detail
|
|
61
|
+
views, balloons, hole/data tables, and other furniture now reserve/check their true
|
|
62
|
+
rendered footprint instead of just their labels. (#518)
|
|
63
|
+
- **Hole/data tables and balloon rings escalate through the layout model.** Dense table
|
|
64
|
+
and balloon outputs can move or negotiate for available room rather than dropping from
|
|
65
|
+
greedy first-fit placement when a small adjustment would fit. (#516, #517)
|
|
66
|
+
|
|
3
67
|
## v0.2.10 — 2026-07-07
|
|
4
68
|
|
|
5
69
|
**Declarative-surface fidelity and layout-engine unification.** Editing and re-running a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: draftwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
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
|
|
@@ -842,10 +842,9 @@ automatically, so you say *what* to dimension, not *where*:
|
|
|
842
842
|
```python
|
|
843
843
|
dwg = build_drawing(part)
|
|
844
844
|
|
|
845
|
-
# Inspect detected features and add a dimension in domain terms
|
|
846
|
-
for f in dwg.features
|
|
847
|
-
|
|
848
|
-
"below", "plan", dwg.draft, name="dim_pocket")
|
|
845
|
+
# Inspect detected features and add a pinned dimension in domain terms:
|
|
846
|
+
env = next(f for f in dwg.model().features if f.kind == "envelope")
|
|
847
|
+
dwg.dimension(env, "length", role="width", side="below", pin=True)
|
|
849
848
|
|
|
850
849
|
crit = dwg.lint_summary() # {"passed", "score", "by_code", "issues":[…suggestion]}
|
|
851
850
|
dwg.repair() # auto-fix mechanically-fixable lint; never worsens
|
|
@@ -876,6 +875,7 @@ the section A–A trigger, the prismatic step-ladder + rotational furniture, and
|
|
|
876
875
|
are all on the IR — the migration is complete (one path; the orchestrator is
|
|
877
876
|
build → plan → render). See
|
|
878
877
|
[`docs/target-architecture.md`](docs/target-architecture.md) and
|
|
878
|
+
[`docs/layout-algorithm-primer.md`](docs/layout-algorithm-primer.md) for a short walkthrough, plus
|
|
879
879
|
[`docs/adr/`](docs/adr/). The engine handles view layout (strip/zone model), scale
|
|
880
880
|
selection, annotation placement, and section rendering.
|
|
881
881
|
|
|
@@ -149,10 +149,9 @@ automatically, so you say *what* to dimension, not *where*:
|
|
|
149
149
|
```python
|
|
150
150
|
dwg = build_drawing(part)
|
|
151
151
|
|
|
152
|
-
# Inspect detected features and add a dimension in domain terms
|
|
153
|
-
for f in dwg.features
|
|
154
|
-
|
|
155
|
-
"below", "plan", dwg.draft, name="dim_pocket")
|
|
152
|
+
# Inspect detected features and add a pinned dimension in domain terms:
|
|
153
|
+
env = next(f for f in dwg.model().features if f.kind == "envelope")
|
|
154
|
+
dwg.dimension(env, "length", role="width", side="below", pin=True)
|
|
156
155
|
|
|
157
156
|
crit = dwg.lint_summary() # {"passed", "score", "by_code", "issues":[…suggestion]}
|
|
158
157
|
dwg.repair() # auto-fix mechanically-fixable lint; never worsens
|
|
@@ -183,6 +182,7 @@ the section A–A trigger, the prismatic step-ladder + rotational furniture, and
|
|
|
183
182
|
are all on the IR — the migration is complete (one path; the orchestrator is
|
|
184
183
|
build → plan → render). See
|
|
185
184
|
[`docs/target-architecture.md`](docs/target-architecture.md) and
|
|
185
|
+
[`docs/layout-algorithm-primer.md`](docs/layout-algorithm-primer.md) for a short walkthrough, plus
|
|
186
186
|
[`docs/adr/`](docs/adr/). The engine handles view layout (strip/zone model), scale
|
|
187
187
|
selection, annotation placement, and section rendering.
|
|
188
188
|
|
|
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING
|
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from draftwright.recognition import TurnedProfile
|
|
24
|
+
from draftwright.sheet import StripDepths
|
|
24
25
|
|
|
25
26
|
from build123d import Align, BoundBox, Location, Mode, Shape, Text
|
|
26
27
|
from build123d_drafting.helpers import (
|
|
@@ -338,7 +339,7 @@ def _legible_locations(positions, scale):
|
|
|
338
339
|
return kept, n_too_close
|
|
339
340
|
|
|
340
341
|
|
|
341
|
-
def _largest_empty_rect(drawable, obstacles):
|
|
342
|
+
def _largest_empty_rect(drawable, obstacles, *, warn: bool = True):
|
|
342
343
|
"""Largest axis-aligned empty rectangle in *drawable* avoiding *obstacles*.
|
|
343
344
|
|
|
344
345
|
*drawable* and each obstacle are ``(x0, y0, x1, y1)`` page-mm boxes. Returns
|
|
@@ -376,10 +377,11 @@ def _largest_empty_rect(drawable, obstacles):
|
|
|
376
377
|
# is unreachable in practice — choose_scale always leaves a gap — but
|
|
377
378
|
# if it ever happens the iso would render over the other views, so flag
|
|
378
379
|
# it rather than fail silently.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
380
|
+
if warn:
|
|
381
|
+
_log.warning(
|
|
382
|
+
"No empty rectangle found for the iso view; obstacles fill the "
|
|
383
|
+
"drawable area — iso may overlap other views"
|
|
384
|
+
)
|
|
383
385
|
return drawable
|
|
384
386
|
return best
|
|
385
387
|
|
|
@@ -514,6 +516,10 @@ class Analysis:
|
|
|
514
516
|
is_rotational: bool
|
|
515
517
|
od_axis: str # rotation/turning axis of a rotational part ("z" default; "x"/"y" #222)
|
|
516
518
|
step_zs: list[float]
|
|
519
|
+
layout_strips: StripDepths
|
|
520
|
+
layout_n_steps: int
|
|
521
|
+
layout_section: bool
|
|
522
|
+
layout_table_sizes: tuple[tuple[float, float], ...]
|
|
517
523
|
sv_right: float
|
|
518
524
|
iso_right_limit: float
|
|
519
525
|
SCALE: float
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import logging
|
|
14
14
|
import math
|
|
15
15
|
import warnings
|
|
16
|
+
from collections.abc import Callable
|
|
16
17
|
|
|
17
18
|
from build123d import Compound, Shape
|
|
18
19
|
from build123d_drafting.helpers import draft_preset
|
|
@@ -20,6 +21,7 @@ from OCP.IFSelect import IFSelect_ReturnStatus
|
|
|
20
21
|
from OCP.STEPControl import STEPControl_Reader
|
|
21
22
|
|
|
22
23
|
from draftwright._core import (
|
|
24
|
+
_CONCENTRIC_TOL_MM,
|
|
23
25
|
_DIM_PAD,
|
|
24
26
|
_FONT_SIZE,
|
|
25
27
|
_MARGIN,
|
|
@@ -40,9 +42,12 @@ from draftwright.recognition import (
|
|
|
40
42
|
full_cylinders,
|
|
41
43
|
)
|
|
42
44
|
from draftwright.sheet import (
|
|
45
|
+
StripDepths,
|
|
43
46
|
_build_zones,
|
|
47
|
+
_est_hole_table_sizes,
|
|
44
48
|
_layout_geometry,
|
|
45
49
|
_measure_strips,
|
|
50
|
+
_will_section,
|
|
46
51
|
choose_scale,
|
|
47
52
|
)
|
|
48
53
|
|
|
@@ -54,6 +59,41 @@ _log = logging.getLogger(__name__)
|
|
|
54
59
|
_SQUARENESS_TOL = 0.05
|
|
55
60
|
_OD_FILL_MIN = 0.8
|
|
56
61
|
_OD_AXIS_TOL = 0.05
|
|
62
|
+
_ScalePick = tuple[float, float, float, float]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _declared_will_section(model, *, is_rotational=False, cx=0.0, cy=0.0) -> bool:
|
|
66
|
+
"""True when a caller-supplied IR model contains a section-driving Z hole.
|
|
67
|
+
|
|
68
|
+
Detection-based layout uses recogniser holes; declared-model builds may
|
|
69
|
+
intentionally supply features detection missed. Inspect the public IR shape
|
|
70
|
+
duck-typed here so declared sections get the same page/scale reservation.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if model is None:
|
|
74
|
+
return False
|
|
75
|
+
features = getattr(model, "features", model)
|
|
76
|
+
|
|
77
|
+
def feature_member(pt) -> bool:
|
|
78
|
+
return not (is_rotational and math.hypot(pt[0] - cx, pt[1] - cy) <= _CONCENTRIC_TOL_MM)
|
|
79
|
+
|
|
80
|
+
for feat in features:
|
|
81
|
+
if getattr(feat, "kind", None) not in ("hole", "pattern"):
|
|
82
|
+
continue
|
|
83
|
+
frame = getattr(feat, "frame", None)
|
|
84
|
+
if frame is None or frame.axis != "z":
|
|
85
|
+
continue
|
|
86
|
+
members = getattr(feat, "members", ()) or (frame.origin,)
|
|
87
|
+
if not any(feature_member(m) for m in members):
|
|
88
|
+
continue
|
|
89
|
+
bore = getattr(feat, "member", feat)
|
|
90
|
+
if (
|
|
91
|
+
getattr(bore, "cbore", None) is not None
|
|
92
|
+
or getattr(bore, "spotface", None) is not None
|
|
93
|
+
or not getattr(bore, "through", True)
|
|
94
|
+
):
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
57
97
|
|
|
58
98
|
|
|
59
99
|
def _import_step(path) -> Compound:
|
|
@@ -106,6 +146,53 @@ def _is_rotational(x_size, y_size, od_diam, od_axis_offset) -> bool:
|
|
|
106
146
|
)
|
|
107
147
|
|
|
108
148
|
|
|
149
|
+
def _converge_step_sizing(
|
|
150
|
+
initial_steps: int,
|
|
151
|
+
measure_strips: Callable[[int], StripDepths],
|
|
152
|
+
pick_scale: Callable[[int, StripDepths], _ScalePick],
|
|
153
|
+
count_legible: Callable[[float], int],
|
|
154
|
+
) -> tuple[_ScalePick, StripDepths, int]:
|
|
155
|
+
"""Choose scale/page with a step-corridor count that matches legibility.
|
|
156
|
+
|
|
157
|
+
The right-side step ladder is reserved before the scale is known, but the
|
|
158
|
+
actual step list is filtered by the chosen scale. Iterate that dependency
|
|
159
|
+
until it reaches a fixed point; if it cycles, reserve the largest count seen
|
|
160
|
+
so the sheet is sized conservatively instead of silently accepting whichever
|
|
161
|
+
value happened to appear on a fixed iteration budget (#520).
|
|
162
|
+
"""
|
|
163
|
+
n_for_sizing = initial_steps
|
|
164
|
+
seen: set[int] = set()
|
|
165
|
+
attempted: list[int] = []
|
|
166
|
+
max_iter = max(4, initial_steps + 2)
|
|
167
|
+
|
|
168
|
+
for _ in range(max_iter):
|
|
169
|
+
if n_for_sizing in seen:
|
|
170
|
+
break
|
|
171
|
+
seen.add(n_for_sizing)
|
|
172
|
+
attempted.append(n_for_sizing)
|
|
173
|
+
|
|
174
|
+
strips = measure_strips(n_for_sizing)
|
|
175
|
+
scale_pick = pick_scale(n_for_sizing, strips)
|
|
176
|
+
n_next = count_legible(scale_pick[0])
|
|
177
|
+
if n_next == n_for_sizing:
|
|
178
|
+
return scale_pick, strips, n_for_sizing
|
|
179
|
+
if n_next in seen:
|
|
180
|
+
n_for_sizing = n_next
|
|
181
|
+
break
|
|
182
|
+
n_for_sizing = n_next
|
|
183
|
+
|
|
184
|
+
conservative_n = max(attempted + [n_for_sizing], default=initial_steps)
|
|
185
|
+
strips = measure_strips(conservative_n)
|
|
186
|
+
scale_pick = pick_scale(conservative_n, strips)
|
|
187
|
+
_log.warning(
|
|
188
|
+
"Step-corridor sizing did not converge from %d steps (tried %s); reserving %d steps",
|
|
189
|
+
initial_steps,
|
|
190
|
+
attempted,
|
|
191
|
+
conservative_n,
|
|
192
|
+
)
|
|
193
|
+
return scale_pick, strips, conservative_n
|
|
194
|
+
|
|
195
|
+
|
|
109
196
|
# A hole is "concentric" with a turned part's rotation axis when its drilling
|
|
110
197
|
# axis is the Z (OD) axis and its opening sits on the part centreline. Such
|
|
111
198
|
# bores are already dimensioned by the ldr_z bore leaders, so they must not
|
|
@@ -202,7 +289,16 @@ def _solids_body(part, src: str = "part"):
|
|
|
202
289
|
|
|
203
290
|
|
|
204
291
|
def _analyse(
|
|
205
|
-
step_file,
|
|
292
|
+
step_file,
|
|
293
|
+
title,
|
|
294
|
+
number,
|
|
295
|
+
tolerance,
|
|
296
|
+
drawn_by,
|
|
297
|
+
out,
|
|
298
|
+
scale=None,
|
|
299
|
+
page=None,
|
|
300
|
+
pmi="off",
|
|
301
|
+
model=None,
|
|
206
302
|
) -> Analysis:
|
|
207
303
|
"""Load STEP or use a build123d Shape, analyse geometry, compute layout.
|
|
208
304
|
|
|
@@ -317,6 +413,20 @@ def _analyse(
|
|
|
317
413
|
patterns = find_hole_patterns(holes)
|
|
318
414
|
bosses = find_bosses(part, cyls=(z_cyls, cross_cyls)) # detect once — the one inventory (#264)
|
|
319
415
|
slots = find_slots(part)
|
|
416
|
+
layout_section = _will_section(
|
|
417
|
+
holes,
|
|
418
|
+
patterns,
|
|
419
|
+
is_rotational=is_rotational,
|
|
420
|
+
cx=cx,
|
|
421
|
+
cy=cy,
|
|
422
|
+
) or _declared_will_section(model, is_rotational=is_rotational, cx=cx, cy=cy)
|
|
423
|
+
layout_table_sizes = _est_hole_table_sizes(
|
|
424
|
+
holes,
|
|
425
|
+
patterns,
|
|
426
|
+
bb,
|
|
427
|
+
font_size=_FONT_SIZE,
|
|
428
|
+
pad_around_text=_pad_around_text,
|
|
429
|
+
)
|
|
320
430
|
|
|
321
431
|
# Choose scale/page, iterating so the reserved step corridor matches the
|
|
322
432
|
# number of steps the legibility gate will actually place (#1) — not the raw
|
|
@@ -324,24 +434,35 @@ def _analyse(
|
|
|
324
434
|
# with 15 tiny treads) reserves a phantom step ladder that blocks a larger
|
|
325
435
|
# scale. Seed conservatively (all faces), then re-gate at the chosen scale;
|
|
326
436
|
# converges in a couple of rounds.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
for _ in range(3):
|
|
330
|
-
strips_i = _measure_strips(
|
|
437
|
+
def _measure_for_step_count(n_steps_i: int) -> StripDepths:
|
|
438
|
+
return _measure_strips(
|
|
331
439
|
holes,
|
|
332
440
|
patterns,
|
|
333
|
-
|
|
441
|
+
n_steps_i,
|
|
334
442
|
bb,
|
|
335
443
|
arrow_length=_arrow_length,
|
|
336
444
|
pad_around_text=_pad_around_text,
|
|
337
445
|
)
|
|
338
|
-
|
|
339
|
-
|
|
446
|
+
|
|
447
|
+
def _pick_for_step_count(n_steps_i: int, strips_i: StripDepths) -> _ScalePick:
|
|
448
|
+
return choose_scale(
|
|
449
|
+
x_size,
|
|
450
|
+
y_size,
|
|
451
|
+
z_size,
|
|
452
|
+
n_steps=n_steps_i,
|
|
453
|
+
scale=scale,
|
|
454
|
+
page=page,
|
|
455
|
+
strips=strips_i,
|
|
456
|
+
section=layout_section,
|
|
457
|
+
table_sizes=layout_table_sizes,
|
|
340
458
|
)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
459
|
+
|
|
460
|
+
(SCALE, PAGE_W, PAGE_H, TB_W), strips_i, n_for_sizing = _converge_step_sizing(
|
|
461
|
+
len(step_zs),
|
|
462
|
+
_measure_for_step_count,
|
|
463
|
+
_pick_for_step_count,
|
|
464
|
+
lambda scale_i: len(_legible_steps(step_zs, bb.min.Z, scale_i)[0]),
|
|
465
|
+
)
|
|
345
466
|
if scale is not None:
|
|
346
467
|
# An explicit scale is the user's call — honour it (#489). Two floors apply:
|
|
347
468
|
# - _MIN_RENDER_MM: a hard geometry limit; below it OCCT's annotation arcs degenerate
|
|
@@ -362,7 +483,15 @@ def _analyse(
|
|
|
362
483
|
f"Use scale ≥ {safe:.3g} or omit the scale for automatic selection."
|
|
363
484
|
)
|
|
364
485
|
auto_scale, _, _, _ = choose_scale(
|
|
365
|
-
x_size,
|
|
486
|
+
x_size,
|
|
487
|
+
y_size,
|
|
488
|
+
z_size,
|
|
489
|
+
n_steps=n_for_sizing,
|
|
490
|
+
scale=None,
|
|
491
|
+
page=page,
|
|
492
|
+
strips=strips_i,
|
|
493
|
+
section=layout_section,
|
|
494
|
+
table_sizes=layout_table_sizes,
|
|
366
495
|
)
|
|
367
496
|
# Warn only when omitting the scale would truly give a legible fit (auto scale itself is
|
|
368
497
|
# legible) but the requested scale is below the floor. A part illegible at every
|
|
@@ -393,7 +522,19 @@ def _analyse(
|
|
|
393
522
|
# View positions + iso empty-rectangle, shared with scale selection (_fits)
|
|
394
523
|
# via _layout_geometry so placement and fit never diverge (#11). _fit_iso_view
|
|
395
524
|
# later scales the iso to fill its rectangle.
|
|
396
|
-
_g = _layout_geometry(
|
|
525
|
+
_g = _layout_geometry(
|
|
526
|
+
x_size,
|
|
527
|
+
y_size,
|
|
528
|
+
z_size,
|
|
529
|
+
SCALE,
|
|
530
|
+
PAGE_W,
|
|
531
|
+
PAGE_H,
|
|
532
|
+
TB_W,
|
|
533
|
+
strips,
|
|
534
|
+
n_steps,
|
|
535
|
+
section=layout_section,
|
|
536
|
+
table_sizes=layout_table_sizes,
|
|
537
|
+
)
|
|
397
538
|
fv_hw = _g.fv_hw
|
|
398
539
|
fv_hh = _g.fv_hh
|
|
399
540
|
pv_hh = _g.pv_hh
|
|
@@ -461,6 +602,10 @@ def _analyse(
|
|
|
461
602
|
is_rotational=is_rotational,
|
|
462
603
|
od_axis=od_axis,
|
|
463
604
|
step_zs=step_zs,
|
|
605
|
+
layout_strips=strips,
|
|
606
|
+
layout_n_steps=n_steps,
|
|
607
|
+
layout_section=layout_section,
|
|
608
|
+
layout_table_sizes=layout_table_sizes,
|
|
464
609
|
sv_right=sv_right,
|
|
465
610
|
iso_right_limit=iso_right_limit,
|
|
466
611
|
SCALE=SCALE,
|
|
@@ -293,6 +293,10 @@ class CorridorCandidate:
|
|
|
293
293
|
``priority`` is kept. An authored GD&T frame sets this above the auto dims so it
|
|
294
294
|
is not dropped in favour of a lower-value auto dim purely by stacking-key order.
|
|
295
295
|
Default 0 (every auto dim) → key order, unchanged.
|
|
296
|
+
anchored/natural: when ``anchored`` is true, the strip solve keeps this candidate
|
|
297
|
+
near its own natural stacking-axis page coordinate instead of the segment edge.
|
|
298
|
+
This is how user-authored pinned dimension intents join the shared solve
|
|
299
|
+
without being invalidated by a later first-fit pass.
|
|
296
300
|
on_place/on_drop: the pass's own post-placement bookkeeping — coverage
|
|
297
301
|
registration / drop lint + `Escalation`, or a slot's below-side fallthrough.
|
|
298
302
|
force: policy-B force-keep after the corridor-respecting pass (locations have
|
|
@@ -307,6 +311,8 @@ class CorridorCandidate:
|
|
|
307
311
|
dedup: tuple | None = None
|
|
308
312
|
precedence: int = 0
|
|
309
313
|
priority: float = 0
|
|
314
|
+
anchored: bool = False
|
|
315
|
+
natural: float | None = None
|
|
310
316
|
force: bool = False
|
|
311
317
|
# The source IR feature this dim was rendered for — recorded as provenance when the
|
|
312
318
|
# dim is placed at drain (ADR 0010). ``None`` leaves the annotation feature-less.
|
|
@@ -376,6 +382,8 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
376
382
|
sizes = {c.name: c.size for c in kept if c.size is not None} # real footprint (#61)
|
|
377
383
|
forbid = {c.name: c.forbid for c in kept if c.forbid is not None} # title-block box (#481)
|
|
378
384
|
prio = {c.name: c.priority for c in kept if c.priority} # over-capacity survival rank (#357)
|
|
385
|
+
anchored = {c.name: c.anchored for c in kept if c.anchored}
|
|
386
|
+
naturals = {c.name: c.natural for c in kept if c.natural is not None}
|
|
379
387
|
left = {
|
|
380
388
|
n
|
|
381
389
|
for n, _ in place_strip_candidates(
|
|
@@ -389,6 +397,8 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
389
397
|
sizes=sizes,
|
|
390
398
|
forbid=forbid,
|
|
391
399
|
priorities=prio,
|
|
400
|
+
anchored=anchored,
|
|
401
|
+
naturals=naturals,
|
|
392
402
|
)
|
|
393
403
|
}
|
|
394
404
|
force_pairs = [(c.name, c.build) for c in kept if c.name in left and c.force]
|
|
@@ -407,6 +417,8 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
407
417
|
sizes=sizes,
|
|
408
418
|
forbid=forbid,
|
|
409
419
|
priorities=prio,
|
|
420
|
+
anchored=anchored,
|
|
421
|
+
naturals=naturals,
|
|
410
422
|
)
|
|
411
423
|
}
|
|
412
424
|
if force_pairs
|
|
@@ -425,10 +437,13 @@ def solve_corridor(dwg, strip, view, axis, cands, tier):
|
|
|
425
437
|
def register_corridor(dwg, key, strip, view, axis, tier, cand):
|
|
426
438
|
"""Queue a :class:`CorridorCandidate` under a shared corridor *key* so one
|
|
427
439
|
:func:`drain_corridors` places the whole cross-pass set together (ADR 0009 end state).
|
|
428
|
-
The first registration for a key fixes its ``(strip, view, axis
|
|
440
|
+
The first registration for a key fixes its ``(strip, view, axis)``; mixed producers on
|
|
441
|
+
the same corridor use the largest requested tier so spacing is not registration-order
|
|
442
|
+
dependent."""
|
|
429
443
|
b = dwg._corridor_batch.setdefault(
|
|
430
444
|
key, {"strip": strip, "view": view, "axis": axis, "tier": tier, "cands": []}
|
|
431
445
|
)
|
|
446
|
+
b["tier"] = max(b["tier"], tier)
|
|
432
447
|
b["cands"].append(cand)
|
|
433
448
|
|
|
434
449
|
|
|
@@ -453,6 +468,8 @@ def place_strip_candidates(
|
|
|
453
468
|
sizes=None,
|
|
454
469
|
forbid=None,
|
|
455
470
|
priorities=None,
|
|
471
|
+
anchored=None,
|
|
472
|
+
naturals=None,
|
|
456
473
|
):
|
|
457
474
|
"""Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
|
|
458
475
|
The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
|
|
@@ -483,6 +500,11 @@ def place_strip_candidates(
|
|
|
483
500
|
the lowest ``(priority, key)``, so a higher priority is kept — an authored GD&T frame
|
|
484
501
|
is not dropped for a lower-value auto dim purely by stacking-key order.
|
|
485
502
|
|
|
503
|
+
*anchored* and *naturals* opt individual candidates into the weighted anchoring
|
|
504
|
+
mode in :func:`plan_strip`. This preserves the old segment-edge natural for every
|
|
505
|
+
caller that does not pass them, while letting authored pinned candidates express the
|
|
506
|
+
page coordinate they asked for inside the same shared solve.
|
|
507
|
+
|
|
486
508
|
``force=True`` skips that corridor check — the caller's last resort when no view took
|
|
487
509
|
the dim cleanly: keep it on its natural view and accept the (same-feature) leader
|
|
488
510
|
crossing rather than drop a real dimension (policy B). Candidates that find no strip
|
|
@@ -537,13 +559,31 @@ def place_strip_candidates(
|
|
|
537
559
|
# Fill innermost-first (nearest the view), matching the old cursor's stack order.
|
|
538
560
|
segs.sort(key=lambda s: abs((s[0] if inner == lo else s[1]) - inner))
|
|
539
561
|
todo = list(cands)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
562
|
+
|
|
563
|
+
def _take_for_segment(items, n):
|
|
564
|
+
if len(items) <= n:
|
|
565
|
+
return items, []
|
|
566
|
+
# Do not let segment-cap slicing preempt the ranked selection step (#357/#393).
|
|
567
|
+
# `plan_strip` drops the lowest (priority, generated-key), but a narrow segment
|
|
568
|
+
# can only see the candidates we hand it. Preselect the highest-priority members
|
|
569
|
+
# for this segment, preserving their original order for crossing-free placement;
|
|
570
|
+
# ties mirror the generated key below (inner=lo keeps later candidates, inner=hi
|
|
571
|
+
# keeps earlier candidates).
|
|
572
|
+
ranked = sorted(
|
|
573
|
+
enumerate(items),
|
|
574
|
+
key=lambda item: (
|
|
575
|
+
(priorities or {}).get(item[1][0], 0.0),
|
|
576
|
+
item[0] if inner == lo else -item[0],
|
|
577
|
+
),
|
|
578
|
+
reverse=True,
|
|
579
|
+
)
|
|
580
|
+
chosen = {i for i, _ in ranked[:n]}
|
|
581
|
+
take = [nb for i, nb in enumerate(items) if i in chosen]
|
|
582
|
+
rest = [nb for i, nb in enumerate(items) if i not in chosen]
|
|
583
|
+
return take, rest
|
|
584
|
+
|
|
585
|
+
def _evaluate_segment(take, seg_lo, seg_hi):
|
|
545
586
|
nat = seg_lo if inner == lo else seg_hi
|
|
546
|
-
anch = (0.0, nat) if axis == "y" else (nat, 0.0)
|
|
547
587
|
# Keys order the tiers so the FIRST candidate lands on the inner tier: for an
|
|
548
588
|
# inner=lo strip that is the lowest position (ascending keys); for a below strip
|
|
549
589
|
# (inner=hi) it is the highest, so the keys reverse.
|
|
@@ -551,23 +591,30 @@ def place_strip_candidates(
|
|
|
551
591
|
(
|
|
552
592
|
StripCandidate(
|
|
553
593
|
f"{(k if inner == lo else len(take) - 1 - k):04d}",
|
|
554
|
-
|
|
594
|
+
(
|
|
595
|
+
(0.0, (naturals or {}).get(nb[0], nat))
|
|
596
|
+
if axis == "y"
|
|
597
|
+
else ((naturals or {}).get(nb[0], nat), 0.0)
|
|
598
|
+
),
|
|
555
599
|
(sizes or {}).get(nb[0], (tier, tier)),
|
|
556
600
|
priority=(priorities or {}).get(nb[0], 0.0),
|
|
601
|
+
anchored=(anchored or {}).get(nb[0], False),
|
|
557
602
|
),
|
|
558
603
|
nb,
|
|
559
604
|
)
|
|
560
605
|
for k, nb in enumerate(take)
|
|
561
606
|
]
|
|
562
607
|
res = plan_strip([sc for sc, _ in triples], seg_lo, seg_hi, pad, axis=axis)
|
|
608
|
+
accepted = []
|
|
609
|
+
rejected = []
|
|
563
610
|
for sc, (name, build) in triples:
|
|
564
611
|
pos = res.placed.get(sc.key)
|
|
565
612
|
if pos is None: # segment over its estimated capacity (shouldn't occur)
|
|
566
|
-
|
|
613
|
+
rejected.append((name, build))
|
|
567
614
|
continue
|
|
568
615
|
dim = build(pos)
|
|
569
616
|
if not force and _box_hits(_geom_box(dim), blockers): # corridor crosses a leader
|
|
570
|
-
|
|
617
|
+
rejected.append((name, build))
|
|
571
618
|
continue
|
|
572
619
|
# A forbidden box (the title block, #481) is rejected even under force — it is
|
|
573
620
|
# placed after the drain, so the strip carve can't see it; a force-kept GD&T frame
|
|
@@ -575,8 +622,27 @@ def place_strip_candidates(
|
|
|
575
622
|
# so dims are byte-identical). Returned unplaced → the caller's on_drop fallthrough.
|
|
576
623
|
fb = (forbid or {}).get(name)
|
|
577
624
|
if fb is not None and _box_hits(_geom_box(dim), (fb,)):
|
|
578
|
-
|
|
625
|
+
rejected.append((name, build))
|
|
579
626
|
continue
|
|
627
|
+
accepted.append(((name, build), dim))
|
|
628
|
+
return accepted, rejected
|
|
629
|
+
|
|
630
|
+
for seg_lo, seg_hi in segs:
|
|
631
|
+
if not todo:
|
|
632
|
+
break
|
|
633
|
+
cap = int((seg_hi - seg_lo) / pad) + 1
|
|
634
|
+
take, todo = _take_for_segment(todo, cap)
|
|
635
|
+
rejected_total = []
|
|
636
|
+
while take:
|
|
637
|
+
accepted, rejected = _evaluate_segment(take, seg_lo, seg_hi)
|
|
638
|
+
rejected_total.extend(rejected)
|
|
639
|
+
vacancies = cap - len(accepted)
|
|
640
|
+
if vacancies <= 0 or not todo:
|
|
641
|
+
break
|
|
642
|
+
fill, todo = _take_for_segment(todo, vacancies)
|
|
643
|
+
take = [nb for nb, _dim in accepted] + fill
|
|
644
|
+
todo = todo + rejected_total
|
|
645
|
+
for (name, _build), dim in accepted:
|
|
580
646
|
# Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
|
|
581
647
|
# dims — `features` maps this batch's names to their source IR feature.
|
|
582
648
|
dwg.add(dim, name, view=view, feature=(features or {}).get(name))
|