Commit Graph

4 Commits

Author SHA1 Message Date
Levi Neuwirth 24b6a34db9 Slurs: side from the stems, endpoints on the notes, apex clear of both
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>
2026-07-09 13:58:56 -04:00
Levi Neuwirth 7aa0b938e2 Stems point away from the middle line, and reach it
Every stem in the engine pointed up, on the notehead's right, at a constant
octave. A C6 three ledger lines above the staff grew an upward stem shooting past
everything -- visible in the two_staff and three_staff renders. It is also why the
slurs are wrong: an Auto slur is placed OPPOSITE the stems, so no correct side
could be chosen while every stem pointed the same way. Stems are therefore a
prerequisite for the slur fix, not merely adjacent to it.

  - Direction: away from the middle line, decided by the head furthest from it,
    ties going down (the convention for a note on the line, and for a chord that
    straddles it evenly).
  - Attachment: the side it points -- an up-stem at the lowest head's right edge,
    a down-stem at the highest head's left. Read from the head's own bounding box
    rather than the rounded NOTEHEAD_STEM_X: for noteheadBlack those are 1.1807
    and 0, and 1.1807 is Bravura's real stemUpSE (1.18). The old 1.15 was a
    rounding -- it accounts for the whole of ten_measure's churn, where every stem
    moved right by 0.031 and nothing else moved at all.
  - Length: an octave from the outer head, but drawn out to the middle line when
    the note lies beyond it, so no stem dangles in the ledger field.

No version moves. This is the PROJECTION changing, not a solver: to_constrained
emits different geometry from the same graph, so ENGRAVER_VERSION's promise (same
input => same output) is untouched. Every golden churns, stub and engrave alike,
because stems are constrained-stage geometry.

Locked by a_stem_points_away_from_the_middle_line_and_reaches_it, which re-pitches
a generated score across four octaves -- the corpus generator writes only low
notes, so nothing in the suite had ever exercised a down-stem. Mutation-verified:
restoring always-up plus fixed-octave fails it.

Parked, found on the way: BRAVURA_METRICS' notehead anchors are unusable as
written. They name stemUpNW/stemDownSE -- the corners an up- and down-stem never
attach to -- and their x of 1180 reads like 1.18 staff spaces written in
thousandths rather than the table's 1/1024 units (1.18 sp = 1208). Nothing
consumes them except metrics_hash, so correcting them would move the
GlyphCatalogIdentity every conformance claim declares. That makes three parked
candidates; the house rule opens a batch pass at three.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:47:47 -04:00
Levi Neuwirth c5173d275e Push 3 (slur quality) 1/3: render dashed/dotted slurs faithfully
E2 drew every slur solid and surfaced a SlurLineStyleNotRendered diagnostic for
a non-Solid authored line. That deferral is now lifted: the Curve primitive
gains a `line: LineStyle` (from the slur's SpanStyle.line), threaded through the
resolved canonical encode (a per-curve style byte), the engrave remap, and
casting; render-svg emits stroke-dasharray (dashed = "0.5 0.35"; dotted =
round-capped "0 0.28"). The diagnostic variant is removed — the style is
rendered, not deferred, so `req:layoutir:slur-curve`'s "never silently rendered
solid" is satisfied by faithful rendering rather than a surfaced gap.

ENGRAVER_VERSION 5→6 (a dashed/dotted slur's resolved bytes and SVG differ).
Zero churn beyond the one dashed slur: the slur fixture's two solid curves are
byte-identical; only the editorial slur's <path> gained the dasharray. Solid
slurs and slur-free scores are unchanged. 932 tests, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:13:32 -04:00
Levi Neuwirth 81b7f42f0a Schema major 2 Phase E2: slur curves + cubic-bézier primitive
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>
2026-07-08 15:33:00 -04:00