Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/adr/0004-compose-then-pack-view-blocks.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ADR 0004 — Compose-then-pack: views as blocks carrying their annotation footprint

- **Status:** Accepted (2026-06-19; amended 2026-06-20, 2026-07-09,
2026-07-18 and 2026-08-14 — see Amendments). **Its fixed four-view topology
2026-07-18, 2026-08-14, 2026-08-23 and 2026-08-25 — see Amendments). **Its fixed four-view topology
assumption is superseded by [ADR 0018](0018-requirement-driven-view-planning-and-editable-sheet-layout.md)
(accepted 2026-08-16)**: which view blocks exist is now 0018's decision. Everything
else here stands and is what 0018 builds on — each selected view is still composed
Expand Down Expand Up @@ -346,3 +346,30 @@ This is post-build verification, not a second occupancy-based fitness function:
The box-math search and bounded measured-repack loop remain the layout authorities. This
amendment permits a bounded semantic corrective trial around a known conservative recovery artifact;
it does not license arbitrary post-build bbox optimisation.

## Amendment (2026-08-25) — the sheet is not the first lever (#1338)

When an automatic plan is incomplete — an axial-coverage gap, or a required annotation
outcome dropped — the bounded larger-scale trial on the **already-selected page** now runs
**before** the optional ISO is removed and before any larger sheet is tried. It is the same
bounded trial the 2026-08-23 amendment introduced for a recovery detail, and it is now
literally the same helper; only the trigger and the gate set differ.

