Casting-off's greedy first-fit + tail-only widow rebalance is replaced by a
deterministic badness-minimizing break search (optimal_breaks, a Knuth-Plass-
style dynamic program over the measure boundaries). ENGRAVER_VERSION 9 -> 10.
Objective: minimize the sum over ALL systems of the squared normalized underfill
((width_limit - w)/width_limit)^2. Squaring evens the systems; including the
FINAL system in the sum subsumes the old widow rebalance (the optimizer won't
leave a narrow final stub if a balanced partition is cheaper). It is the
additive, DP-tractable analog of the retired distribution_cost (max of the
catalog's break penalty and width-CV imbalance). On the ten-measure fixture the
search settles on 5/4 measures where greedy left a fuller-then-shorter split,
filling the final system more and pulling casting_off_quality down (~0.80 ->
~0.61) — the payoff, visible now that horizontal justification drives
system_break to ~0 so casting_off mostly sees the last system's fullness.
Break requirements (hard/soft/page) bound the DP's segments — a system may not
span a forced break — and walk_region still honours them and records skipped
content-less soft breaks as IrOverride, unchanged; optimal_breaks reports only
the automatic breaks. A system may exceed the width only as a single
unsplittable measure. Minimal still makes no optimality claim.
Deterministic: minimizes lexicographic (cost, system_count). Tests:
optimal_breaks_{balances_systems_and_avoids_a_final_widow, never_spans_a_forced_
break, is_deterministic_and_empty_when_unbounded}; the widow test now checks the
balanced measure distribution; wrapping-fixture metrics updated (casting_off
improved). Removed rebalance_widows/rebalance_region/distribution_cost + their
tests. Goldens regenerated (balanced systems; view_box stable — justification
still fills to width). 944 tests, clippy 0, docs -D warnings, conformance 8/8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| src | ||
| Cargo.toml | ||
| DECISIONS.md | ||
| README.md | ||
README.md
epiphany-engrave
Agent I's engraving constraint solver (spec Chapter 9): turns a
ConstrainedLayoutIR into a ResolvedLayoutIR with real geometry. It is the
production-side replacement for epiphany-layout-ir's interface-only
StubSolver; the two live in separate crates so the spec's core/product boundary
stays sharp.
Status: Minimal tier, with casting-off
Engraverruns a deterministic horizontal spacing pass (the first axis of the two-pass spring layout): each spring slot is placed left-to-right by a collision-aware advance derived from real glyph bearings.- A casting-off pass (Phase 3's layout track) then breaks the spaced line
into systems at measure boundaries (greedy first-fit against a
PageGeometry— default A4 portrait at an 8 mm staff), stacks systems vertically at the vertical-band model's inter-system gap, assigns pages by content height, and populates the realResolvedPage/ResolvedSystemtree. Every position is baked into a single y-up world frame (pages stacked vertically), so the SVG renderer and hit-testing consume the flat glyph/stroke lists unchanged. - The IR's declared constraints are evaluated — geometric families against
the pre-casting spaced frame, break constraints against the final break
structure (hard breaks are always honoured; a pathological soft break is
skipped and recorded as an
IrOverridedecision). Chosen breaks are recorded asEngravingDecisions withSynthesisKind::EngravedBreaktargets, attributed to the user override that requested them when one did. - It reports
SolverTier::Minimal: hard constraints (break family included) satisfied, no optimality claim — the quality-metric vector stays the honest all-worst placeholder until the Quality Metric Catalog lands.
Deferred: the vertical soft-spring solve, per-system justification/stretch,
optimal break search, widow/orphan control, and casting-off caching. See
DECISIONS.md.
use epiphany_engrave::Engraver;
use epiphany_layout_ir::{ConstraintSolver, SolverConfig};
let report = Engraver::default().solve(&constrained_ir, &SolverConfig::default());
assert!(report.satisfied_hard_constraints);
let resolved = report.layout; // real pages/systems; hand to epiphany-render-svg