draftwright 0.2.11__tar.gz → 0.2.13__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.
Files changed (101) hide show
  1. {draftwright-0.2.11 → draftwright-0.2.13}/CHANGELOG.md +70 -0
  2. {draftwright-0.2.11 → draftwright-0.2.13}/PKG-INFO +5 -5
  3. {draftwright-0.2.11 → draftwright-0.2.13}/README.md +4 -4
  4. {draftwright-0.2.11 → draftwright-0.2.13}/pyproject.toml +1 -1
  5. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/analysis.py +44 -0
  6. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/annotations/_common.py +29 -3
  7. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/annotations/from_model.py +140 -84
  8. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/annotations/holes.py +106 -29
  9. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/annotations/orchestrator.py +14 -31
  10. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/builder.py +73 -23
  11. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/drawing.py +288 -56
  12. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/linting/suggest.py +13 -9
  13. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/model/__init__.py +4 -0
  14. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/model/detect.py +28 -9
  15. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/model/ir.py +53 -9
  16. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/sheet.py +165 -69
  17. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/sheet_dsl.py +73 -1
  18. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/sheet_emit.py +90 -13
  19. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/dshape.json +3 -3
  20. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/side_drilled.json +3 -3
  21. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_make_drawing.py +461 -22
  22. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_pmi.py +30 -9
  23. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_render_seam.py +34 -11
  24. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_sheet_emit.py +160 -15
  25. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_strip_layout.py +66 -0
  26. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_tolerances.py +16 -0
  27. {draftwright-0.2.11 → draftwright-0.2.13}/.gitignore +0 -0
  28. {draftwright-0.2.11 → draftwright-0.2.13}/LICENSE +0 -0
  29. {draftwright-0.2.11 → draftwright-0.2.13}/skills/SKILL.md +0 -0
  30. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/__init__.py +0 -0
  31. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/_core.py +0 -0
  32. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/annotate.py +0 -0
  33. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/annotations/__init__.py +0 -0
  34. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/annotations/sections.py +0 -0
  35. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/cli.py +0 -0
  36. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/export.py +0 -0
  37. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/features.py +0 -0
  38. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/fits.py +0 -0
  39. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/fonts/IBMPlexMono-Regular.ttf +0 -0
  40. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/fonts/IBMPlexSansCondensed-Regular.ttf +0 -0
  41. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/fonts/LICENSE-IBMPlexMono-OFL.txt +0 -0
  42. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/fonts/LICENSE-IBMPlexSansCondensed-OFL.txt +0 -0
  43. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/fonts/__init__.py +0 -0
  44. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/intents.py +0 -0
  45. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/layout.py +0 -0
  46. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/linting/__init__.py +0 -0
  47. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/linting/coverage.py +0 -0
  48. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/linting/issues.py +0 -0
  49. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/linting/structural.py +0 -0
  50. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/make_drawing.py +0 -0
  51. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/model/declare.py +0 -0
  52. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/model/planner.py +0 -0
  53. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/pmi.py +0 -0
  54. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/projection.py +0 -0
  55. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/recognition/__init__.py +0 -0
  56. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/recognition/_features.py +0 -0
  57. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/recognition/levels.py +0 -0
  58. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/recognition/slots.py +0 -0
  59. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/recognition/turned.py +0 -0
  60. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/registry.py +0 -0
  61. {draftwright-0.2.11 → draftwright-0.2.13}/src/draftwright/repair.py +0 -0
  62. {draftwright-0.2.11 → draftwright-0.2.13}/tests/_kernel.py +0 -0
  63. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_01_asme1_ap203.stp +0 -0
  64. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_01_asme1_ap242.stp +0 -0
  65. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_02_asme1_ap203.stp +0 -0
  66. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_02_asme1_ap242.stp +0 -0
  67. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_03_asme1_ap203.stp +0 -0
  68. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_03_asme1_ap242.stp +0 -0
  69. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_04_asme1_ap203.stp +0 -0
  70. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_04_asme1_ap242.stp +0 -0
  71. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_05_asme1_ap203.stp +0 -0
  72. {draftwright-0.2.11 → draftwright-0.2.13}/tests/fixtures/nist_ctc_05_asme1_ap242.stp +0 -0
  73. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/box.json +0 -0
  74. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/bracket.json +0 -0
  75. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/drive_screw_x.json +0 -0
  76. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/flange.json +0 -0
  77. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/holed_slot.json +0 -0
  78. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/plate_holes.json +0 -0
  79. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/slotted.json +0 -0
  80. {draftwright-0.2.11 → draftwright-0.2.13}/tests/layout_snapshots/turned_shaft.json +0 -0
  81. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_declare.py +0 -0
  82. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_e2e_slice.py +0 -0
  83. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_e2e_standards.py +0 -0
  84. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_fits.py +0 -0
  85. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_gdt_placement.py +0 -0
  86. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_layout.py +0 -0
  87. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_layout_cleanliness.py +0 -0
  88. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_layout_property.py +0 -0
  89. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_layout_snapshot.py +0 -0
  90. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_lint_reconciliation.py +0 -0
  91. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_lint_structural.py +0 -0
  92. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_linting.py +0 -0
  93. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_object_aspects.py +0 -0
  94. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_part_model.py +0 -0
  95. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_recognition.py +0 -0
  96. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_registry.py +0 -0
  97. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_sheet_gdt.py +0 -0
  98. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_sheet_notes.py +0 -0
  99. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_sheet_of.py +0 -0
  100. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_sheet_tables.py +0 -0
  101. {draftwright-0.2.11 → draftwright-0.2.13}/tests/test_turned_steps.py +0 -0
