draftwright 0.2.11__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.11 → draftwright-0.2.12}/CHANGELOG.md +36 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/PKG-INFO +5 -5
- {draftwright-0.2.11 → draftwright-0.2.12}/README.md +4 -4
- {draftwright-0.2.11 → draftwright-0.2.12}/pyproject.toml +1 -1
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/annotations/_common.py +29 -3
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/annotations/from_model.py +140 -84
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/annotations/holes.py +106 -29
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/annotations/orchestrator.py +14 -31
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/builder.py +26 -17
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/drawing.py +288 -56
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/linting/suggest.py +13 -9
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/model/__init__.py +4 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/model/detect.py +28 -9
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/model/ir.py +53 -9
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/sheet_dsl.py +73 -1
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/sheet_emit.py +90 -13
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/dshape.json +3 -3
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/side_drilled.json +3 -3
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_make_drawing.py +214 -13
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_pmi.py +30 -9
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_render_seam.py +34 -11
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_sheet_emit.py +160 -15
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_strip_layout.py +66 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/.gitignore +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/LICENSE +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/skills/SKILL.md +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/__init__.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/_core.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/analysis.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/annotate.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/annotations/__init__.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/annotations/sections.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/cli.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/export.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/features.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/fits.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/fonts/__init__.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/intents.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/layout.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/linting/__init__.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/linting/coverage.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/linting/issues.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/linting/structural.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/make_drawing.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/model/declare.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/model/planner.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/pmi.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/projection.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/recognition/__init__.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/recognition/_features.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/recognition/levels.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/recognition/slots.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/recognition/turned.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/registry.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/repair.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/src/draftwright/sheet.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/_kernel.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/box.json +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/bracket.json +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/drive_screw_x.json +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/flange.json +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/holed_slot.json +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/plate_holes.json +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/slotted.json +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/layout_snapshots/turned_shaft.json +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_declare.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_e2e_slice.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_e2e_standards.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_fits.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_gdt_placement.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_layout.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_layout_cleanliness.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_layout_property.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_layout_snapshot.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_lint_reconciliation.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_lint_structural.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_linting.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_object_aspects.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_part_model.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_recognition.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_registry.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_sheet_gdt.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_sheet_notes.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_sheet_of.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_sheet_tables.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_tolerances.py +0 -0
- {draftwright-0.2.11 → draftwright-0.2.12}/tests/test_turned_steps.py +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
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
|
+
|
|
3
39
|
## v0.2.11 — 2026-07-08
|
|
4
40
|
|
|
5
41
|
**Automatic layout now has one authority.** Page/scale selection, section placement,
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -562,7 +584,6 @@ def place_strip_candidates(
|
|
|
562
584
|
|
|
563
585
|
def _evaluate_segment(take, seg_lo, seg_hi):
|
|
564
586
|
nat = seg_lo if inner == lo else seg_hi
|
|
565
|
-
anch = (0.0, nat) if axis == "y" else (nat, 0.0)
|
|
566
587
|
# Keys order the tiers so the FIRST candidate lands on the inner tier: for an
|
|
567
588
|
# inner=lo strip that is the lowest position (ascending keys); for a below strip
|
|
568
589
|
# (inner=hi) it is the highest, so the keys reverse.
|
|
@@ -570,9 +591,14 @@ def place_strip_candidates(
|
|
|
570
591
|
(
|
|
571
592
|
StripCandidate(
|
|
572
593
|
f"{(k if inner == lo else len(take) - 1 - k):04d}",
|
|
573
|
-
|
|
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
|
+
),
|
|
574
599
|
(sizes or {}).get(nb[0], (tier, tier)),
|
|
575
600
|
priority=(priorities or {}).get(nb[0], 0.0),
|
|
601
|
+
anchored=(anchored or {}).get(nb[0], False),
|
|
576
602
|
),
|
|
577
603
|
nb,
|
|
578
604
|
)
|
|
@@ -59,7 +59,7 @@ from draftwright.annotations._common import (
|
|
|
59
59
|
strip_obstacles,
|
|
60
60
|
)
|
|
61
61
|
from draftwright.layout import StripCandidate, plan_strip
|
|
62
|
-
from draftwright.model.ir import HoleFeature, PatternFeature
|
|
62
|
+
from draftwright.model.ir import AUTHORED_DIMENSION_KINDS, HoleFeature, PatternFeature
|
|
63
63
|
from draftwright.model.planner import DimensionGroup, plan_locations
|
|
64
64
|
|
|
65
65
|
|
|
@@ -361,15 +361,32 @@ def render_slots(dwg, model, a, *, only=None) -> int:
|
|
|
361
361
|
# by datum distance). Segregating the two runs keeps a slot length from landing mid-ladder.
|
|
362
362
|
_SIZE_SUBCHAIN = 0
|
|
363
363
|
_LOC_SUBCHAIN = 1
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
364
|
+
_OVERALL_SUBCHAIN = 2
|
|
365
|
+
_MANDATORY_OVERALL_PRIORITY = 100.0
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _location_candidate(
|
|
369
|
+
dwg,
|
|
370
|
+
name,
|
|
371
|
+
*,
|
|
372
|
+
view,
|
|
373
|
+
span_key,
|
|
374
|
+
distance,
|
|
375
|
+
build,
|
|
376
|
+
feature=None,
|
|
377
|
+
pinned=False,
|
|
378
|
+
):
|
|
367
379
|
"""A :class:`CorridorCandidate` for a datum-referenced hole/pattern location dim.
|
|
368
380
|
Location dims outrank a coincident slot-position line in dedup (#345) and form the
|
|
369
381
|
outer, datum-distance-ordered run of the ladder (#346). Force-kept (policy B): a plan-X
|
|
370
382
|
/ side-Y location has no alternate view, so a corridor block keeps it rather than drops
|
|
371
383
|
it; only a physically full strip drops (``location_ref_dropped`` → hole-table escalate)."""
|
|
372
384
|
|
|
385
|
+
def _placed(nm):
|
|
386
|
+
dwg._cover_scattered_hole_doc(nm)
|
|
387
|
+
if pinned:
|
|
388
|
+
dwg.pin(nm)
|
|
389
|
+
|
|
373
390
|
def _drop(nm):
|
|
374
391
|
edge = "plan view" if view == "plan" else "side view"
|
|
375
392
|
dwg._record_build_issue(
|
|
@@ -382,16 +399,17 @@ def _location_candidate(dwg, name, *, view, span_key, distance, build, feature=N
|
|
|
382
399
|
build=build,
|
|
383
400
|
order=(_LOC_SUBCHAIN, distance, name),
|
|
384
401
|
# A placed location may later be replaced by the scattered-hole table (#351 PR-4c).
|
|
385
|
-
on_place=
|
|
402
|
+
on_place=_placed,
|
|
386
403
|
on_drop=_drop,
|
|
387
404
|
dedup=(view, span_key[0], span_key[1]),
|
|
388
|
-
precedence=2,
|
|
405
|
+
precedence=3 if pinned else 2,
|
|
406
|
+
priority=100.0 if pinned else 0.0,
|
|
389
407
|
force=True,
|
|
390
408
|
feature=feature, # provenance (ADR 0010): the located hole/pattern
|
|
391
409
|
)
|
|
392
410
|
|
|
393
411
|
|
|
394
|
-
def render_locations(dwg, model, a, *, only=None) -> int:
|
|
412
|
+
def render_locations(dwg, model, a, *, only=None, pinned=None) -> int:
|
|
395
413
|
"""Baseline X/Y hole-location dims from the IR (#238). The planner decides the
|
|
396
414
|
intent (`plan_locations`: which refs, from which datum); this renderer owns the
|
|
397
415
|
layout (Amendment 4) — X dims tier above the plan view, Y dims above the side
|
|
@@ -402,7 +420,11 @@ def render_locations(dwg, model, a, *, only=None) -> int:
|
|
|
402
420
|
*only*, when given, restricts placement to refs whose source feature is in the set —
|
|
403
421
|
the #426 finalize() path passes the recorded ``locate`` intents' features so the
|
|
404
422
|
corridor solve runs over the user's edited subset. ``None`` (the auto-pass) places
|
|
405
|
-
every ref, byte-identically.
|
|
423
|
+
every ref, byte-identically.
|
|
424
|
+
|
|
425
|
+
*pinned* carries the #511 first slice: deferred user ``locate(..., pin=True)`` calls
|
|
426
|
+
remain first-class corridor candidates, but get higher survival/dedup priority and
|
|
427
|
+
pin their placed names instead of being hand-added after the solve."""
|
|
406
428
|
planned = plan_locations(model)
|
|
407
429
|
if not planned:
|
|
408
430
|
return 0
|
|
@@ -430,6 +452,7 @@ def render_locations(dwg, model, a, *, only=None) -> int:
|
|
|
430
452
|
refs.append((rx, ry, pd.feature)) # carry the source feature for provenance (ADR 0010)
|
|
431
453
|
if not refs:
|
|
432
454
|
return 0
|
|
455
|
+
pinned_set = set(pinned or ())
|
|
433
456
|
tier = draft.font_size + 2 * draft.pad_around_text
|
|
434
457
|
n = 0
|
|
435
458
|
|
|
@@ -452,8 +475,12 @@ def render_locations(dwg, model, a, *, only=None) -> int:
|
|
|
452
475
|
PX, PY = a.proj.plan_x, a.proj.plan_y
|
|
453
476
|
x_refs: list = []
|
|
454
477
|
for r in refs:
|
|
455
|
-
|
|
456
|
-
|
|
478
|
+
for u in x_refs:
|
|
479
|
+
if abs(r[0] - u[0]) < 0.5:
|
|
480
|
+
u[3] = u[3] or r[2] in pinned_set
|
|
481
|
+
break
|
|
482
|
+
else:
|
|
483
|
+
x_refs.append([r[0], r[1], r[2], r[2] in pinned_set])
|
|
457
484
|
_x_drawable = {r[0] for r in x_refs if abs(r[0] - datum_x) * a.SCALE >= 1.0}
|
|
458
485
|
_kept_x, _n_x_close = _legible_locations(_x_drawable, a.SCALE)
|
|
459
486
|
if _n_x_close:
|
|
@@ -472,7 +499,7 @@ def render_locations(dwg, model, a, *, only=None) -> int:
|
|
|
472
499
|
# pass carving around the other and interleaving. No alternate view for a plan-X
|
|
473
500
|
# location, so a corridor-blocked dim is force-kept (policy B), not relocated; only a
|
|
474
501
|
# physically full strip drops (→ location_ref_dropped, escalates the hole table).
|
|
475
|
-
for i, (rx, ry, feat) in enumerate(sorted(x_refs, key=lambda r: abs(r[0] - datum_x))):
|
|
502
|
+
for i, (rx, ry, feat, pin_ref) in enumerate(sorted(x_refs, key=lambda r: abs(r[0] - datum_x))):
|
|
476
503
|
if abs(rx - datum_x) * a.SCALE < 1.0:
|
|
477
504
|
continue # on the datum edge — nothing to dimension
|
|
478
505
|
n += 1
|
|
@@ -502,6 +529,7 @@ def render_locations(dwg, model, a, *, only=None) -> int:
|
|
|
502
529
|
label=_fmt(_rx - datum_x),
|
|
503
530
|
),
|
|
504
531
|
feature=_xfeat,
|
|
532
|
+
pinned=pin_ref,
|
|
505
533
|
),
|
|
506
534
|
)
|
|
507
535
|
|
|
@@ -511,8 +539,12 @@ def render_locations(dwg, model, a, *, only=None) -> int:
|
|
|
511
539
|
iso_x0, iso_y0, _, _ = _iso_bbox(dwg)
|
|
512
540
|
y_refs: list = []
|
|
513
541
|
for r in refs:
|
|
514
|
-
|
|
515
|
-
|
|
542
|
+
for u in y_refs:
|
|
543
|
+
if abs(r[1] - u[1]) < 0.5:
|
|
544
|
+
u[3] = u[3] or r[2] in pinned_set
|
|
545
|
+
break
|
|
546
|
+
else:
|
|
547
|
+
y_refs.append([r[0], r[1], r[2], r[2] in pinned_set])
|
|
516
548
|
_y_drawable = {r[1] for r in y_refs if abs(r[1] - datum_y) * a.SCALE >= 1.0}
|
|
517
549
|
_kept_y, _n_y_close = _legible_locations(_y_drawable, a.SCALE)
|
|
518
550
|
if _n_y_close:
|
|
@@ -528,9 +560,9 @@ def render_locations(dwg, model, a, *, only=None) -> int:
|
|
|
528
560
|
# Cap the side-above strip below the iso view so Y-location dims never run under it
|
|
529
561
|
# (the carve respects outer_limit); the dim_pitch_side dims are obstacles the carve
|
|
530
562
|
# avoids structurally, retiring the old manual allocate(10.0) reservation + cursor.
|
|
531
|
-
if y_refs and any(SX(ry) + 10 > iso_x0 - 4 for _, ry,
|
|
563
|
+
if y_refs and any(SX(ry) + 10 > iso_x0 - 4 for _, ry, _feat, _pin in y_refs):
|
|
532
564
|
a.sv_zones.above.outer_limit = min(a.sv_zones.above.outer_limit, iso_y0 - 4)
|
|
533
|
-
for i, (rx, ry, feat) in enumerate(sorted(y_refs, key=lambda r: abs(r[1] - datum_y))):
|
|
565
|
+
for i, (rx, ry, feat, pin_ref) in enumerate(sorted(y_refs, key=lambda r: abs(r[1] - datum_y))):
|
|
534
566
|
if abs(ry - datum_y) * a.SCALE < 1.0:
|
|
535
567
|
continue
|
|
536
568
|
n += 1
|
|
@@ -558,6 +590,7 @@ def render_locations(dwg, model, a, *, only=None) -> int:
|
|
|
558
590
|
label=_fmt(_ry - datum_y),
|
|
559
591
|
),
|
|
560
592
|
feature=_yfeat,
|
|
593
|
+
pinned=pin_ref,
|
|
561
594
|
),
|
|
562
595
|
)
|
|
563
596
|
return n
|
|
@@ -802,8 +835,8 @@ def _envelope_tier(dwg, strip, view, size):
|
|
|
802
835
|
``allocate`` gave the right order only because an earlier pass had advanced a
|
|
803
836
|
shared cursor; that coupling inverted the moment the location pass moved to
|
|
804
837
|
``plan_strip`` (#321), which never advances the cursor. Reading obstacle boxes
|
|
805
|
-
decouples the two passes.
|
|
806
|
-
|
|
838
|
+
decouples the two passes. The current renderer queues envelope dims into the shared
|
|
839
|
+
corridor instead; #133 mandatory-dim starvation is guarded by envelope priority.
|
|
807
840
|
|
|
808
841
|
Assumes a below/above strip (Y stacking axis) — the only strips ``render_envelope``
|
|
809
842
|
uses; a left/right strip would need the X interval of each obstacle box."""
|
|
@@ -822,55 +855,78 @@ def _envelope_tier(dwg, strip, view, size):
|
|
|
822
855
|
|
|
823
856
|
def render_envelope(dwg, groups, a) -> int:
|
|
824
857
|
"""Overall width (plan, below) + depth (side, below) envelope dims via the IR,
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
suppression (square footprint / X-turned;
|
|
829
|
-
suppressed dims and
|
|
858
|
+
registered into the same below-strip corridor as feature/location/GD&T/PMI candidates.
|
|
859
|
+
The overall dims use the last ladder subchain so they stack outermost by construction,
|
|
860
|
+
while their mandatory priority prevents best-effort below-strip occupants from starving
|
|
861
|
+
principal dimensions. The **planner** decides suppression (square footprint / X-turned;
|
|
862
|
+
#250); this renderer just skips suppressed dims and queues the rest. Returns the count
|
|
863
|
+
queued."""
|
|
830
864
|
env = envelope_group(groups)
|
|
831
865
|
if env is None:
|
|
832
866
|
return 0
|
|
833
867
|
n = 0
|
|
868
|
+
|
|
869
|
+
def _queue(name, strip, view, tier, distance, build):
|
|
870
|
+
register_corridor(
|
|
871
|
+
dwg,
|
|
872
|
+
(view, "below"),
|
|
873
|
+
strip,
|
|
874
|
+
view,
|
|
875
|
+
"y",
|
|
876
|
+
tier,
|
|
877
|
+
CorridorCandidate(
|
|
878
|
+
name=name,
|
|
879
|
+
build=build,
|
|
880
|
+
order=(_OVERALL_SUBCHAIN, distance, name),
|
|
881
|
+
on_place=lambda _nm: None,
|
|
882
|
+
on_drop=lambda _nm: None,
|
|
883
|
+
priority=_MANDATORY_OVERALL_PRIORITY,
|
|
884
|
+
force=True,
|
|
885
|
+
),
|
|
886
|
+
)
|
|
887
|
+
|
|
834
888
|
width = _env_pd(env, "width")
|
|
835
889
|
if env_dim_placed(width):
|
|
836
890
|
(x0, y0, z0), (x1, _, _) = width.param.span
|
|
837
891
|
p1, p2 = dwg.at("plan", x0, y0, z0), dwg.at("plan", x1, y0, z0)
|
|
838
892
|
witness = p1[1] - 2
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
)
|
|
853
|
-
|
|
893
|
+
_queue(
|
|
894
|
+
"m_env_width",
|
|
895
|
+
a.pv_zones.below,
|
|
896
|
+
"plan",
|
|
897
|
+
_SLOT_DIM_WIDTH,
|
|
898
|
+
abs(x1 - x0),
|
|
899
|
+
lambda pos, _p1=p1, _p2=p2, _w=witness, _v=width.param.value: _dim(
|
|
900
|
+
(_p1[0], _w, 0),
|
|
901
|
+
(_p2[0], _w, 0),
|
|
902
|
+
"below",
|
|
903
|
+
_w - pos,
|
|
904
|
+
dwg.draft,
|
|
905
|
+
label=_fmt(_v),
|
|
906
|
+
),
|
|
907
|
+
)
|
|
908
|
+
n += 1
|
|
854
909
|
depth = _env_pd(env, "depth")
|
|
855
910
|
if env_dim_placed(depth):
|
|
856
911
|
(x0, y0, z0), (_, y1, _) = depth.param.span
|
|
857
912
|
p1, p2 = dwg.at("side", x0, y0, z0), dwg.at("side", x0, y1, z0)
|
|
858
913
|
witness = p1[1] - 2
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
)
|
|
873
|
-
|
|
914
|
+
_queue(
|
|
915
|
+
"m_env_depth",
|
|
916
|
+
a.sv_zones.below,
|
|
917
|
+
"side",
|
|
918
|
+
_SLOT_DIM_DEPTH,
|
|
919
|
+
abs(y1 - y0),
|
|
920
|
+
lambda pos, _p1=p1, _p2=p2, _w=witness, _v=depth.param.value: _dim(
|
|
921
|
+
(_p1[0], _w, 0),
|
|
922
|
+
(_p2[0], _w, 0),
|
|
923
|
+
"below",
|
|
924
|
+
_w - pos,
|
|
925
|
+
dwg.draft,
|
|
926
|
+
label=_fmt(_v),
|
|
927
|
+
),
|
|
928
|
+
)
|
|
929
|
+
n += 1
|
|
874
930
|
return n
|
|
875
931
|
|
|
876
932
|
|
|
@@ -1122,7 +1178,7 @@ def _detect_step_repeat(step_zs, bb_min_z, bb_max_z, tol_frac=0.10):
|
|
|
1122
1178
|
return n, mean_rise
|
|
1123
1179
|
|
|
1124
1180
|
|
|
1125
|
-
def render_height_ladder(dwg, model, a) -> int:
|
|
1181
|
+
def render_height_ladder(dwg, model, a, *, include_overall: bool = True) -> int:
|
|
1126
1182
|
"""Front-view right ladder: prismatic step heights (from `StepLevelFeature`)
|
|
1127
1183
|
stacked inner→outer, then the overall height outermost — through `fv_zones.right`,
|
|
1128
1184
|
preserving the leapfrog witness cursor (#237). Replaces the engine's inline
|
|
@@ -1214,7 +1270,7 @@ def render_height_ladder(dwg, model, a) -> int:
|
|
|
1214
1270
|
# — #222).
|
|
1215
1271
|
rot = next((f for f in model.features if f.kind == "rotational"), None)
|
|
1216
1272
|
od_is_height = rot is not None and rot.frame.axis in ("x", "y")
|
|
1217
|
-
suppress_height = model.orientation == "z" or od_is_height
|
|
1273
|
+
suppress_height = (not include_overall) or model.orientation == "z" or od_is_height
|
|
1218
1274
|
px = (
|
|
1219
1275
|
None
|
|
1220
1276
|
if suppress_height
|
|
@@ -1434,9 +1490,9 @@ def _bore_half_span(pmi_kind: str, value: float) -> float:
|
|
|
1434
1490
|
"""Half the perpendicular span of a bore-size dim from the bore centroid — the
|
|
1435
1491
|
distance to each witness base point. A ``"diameter"`` record stores the full
|
|
1436
1492
|
diameter (so half = radius = value/2); a ``"radius"`` record already stores the
|
|
1437
|
-
radius (half = value). Keyed on
|
|
1438
|
-
|
|
1439
|
-
|
|
1493
|
+
radius (half = value). Keyed on the drafting dimension category, NOT ``.kind`` (the
|
|
1494
|
+
IR feature kind) — the #360 bug used the latter, so the diameter branch was dead and
|
|
1495
|
+
every diameter dim spanned ±diameter (2× wide)."""
|
|
1440
1496
|
return value / 2 if pmi_kind == "diameter" else value
|
|
1441
1497
|
|
|
1442
1498
|
|
|
@@ -1447,10 +1503,26 @@ _PMI_SUBCHAIN = 3
|
|
|
1447
1503
|
_PMI_CORRIDOR_PRIORITY = 1.0
|
|
1448
1504
|
|
|
1449
1505
|
|
|
1506
|
+
def _renderable_pmi_records(records):
|
|
1507
|
+
"""PMI records the dimension renderer may place.
|
|
1508
|
+
|
|
1509
|
+
Raw ``PmiFeature`` fallbacks can preserve unsupported AP242 records. Do not render those
|
|
1510
|
+
just because they happen to carry a numeric value and references; only drafting dimension
|
|
1511
|
+
categories belong in this placement path.
|
|
1512
|
+
"""
|
|
1513
|
+
return [
|
|
1514
|
+
r
|
|
1515
|
+
for r in records
|
|
1516
|
+
if r.pmi_kind in AUTHORED_DIMENSION_KINDS and r.value > 0 and len(r.ref_pts) >= 2
|
|
1517
|
+
]
|
|
1518
|
+
|
|
1519
|
+
|
|
1450
1520
|
def render_pmi(dwg, model, a) -> int:
|
|
1451
|
-
"""Render
|
|
1452
|
-
|
|
1453
|
-
|
|
1521
|
+
"""Render imported authored dimensions from concept IR as first-class candidates.
|
|
1522
|
+
|
|
1523
|
+
AP242 dimensional PMI lowers to ``AuthoredDimension``; unsupported raw PMI fallback
|
|
1524
|
+
records still ride as ``PmiFeature`` so they remain visible to diagnostics (#208/#393).
|
|
1525
|
+
Replaces the engine's ``_annotate_pmi``.
|
|
1454
1526
|
|
|
1455
1527
|
Called from ``_auto_annotate`` before ``drain_corridors`` so authored PMI
|
|
1456
1528
|
co-solves with automatic strip candidates. Skips records whose page
|
|
@@ -1464,26 +1536,9 @@ def render_pmi(dwg, model, a) -> int:
|
|
|
1464
1536
|
too compressed in the side view)
|
|
1465
1537
|
"""
|
|
1466
1538
|
draft = dwg.draft
|
|
1467
|
-
pmi = [f for f in model.features if f.kind
|
|
1468
|
-
usable =
|
|
1469
|
-
n_gtol = sum(
|
|
1470
|
-
1
|
|
1471
|
-
for r in pmi
|
|
1472
|
-
if r.pmi_kind
|
|
1473
|
-
not in (
|
|
1474
|
-
"linear",
|
|
1475
|
-
"diameter",
|
|
1476
|
-
"radius",
|
|
1477
|
-
"angular",
|
|
1478
|
-
"curved_dist",
|
|
1479
|
-
"oriented",
|
|
1480
|
-
"curve_length",
|
|
1481
|
-
"thickness",
|
|
1482
|
-
"label",
|
|
1483
|
-
"presentation",
|
|
1484
|
-
)
|
|
1485
|
-
and r.value > 0
|
|
1486
|
-
)
|
|
1539
|
+
pmi = [f for f in model.features if f.kind in ("authored_dimension", "pmi")]
|
|
1540
|
+
usable = _renderable_pmi_records(pmi)
|
|
1541
|
+
n_gtol = sum(1 for r in pmi if r.pmi_kind not in AUTHORED_DIMENSION_KINDS and r.value > 0)
|
|
1487
1542
|
if n_gtol:
|
|
1488
1543
|
_log.debug("PMI annotate: %d gtol/datum record(s) not yet annotatable (Phase 4)", n_gtol)
|
|
1489
1544
|
if not usable:
|
|
@@ -1920,10 +1975,11 @@ def render_pmi(dwg, model, a) -> int:
|
|
|
1920
1975
|
# NOT through the dimension planner (their IR items carry no DimParameters). "note" is a
|
|
1921
1976
|
# free-text manufacturing note (#488) — the same leader-into-a-strip mechanism, glyph = text.
|
|
1922
1977
|
_GDT_KINDS = ("control_frame", "datum_ref", "finish", "note")
|
|
1923
|
-
#
|
|
1924
|
-
# (_SIZE_SUBCHAIN=0)
|
|
1925
|
-
# mid-ladder among
|
|
1926
|
-
|
|
1978
|
+
# Authored-intent run of the shared corridor ladder: GD&T frames tier BEYOND the
|
|
1979
|
+
# feature-size (_SIZE_SUBCHAIN=0), datum-location (_LOC_SUBCHAIN=1), and overall
|
|
1980
|
+
# envelope (_OVERALL_SUBCHAIN=2) dim runs, so a frame never lands mid-ladder among
|
|
1981
|
+
# the dimensions it annotates.
|
|
1982
|
+
_GDT_SUBCHAIN = 3
|
|
1927
1983
|
# Over-capacity survival rank for an authored GD&T frame (#357): a declared control frame /
|
|
1928
1984
|
# datum / finish / note is deliberate intent, so on a strip too full for every candidate it is
|
|
1929
1985
|
# kept over the auto dims (locations/slots, priority 0) rather than dropped by stacking-key order.
|