epiphany/crates/epiphany-engrave
Levi Neuwirth 586969125e Push 3 (slur quality) 2/3: split break-spanning slur curves (de Casteljau)
E2 drew a slur spanning a system break whole in its start system, with a
floating end control point detached from its end note (the documented Minimal
boundary). Casting now SPLITS such a curve into per-system sub-curves.

curve_system → curve_fate, mirroring stroke_fate: a curve that fits in one
system rides it whole (CurveFate::Rigid, byte-identical to before); a curve
overlapping ≥2 systems' clip intervals splits (CurveFate::Split) into one
sub-cubic per system, cut at each system's content clip edges. The cut uses de
Casteljau subdivision (sub_cubic = two splits: take [0,t1], then its [t0/t1,1]
tail) at the parameters param_at_x finds by bisecting the x-monotonic curve
(a slur's control points are x-ascending by construction; a non-monotonic curve
— not engraver-produced — falls back to riding its start system whole). The
first segment carries the slur's exact provenance (the round-trip surjection
recovers the source once); later segments synthesize continuations under
SYSTEM_CONTINUATION_SYNTHESIS with a (stable_id, ordinal) key, as split strokes
do. Each segment's control hull grows its own system's extent.

ENGRAVER_VERSION 6→7 (a break-spanning slur's baked geometry differs); a slur
that fits in one system is unchanged, so the fixture goldens are byte-identical
(its slurs are short). Tests: sub_cubic reproduces the original curve on its
sub-range + param_at_x inverts x; a whole-score slur splits into ≥2 segments
across distinct system y-bands with correct provenance. 934 tests, 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:21:19 -04:00
..
src Push 3 (slur quality) 2/3: split break-spanning slur curves (de Casteljau) 2026-07-08 17:21:19 -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 Push 3 (slur quality) 2/3: split break-spanning slur curves (de Casteljau) 2026-07-08 17:21:19 -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