draftwright 0.2.10__tar.gz → 0.2.11__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.11}/CHANGELOG.md +28 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/PKG-INFO +1 -1
- {draftwright-0.2.10 → draftwright-0.2.11}/pyproject.toml +1 -1
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/_core.py +11 -5
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/analysis.py +159 -14
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/annotations/_common.py +48 -8
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/annotations/from_model.py +268 -165
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/annotations/holes.py +126 -54
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/annotations/orchestrator.py +17 -19
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/annotations/sections.py +11 -13
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/builder.py +33 -18
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/drawing.py +151 -57
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/repair.py +8 -25
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/sheet.py +239 -82
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/bracket.json +12 -12
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_make_drawing.py +348 -47
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_pmi.py +7 -4
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_strip_layout.py +53 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/.gitignore +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/LICENSE +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/README.md +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/skills/SKILL.md +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/cli.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/export.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/features.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/fits.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/linting/suggest.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/model/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/model/declare.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/model/detect.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/model/ir.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/sheet_dsl.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/src/draftwright/sheet_emit.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/_kernel.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/dshape.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/side_drilled.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_declare.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_fits.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_gdt_placement.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_layout.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_lint_reconciliation.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_linting.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_object_aspects.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_part_model.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_recognition.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_registry.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_render_seam.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_sheet_emit.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_sheet_notes.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_sheet_of.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_sheet_tables.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_tolerances.py +0 -0
- {draftwright-0.2.10 → draftwright-0.2.11}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.11 — 2026-07-08
|
|
4
|
+
|
|
5
|
+
**Automatic layout now has one authority.** Page/scale selection, section placement,
|
|
6
|
+
furniture footprints, and table/balloon escalation now negotiate through the same layout
|
|
7
|
+
fitness model instead of using separate fixed offsets or first-fit policies after the main
|
|
8
|
+
solve.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Solver placement paths are more consistent.** STEP PMI dimensions now queue into
|
|
13
|
+
the shared ADR 0009 corridor solve instead of carving after the drain; front-view
|
|
14
|
+
hole callouts use the strip solver instead of fixed row stepping; pitch-dimension
|
|
15
|
+
fallback searches bounded, obstacle-aware positions; repair no longer hides
|
|
16
|
+
`annotation_overlap` with a fixed-step nudge; and step-count sizing now handles
|
|
17
|
+
non-convergence conservatively. (#524)
|
|
18
|
+
- **Page/scale fitness is shared across initial selection and repack.** Later layout
|
|
19
|
+
passes now compare candidates with the same model as the page chooser, preventing a
|
|
20
|
+
nominally "better" repack from invalidating the original layout decision. (#519)
|
|
21
|
+
- **Section A-A participates in layout selection.** Section placement is measured as
|
|
22
|
+
part of page/scale fitness, so a section view is no longer a fixed-offset afterthought
|
|
23
|
+
that can disappear on dense sheets. (#515)
|
|
24
|
+
- **Furniture placement reserves full rendered footprints.** Section arrows, detail
|
|
25
|
+
views, balloons, hole/data tables, and other furniture now reserve/check their true
|
|
26
|
+
rendered footprint instead of just their labels. (#518)
|
|
27
|
+
- **Hole/data tables and balloon rings escalate through the layout model.** Dense table
|
|
28
|
+
and balloon outputs can move or negotiate for available room rather than dropping from
|
|
29
|
+
greedy first-fit placement when a small adjustment would fit. (#516, #517)
|
|
30
|
+
|
|
3
31
|
## v0.2.10 — 2026-07-07
|
|
4
32
|
|
|
5
33
|
**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.11
|
|
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
|
|
@@ -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,
|
|
@@ -537,11 +537,30 @@ def place_strip_candidates(
|
|
|
537
537
|
# Fill innermost-first (nearest the view), matching the old cursor's stack order.
|
|
538
538
|
segs.sort(key=lambda s: abs((s[0] if inner == lo else s[1]) - inner))
|
|
539
539
|
todo = list(cands)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
540
|
+
|
|
541
|
+
def _take_for_segment(items, n):
|
|
542
|
+
if len(items) <= n:
|
|
543
|
+
return items, []
|
|
544
|
+
# Do not let segment-cap slicing preempt the ranked selection step (#357/#393).
|
|
545
|
+
# `plan_strip` drops the lowest (priority, generated-key), but a narrow segment
|
|
546
|
+
# can only see the candidates we hand it. Preselect the highest-priority members
|
|
547
|
+
# for this segment, preserving their original order for crossing-free placement;
|
|
548
|
+
# ties mirror the generated key below (inner=lo keeps later candidates, inner=hi
|
|
549
|
+
# keeps earlier candidates).
|
|
550
|
+
ranked = sorted(
|
|
551
|
+
enumerate(items),
|
|
552
|
+
key=lambda item: (
|
|
553
|
+
(priorities or {}).get(item[1][0], 0.0),
|
|
554
|
+
item[0] if inner == lo else -item[0],
|
|
555
|
+
),
|
|
556
|
+
reverse=True,
|
|
557
|
+
)
|
|
558
|
+
chosen = {i for i, _ in ranked[:n]}
|
|
559
|
+
take = [nb for i, nb in enumerate(items) if i in chosen]
|
|
560
|
+
rest = [nb for i, nb in enumerate(items) if i not in chosen]
|
|
561
|
+
return take, rest
|
|
562
|
+
|
|
563
|
+
def _evaluate_segment(take, seg_lo, seg_hi):
|
|
545
564
|
nat = seg_lo if inner == lo else seg_hi
|
|
546
565
|
anch = (0.0, nat) if axis == "y" else (nat, 0.0)
|
|
547
566
|
# Keys order the tiers so the FIRST candidate lands on the inner tier: for an
|
|
@@ -560,14 +579,16 @@ def place_strip_candidates(
|
|
|
560
579
|
for k, nb in enumerate(take)
|
|
561
580
|
]
|
|
562
581
|
res = plan_strip([sc for sc, _ in triples], seg_lo, seg_hi, pad, axis=axis)
|
|
582
|
+
accepted = []
|
|
583
|
+
rejected = []
|
|
563
584
|
for sc, (name, build) in triples:
|
|
564
585
|
pos = res.placed.get(sc.key)
|
|
565
586
|
if pos is None: # segment over its estimated capacity (shouldn't occur)
|
|
566
|
-
|
|
587
|
+
rejected.append((name, build))
|
|
567
588
|
continue
|
|
568
589
|
dim = build(pos)
|
|
569
590
|
if not force and _box_hits(_geom_box(dim), blockers): # corridor crosses a leader
|
|
570
|
-
|
|
591
|
+
rejected.append((name, build))
|
|
571
592
|
continue
|
|
572
593
|
# A forbidden box (the title block, #481) is rejected even under force — it is
|
|
573
594
|
# placed after the drain, so the strip carve can't see it; a force-kept GD&T frame
|
|
@@ -575,8 +596,27 @@ def place_strip_candidates(
|
|
|
575
596
|
# so dims are byte-identical). Returned unplaced → the caller's on_drop fallthrough.
|
|
576
597
|
fb = (forbid or {}).get(name)
|
|
577
598
|
if fb is not None and _box_hits(_geom_box(dim), (fb,)):
|
|
578
|
-
|
|
599
|
+
rejected.append((name, build))
|
|
579
600
|
continue
|
|
601
|
+
accepted.append(((name, build), dim))
|
|
602
|
+
return accepted, rejected
|
|
603
|
+
|
|
604
|
+
for seg_lo, seg_hi in segs:
|
|
605
|
+
if not todo:
|
|
606
|
+
break
|
|
607
|
+
cap = int((seg_hi - seg_lo) / pad) + 1
|
|
608
|
+
take, todo = _take_for_segment(todo, cap)
|
|
609
|
+
rejected_total = []
|
|
610
|
+
while take:
|
|
611
|
+
accepted, rejected = _evaluate_segment(take, seg_lo, seg_hi)
|
|
612
|
+
rejected_total.extend(rejected)
|
|
613
|
+
vacancies = cap - len(accepted)
|
|
614
|
+
if vacancies <= 0 or not todo:
|
|
615
|
+
break
|
|
616
|
+
fill, todo = _take_for_segment(todo, vacancies)
|
|
617
|
+
take = [nb for nb, _dim in accepted] + fill
|
|
618
|
+
todo = todo + rejected_total
|
|
619
|
+
for (name, _build), dim in accepted:
|
|
580
620
|
# Record feature provenance (ADR 0010): the drain-time seam for corridor-placed
|
|
581
621
|
# dims — `features` maps this batch's names to their source IR feature.
|
|
582
622
|
dwg.add(dim, name, view=view, feature=(features or {}).get(name))
|