epiphany/crates/epiphany-engrave
Levi Neuwirth 9539b1652b layout-ir, engrave: ledger lines
Notes above or below the staff now get ledger lines — the gap the GUI surfaced
the moment a note is moved off the staff. Each notehead carries its StaffStep;
to_constrained emits one short horizontal stroke per whole step between the
staff (lines at steps 0..=8) and the note, reaching LEDGER_LINE_EXTENSION past
each side of the note's actual bounding box (so a wide whole note gets a wider
ledger), synthesized from the pitch so the strokes are deterministic and
hit-testable. The synthesis key splits component (high 64 bits) from signed step
(low 64) so two components of a very low note can never collide.

Ledger lines are fixed-width marks, not system-spanning lines, so the Engraver's
horizontal spacing must not scale them. is_rigid_width_stroke marks them; the
remap translates such a stroke rigidly by its *owning glyph's* column delta
(found by source, not the stroke midpoint — which for a wide head can fall nearer
a neighbouring column), so it keeps both its length and its offset from the
notehead. The spacing pass folds each ledger's extent into its notehead's slot,
so adjacent off-staff notes' ledgers reserve room and do not overlap. The stub
solver passes ledgers through unchanged.

Tests cover the step geometry, key distinctness (incl. steps below -128), the
bbox span, width preservation and offset (no-drift) through the Engraver, the
adjacent-overlap spacing, and an explicit two-whole-note off-staff drift case.
Render goldens regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS
2026-06-29 17:12:56 -04:00
..
src layout-ir, engrave: ledger lines 2026-06-29 17:12:56 -04:00
Cargo.toml Agent I-3: criterion-6 round-trip and golden lock on the real Engraver 2026-06-26 21:58:31 -04:00
DECISIONS.md Pass-12 batch: mark the resolved I-series rows done 2026-06-27 12:53:36 -04:00
README.md Phase 2 (Agent I): visible-slice scaffold — Bravura SVG renderer vs stub 2026-06-23 20:01:40 -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: honest scaffold (renderer-against-stub phase)

Per the QUICKSTART development pattern, Agent I builds the SVG renderer (epiphany-render-svg) against the stub solver first, then grows this crate into the real two-pass spring solver. This commit is the first increment:

  • Engraver runs a deterministic horizontal spacing pass (the first axis of the planned two-pass spring layout): each spring slot is placed left-to-right by its preferred width instead of being echoed verbatim.
  • It honestly reports SolverTier::Stub — it does not yet evaluate the IR's declared hard constraints or compute quality metrics, so it has not earned Minimal. It is promoted to Minimal in the change that lands real constraint satisfaction.

The vertical spring pass, soft-constraint solve, hard-constraint evaluation, and the quality-metric vector are the next-phase / Phase-3 work. See DECISIONS.md.

use epiphany_engrave::Engraver;
use epiphany_layout_ir::{ConstraintSolver, SolverConfig};

let report = Engraver.solve(&constrained_ir, &SolverConfig::default());
assert!(report.satisfied_hard_constraints); // for constraint-free stub-pipeline input
let resolved = report.layout;               // hand to epiphany-render-svg