@@ -1,5 +1,75 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.13 — 2026-07-09
4
+
5
+ **Compose-then-pack is now the layout authority.** This patch finishes the
6
+ ViewBlock footprint migration: estimated and measured layout paths now use the
7
+ same composed block model, furniture declarations reserve their real footprint,
8
+ and measure/repack iterates until stable instead of trusting one measured pass.
9
+
10
+ ### Changed
11
+
12
+ - **ViewBlock footprints now drive the orthographic stack.** The old
13
+ estimator-only plan-view balloon lift is gone; plan balloon headroom is part
14
+ of the plan block in both estimated and measured/repack layout. (#112)
15
+ - **Measure/repack iterates to a fixed point.** Repack now keeps measuring and
16
+ repacking while view-owned footprints continue to grow, with bounded
17
+ convergence and tests for multi-pass growth. (#302)
18
+ - **Estimated view-block composition is explicit.** Scale/page fitness, repack,
19
+ and furniture placement share the same composed block semantics instead of
20
+ rebuilding corridor estimates independently.
21
+
22
+ ### Fixed
23
+
24
+ - **Declared callout furniture reserves layout footprint.** User-declared
25
+ furniture is now measured and fed into the layout model, reducing post-hoc
26
+ collisions from declarations that previously existed only at render time.
27
+ - **Annotation-box composition owns strip sizing.** Strip depths reduce from
28
+ composed annotation boxes, making footprint sizing less dependent on divergent
29
+ scalar estimates.
30
+
31
+ ### Documentation
32
+
33
+ - **Layout architecture docs reflect the current state.** ADR 0004 and the
34
+ layout primer now mark the core layout-authority tranche complete and separate
35
+ remaining work into hardening, coverage, detail-view, and manual-intent phases.
36
+
37
+ ## v0.2.12 — 2026-07-09
38
+
39
+ **Sheet scripts now round-trip authored layout intent.** This patch finishes the next
40
+ slice of the layout-authority work: below/right ladders, pinned edits, height ladders,
41
+ detected envelopes, and AP242 authored dimensions now survive the declarative Sheet path
42
+ without being invalidated by post-hoc placement or raw imported metadata.
43
+
44
+ ### Added
45
+
46
+ - **Pinned edit intents route through the corridor solve.** `locate(..., pin=True)` and
47
+ pinned dimensions now become priority-ranked candidates in the shared corridor instead
48
+ of fixed post-hoc edits, so user intent participates in the same ordering/spacing model
49
+ as automatic dimensions. (#511)
50
+ - **AP242 dimensional PMI lowers to authored drafting dimensions.** Imported AP242
51
+ size/location dimensions now become `AuthoredDimension` IR and generated Sheet scripts
52
+ emit `sheet.dimension(...)`; unsupported PMI records remain explicit raw fallbacks.
53
+ (#503, #422, #62)
54
+
55
+ ### Changed
56
+
57
+ - **`place_dim` is deprecated.** Manual dimensions should use pinned candidate/dimension
58
+ intent instead of the old incremental edit path.
59
+ - **Prismatic height ladders and detected envelopes round-trip through Sheet scripts.**
60
+ Generated scripts now preserve `StepLevelFeature` and detected `EnvelopeFeature`
61
+ fallbacks, preventing CTC01-style ladder swaps and raw STEP-envelope remeasurement
62
+ drift.
63
+
64
+ ### Fixed
65
+
66
+ - **Below/right corridors now share one solve.** Side-hole locations, height ladders,
67
+ PMI/GD&T, envelope dims, and related below/right strip occupants negotiate in the shared
68
+ corridor rather than competing through separate late passes. (#477)
69
+ - **Sheet script parity is tighter.** Generated scripts preserve member positions, step
70
+ levels, AP242 authored dimensions, raw PMI fallbacks, and detected envelopes so
71
+ direct-vs-script CTC01 output has matching annotation names.
72
+
3
73
  ## v0.2.11 — 2026-07-08
4
74
 
5
75
  **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.11
3
+ Version: 0.2.13
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 (auto-placed):
846
- for f in dwg.features("plan"):
847
- dwg.place_dim(f.page_pos, (f.page_pos[0] + f.diameter, f.page_pos[1]),
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 (auto-placed):
153
- for f in dwg.features("plan"):
154
- dwg.place_dim(f.page_pos, (f.page_pos[0] + f.diameter, f.page_pos[1]),
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draftwright"
7
- version = "0.2.11"
7
+ version = "0.2.13"
8
8
  description = "Automated technical-drawing generation for build123d"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -14,6 +14,7 @@ import logging
14
14
  import math
15
15
  import warnings
16
16
  from collections.abc import Callable
17
+ from dataclasses import replace
17
18
 
18
19
  from build123d import Compound, Shape
19
20
  from build123d_drafting.helpers import draft_preset
@@ -31,6 +32,8 @@ from draftwright._core import (
31
32
  _legible_steps,
32
33
  _Projector,
33
34
  )
35
+ from draftwright.model.ir import Datum, PartModel, StepFeature
36
+ from draftwright.model.planner import plan_dimensions
34
37
  from draftwright.recognition import (
35
38
  analyse_cylinders,
36
39
  analyse_face_levels,
@@ -45,6 +48,7 @@ from draftwright.sheet import (
45
48
  StripDepths,
46
49
  _build_zones,
47
50
  _est_hole_table_sizes,
51
+ _est_planned_bore_callout_width,
48
52
  _layout_geometry,
49
53
  _measure_strips,
50
54
  _will_section,
@@ -96,6 +100,32 @@ def _declared_will_section(model, *, is_rotational=False, cx=0.0, cy=0.0) -> boo
96
100
  return False
97
101
 
98
102
 
103
+ def _coerce_layout_model(model, part, decorations=None) -> PartModel | None:
104
+ """Return the caller-declared IR with authored decorations for layout sizing.
105
+
106
+ This mirrors the builder's render-time coercion, but stays local to analysis so
107
+ page/scale/strip selection can see the same authored callout text the renderer
108
+ will later place (#450).
109
+ """
110
+ if model is None:
111
+ return None
112
+ if isinstance(model, PartModel):
113
+ if decorations:
114
+ return replace(model, decorations={**model.decorations, **decorations})
115
+ return model
116
+ features = list(model)
117
+ bbox = part.bounding_box()
118
+ orientation = next((f.frame.axis for f in features if isinstance(f, StepFeature)), None)
119
+ datum = Datum(id="datum_xy", kind="point", at=(bbox.min.X, bbox.min.Y, bbox.min.Z))
120
+ return PartModel(
121
+ bbox=bbox,
122
+ orientation=orientation,
123
+ features=features,
124
+ datums=[datum],
125
+ decorations=decorations or {},
126
+ )
127
+
128
+
99
129
  def _import_step(path) -> Compound:
100
130
  """Read solid geometry from a STEP file via OCCT's ``STEPControl_Reader``.
101
131
 
@@ -299,6 +329,7 @@ def _analyse(
299
329
  page=None,
300
330
  pmi="off",
301
331
  model=None,
332
+ decorations=None,
302
333
  ) -> Analysis:
303
334
  """Load STEP or use a build123d Shape, analyse geometry, compute layout.
304
335
 
@@ -409,6 +440,17 @@ def _analyse(
409
440
  _draft_est = draft_preset(font_size=_FONT_SIZE, decimal_precision=1)
410
441
  _arrow_length = _draft_est.arrow_length
411
442
  _pad_around_text = _draft_est.pad_around_text
443
+ layout_model = _coerce_layout_model(model, part, decorations)
444
+ declared_bore_width = (
445
+ _est_planned_bore_callout_width(
446
+ plan_dimensions(layout_model),
447
+ _draft_est,
448
+ font_size=_FONT_SIZE,
449
+ pad_around_text=_pad_around_text,
450
+ )
451
+ if layout_model is not None
452
+ else 0.0
453
+ )
412
454
  holes = find_holes(part, cyls=(z_cyls, cross_cyls))
413
455
  patterns = find_hole_patterns(holes)
414
456
  bosses = find_bosses(part, cyls=(z_cyls, cross_cyls)) # detect once — the one inventory (#264)
@@ -442,6 +484,7 @@ def _analyse(
442
484
  bb,
443
485
  arrow_length=_arrow_length,
444
486
  pad_around_text=_pad_around_text,
487
+ declared_bore_callout_width=declared_bore_width,
445
488
  )
446
489
 
447
490
  def _pick_for_step_count(n_steps_i: int, strips_i: StripDepths) -> _ScalePick:
@@ -518,6 +561,7 @@ def _analyse(
518
561
  bb,
519
562
  arrow_length=_arrow_length,
520
563
  pad_around_text=_pad_around_text,
564
+ declared_bore_callout_width=declared_bore_width,
521
565
  )
522
566
  # View positions + iso empty-rectangle, shared with scale selection (_fits)
523
567
  # via _layout_geometry so placement and fit never diverge (#11). _fit_iso_view
@@ -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, tier)``."""
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
- anch,
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
  )