The rendered slurs were wrong in three independent ways, all visible in the
two-staff and three-staff goldens.
1. Side. SlurDirection::Auto always arced above. The single-voice rule is
OPPOSITE the stems -- all stems up puts the slur under the noteheads, all
down puts it over them, and a mixed-stem span (which has no notehead side)
goes above. Every Auto slur over a stem-up passage was drawn through its own
stems. This is why stem direction had to land first: with every stem pointing
up, "opposite the stems" means nothing.
2. Endpoints. They sat at staff_top + gap -- a constant offset from the STAFF,
not from the notes -- so a slur between two C6s hung below its own noteheads
and crossed their ledger lines. They now sit a gap outside the endpoint
column's ink, at the notehead's centre. Where the stem points the same way as
the slur, that ink includes the stem, so the endpoint clears the stem tip.
3. Clearance. The apex was span-proportional and blind, so a note between the
endpoints poked straight through the arc. ColumnInk -- per staff, per column:
top, bottom, stem direction, notehead centre -- is the obstacle field. The
control points sit on the chord at thirds, so x is exactly linear in t and
the arc's departure from the chord is 3*lift*t*(1-t); a column at t needing d
more clearance forces an apex of at least d/(4*t*(1-t)).
An authored height is a floor, not a ceiling: clearance may raise it, so obeying
an author cannot draw a slur through a note. An authored direction still wins.
Obstacles are measured at the notehead CENTRE, the same x the endpoints use. The
first cut used the raw column x, which skews t and silently over-lifts: the
two-staff slur cleared its C6 by 4.05 spaces where 3.5 was needed. The clearance
test now asserts an upper bound as well as a lower one.
SLUR_INSET is gone. Endpoints at the notehead centres are what its 0.6-space
"tuck" approximated for the start point -- and got wrong for the end, where it
tucked a full notehead width to the LEFT of the final note.
Four mutations verified: always-above, staff-relative endpoints, no clearance
pass, and obstacles at the column x. The staff-relative-endpoint mutation PASSED
at first -- the tests asserted only "above the staff" / "below the staff", which a
staff-relative endpoint satisfies by construction. The exact-endpoint assertion
exists because that mutation survived.
Projection change, so no version moves; goldens churn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every non-final system of a multi-system region now stretches its horizontal
slack so its ink fills the content width, instead of sitting at its natural
left-aligned width. ENGRAVER_VERSION 7 → 8 (any wrapping score's baked geometry
differs; a single-system score is unchanged — its only system is ragged-right).
Casting bakes each system by a Placement: a vertical dy plus a horizontal affine
world_x = a·x + b (rigid = a:1, b:dx). A justified system spreads the slack
linearly (a = 1 + extra/span). The map is CLAMPED to the slot-source range: affine
within it, rigid slope-1 beyond it (bearing overhangs, staff lines drawn to the
ink edge), so the mapped ink extremes agree exactly with the per-slot deltas and
the ink spans exactly [left_margin, left_margin + content_width] (no over/under-
shoot). Slot-relative like the E1 remap: glyphs translate by the map at their
SLOT's source (intra-slot offsets survive), spanning strokes map endpoints through
the affine (they stretch), rigid-width strokes (stems/ledgers, via owning_glyph)
track their slot, slur control points map through the affine.
Not justified: a region's last system (ragged-right by convention), a degenerate
span, or a system already at/over width (never compressed into overlap).
Quality consequences (honest): system_break_penalty collapses to ~0 (the point);
the width-uniformity axes rise as the full non-final system contrasts with the
ragged last line (ten-measure casting_off ~0.45 → ~0.80) — a metric-semantics
follow-up noted in DECISIONS. Tests retargeted: the widow rebalance now shows in
measure distribution not baked widths; the floor-column test moved to a synthetic
vector (robust to fixture values). Goldens regenerated (view_box widens to the
content width; same 147 primitives, no collisions). 939 tests, clippy 0, docs
-D warnings, conformance 8/8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The third pipeline primitive kind, review-hardened (5 verified findings fixed
pre-commit). A `Curve` (four control points, mirroring `Stroke`) threads
through all three IR stages, the canonical-encode fingerprint (a 5th u32 count
prefix; width-lock 4→5), the round-trip provenance chains and count identity,
`to_render`, the stub solver, engrave remap + casting, SVG path emission, and
hit-testing.
Slurs draw as one cubic bézier per slur carrying the slur's exact provenance
(no synthesis). LayoutContent::Slur resolves each endpoint event to a Note
column at to_logical (SlurEndpoint At/Unresolved — E1's honest-placement
discipline); a symmetric arc whose apex sits `height` from the endpoint line;
curvature_override direction+height honored, style.line (dashed) deferred to
Push 3. Honest non-drawing (traced anchor kept) for an unresolved endpoint, a
non-left-to-right span, or a cross-staff slur (no single staff — would float
at yo=0). Authored height/thickness sanitized to defaults when non-positive
(a negative thickness would else fail validation and blank the layout).
Hit-test: HitShape::Curve (Copy) flattens the cubic to a 16-segment capsule
inside contains/intersects (one region per curve); a slur click resolves to
Slur generically (no editor arm; edit ops refuse it). render-svg: stroked
unfilled <path C> after strokes/before glyphs + curve_count + content-hull
bounds. engrave: HorizontalRemap::curves; casting curve_system = Rigid-to-
start-system (no de Casteljau split — deferred; break-spanning slur draws
whole in its start system, kept for the source surjection); ENGRAVER_VERSION
4→5; slur_shape_penalty now 0.0 by construction (Minimal draws the ideal arc).
Existing SVG goldens changed only in the provenance-note comment (curves now
enumerated) — geometry byte-identical; 6 snapshots gained curve_count; new
ten_measure_with_slurs goldens. 929 tests, conformance 8/8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>