epiphany/crates/epiphany-engrave
Levi Neuwirth 3978225e06 Inter-staff slice 2/2: renegotiate the gaps between a system's staves
The last vertical-spring piece: the gaps BETWEEN a system's staves are
renegotiated so tightly ledgered or slurred adjacent staves — which the
constrained stage stacks at a fixed pitch — separate. ENGRAVER_VERSION 10 -> 11
(a multi-staff score whose staves press together shifts them apart; a
single-staff score, with no inter-staff pair, is byte-identical).

Attribution (vertical_band + owning-glyph, per the chosen approach): a glyph via
its vertical_band (VerticalBandKind::Staff -> StaffId); a stem/ledger via its
notehead (component_glyph); a staff line via its Staff source; a slur via the
notehead nearest its start. Spacing is horizontal-only, so a primitive's y is
unchanged from the source frame the attribution reads.

The solve: per system, per staff, collect the real content y-extent (glyphs,
strokes, curves — ledgers and slurs included); order staves top-to-bottom by
their staff-line reference y (order fixed); shift each staff down by the
cumulative amount needed to bring its gap to the one above up to the band
model's preferred inter-staff gap. staff_shift[(system, staff)] is a per-staff
dy the bake applies (Placement::sunk) atop the per-system dy, so glyphs,
strokes, curves, the staff/measure/system records, content bounds, hit-test, and
quality metrics all read the same shifted geometry. The shifts grow each
system's extent, which the vertical stacking and justification then consume.

Regression inter_staff_solve_separates_colliding_staves (the two-staff fixture's
staff-line gap opens past the fixed pitch; a single-staff score keeps one staff
per system) + the two_staff_close_content render golden updates (slice 1 tight,
slice 2 separated). Only that engrave golden churned; single-staff goldens
byte-stable. 947 tests, clippy 0, docs -D warnings, conformance 8/8.

This completes the Standard-tier layout story end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 23:04:25 -04:00
..
src Inter-staff slice 2/2: renegotiate the gaps between a system's staves 2026-07-08 23:04:25 -04:00
Cargo.toml Phase 3 tranche 1: casting-off, K1 schema-fill, value-restoring undo 2026-07-02 21:55:26 -04:00
DECISIONS.md Inter-staff slice 2/2: renegotiate the gaps between a system's staves 2026-07-08 23:04:25 -04:00
README.md Phase 3 tranche 1: casting-off, K1 schema-fill, value-restoring undo 2026-07-02 21:55:26 -04:00

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

  • Engraver runs 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 real ResolvedPage/ResolvedSystem tree. 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 IrOverride decision). Chosen breaks are recorded as EngravingDecisions with SynthesisKind::EngravedBreak targets, 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