The order matters because the previous recovery sequence — drop the optional ISO, then
escalate the sheet — could return a candidate strictly worse than one it never reached.
GRM-03 (28.7 × 10 × 10 mm) settled on 5:1/A3 *without* its ISO while 5:1/**A4** is clean
*with* it; the synthetic five-step profile behind #1299 did the same.

The constraints of the 2026-08-23 amendment carry over unchanged: the candidate is confined
to the settled page and arrangement, is compiled through the ordinary pipeline, shares the
hard two-candidate work budget, wins only by passing the same structural, required-outcome
and (when the failure was axial) axial-coverage gates the larger sheet would have had to
pass, and every attempt, rejection and winner is recorded in the structured scale decision.
An explicitly requested page pins the **sheet**, not the scale, so the trial still applies
there — it fills the sheet the caller chose instead of returning an incomplete layout on it.

What this amendment does **not** settle: raising the drawing scale is still the only lever
the ladder has for a placement shortage, and scale is a statement about the part, not a
layout knob. Text height is fixed in page mm, so every step up this ladder buys annotation
room by shrinking the text relative to the geometry. Bounding what a single dropped
annotation may buy in scale and sheet size is open in #1336.
5 changes: 0 additions & 5 deletions docs/multi-feature-object-reference-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,6 @@ sheet.dimension(hole1, "bore.diameter")
sheet.dimension(hole1, "bore.depth")
# ... twelve more dimension lines ...

sheet.authored_views()
sheet.view("front")
sheet.view("plan")
sheet.view("side")

drawing = sheet.build()
drawing.export('thumbwheel', formats=('pdf',))
```
Expand Down
93 changes: 67 additions & 26 deletions src/draftwright/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1797,23 +1797,17 @@ def _try_larger_standard_pages(
)
return None, None

# #1155: the compose-time estimate conservatively reserves an enlarged
# detail for a crowded run. Some larger preferred scales make that run
# readable inline, so the detail reservation disappears and the same page
# becomes feasible — GRM-04 is 2:1 under the estimate but complete at 5:1
# after its Y location re-homes from side-below to plan-right. Measure
# those larger candidates only when the settled result actually contains
# that semantic recovery artifact: post-build occupied rectangles are not
# a scale-selection input. A candidate may win only on the same sheet and
# settled arrangement, with no recovery detail or required placement loss.
if dimensions_are_automatic and views_are_automatic and _has_detail_view(drawing.views):
_record_attempt(
drawing.scale,
"detail_reservation_conservative",
reason="measured_upscale",
candidate=drawing,
)
candidate_scales = sorted(item for item in _SCALES if item > original_scale)[
def _try_larger_scales_on_selected_page(starting_scale, *, reason, require_axial_coverage):
"""Try the bounded larger-scale tail on the ALREADY SELECTED sheet.

The sheet is not the first lever. Raising the scale spreads the features apart
on the page the automatic selection already chose, so a placement shortage that
a larger sheet would clear can often be cleared without changing the sheet the
shop receives, and without spending an optional view for it (#1338). Each
candidate passes the same structural, required-outcome and (when the failure
was an axial one) axial-coverage gates as any other attempt.
"""
candidate_scales = sorted(item for item in _SCALES if item > starting_scale)[
:_AUTOMATIC_UPSCALE_TRIAL_LIMIT
]
for candidate_scale in candidate_scales:
Expand All @@ -1827,41 +1821,69 @@ def _try_larger_standard_pages(
if not _is_expected_candidate_build_failure(exc):
raise
_log.info(
"measured upscale %s:1 rejected (candidate build failed: %s)",
"%s %s:1 rejected (candidate build failed: %s)",
reason,
candidate_scale,
exc,
)
_record_attempt(
candidate_scale,
"error",
reason="measured_upscale",
reason=reason,
views=drawing.views,
page=original_page,
error=str(exc),
)
continue
candidate_drawing = _retain_arrangement(candidate_drawing)
assert (candidate_drawing.page_w, candidate_drawing.page_h) == original_page
issues, blockers, rejection = _qualify_candidate(candidate_drawing)
issues, blockers, rejection = _qualify_candidate(
candidate_drawing,
require_axial_coverage=require_axial_coverage,
)
if rejection is None:
_record_attempt(
candidate_scale,
"complete",
reason="measured_upscale",
reason=reason,
candidate=candidate_drawing,
)
drawing = candidate_drawing
settled_issues = issues
replanned = True
break
return candidate_drawing, issues
_record_attempt(
candidate_scale,
"rejected",
blockers,
reason="measured_upscale",
reason=reason,
rejection=rejection,
candidate=candidate_drawing,
)
return None, None

# #1155: the compose-time estimate conservatively reserves an enlarged
# detail for a crowded run. Some larger preferred scales make that run
# readable inline, so the detail reservation disappears and the same page
# becomes feasible — GRM-04 is 2:1 under the estimate but complete at 5:1
# after its Y location re-homes from side-below to plan-right. Measure
# those larger candidates only when the settled result actually contains
# that semantic recovery artifact: post-build occupied rectangles are not
# a scale-selection input. A candidate may win only on the same sheet and
# settled arrangement, with no recovery detail or required placement loss.
if dimensions_are_automatic and views_are_automatic and _has_detail_view(drawing.views):
_record_attempt(
drawing.scale,
"detail_reservation_conservative",
reason="measured_upscale",
candidate=drawing,
)
upscaled, upscaled_issues = _try_larger_scales_on_selected_page(
original_scale,
reason="measured_upscale",
require_axial_coverage=False,
)
if upscaled is not None:
drawing = upscaled
settled_issues = upscaled_issues
replanned = True

# #443/#1299: a pictorial view is useful context, but it cannot outrank the
# dimensions or other required annotations needed to manufacture a part.
Expand Down Expand Up @@ -1892,6 +1914,7 @@ def _try_larger_standard_pages(
blocker for blocker in original_blockers if blocker["source_ids"]
)
settled_issues = original_issues
recovered_on_selected_page = False
if original_has_axial_gap or source_blockers:
_record_attempt(
drawing.scale,
Expand All @@ -1904,6 +1927,24 @@ def _try_larger_standard_pages(
reason="remove_optional_iso",
candidate=drawing,
)
# #1338: before spending the optional ISO and then the sheet, try the
# bounded larger-scale tail on the page already selected. GRM-03 settled
# on 5:1/A3 without its ISO while 5:1/A4 is clean WITH it — a strictly
# better candidate the ladder never reached, because its only recovery
# order was drop-the-ISO then escalate-the-page. The gates are unchanged:
# this wins only by passing the same axial and required-outcome checks the
# larger sheet would have had to pass.
upscaled, upscaled_issues = _try_larger_scales_on_selected_page(
drawing.scale,
reason="scale_escalation_on_selected_page",
require_axial_coverage=True,
)
if upscaled is not None:
drawing = upscaled
settled_issues = upscaled_issues
replanned = True
recovered_on_selected_page = True
if (original_has_axial_gap or source_blockers) and not recovered_on_selected_page:
try:
without_iso_proposal = _build(
None,
Expand Down
Loading