Skip to content

fix(rendering): correct the minimum bar spacing and hammer-on/slide slur placement - #2943

Merged
Danielku15 merged 20 commits into
developfrom
fix/2904-first-spring-min-force
Oct 9, 2026
Merged

Danielku15 merged 20 commits into
developfrom
fix/2904-first-spring-min-force

Conversation

@Danielku15

Copy link
Copy Markdown
Member

Note

AI-authored disclosure (alphatab-ai-authored-v1)

Portions of this content were authored by an AI agent. The agent has read
AGENTS.md and the human submitter accepts responsibility for
compliance with the rules in that document.

Issues

Fixes #2904

Proposed changes

#2904 reports a slur with NaN control points between two legato-slide chords at narrow widths. The root cause
is not the slur: the bar gets far less width than its content needs (258px for a measured minimum of 809px at a
400px container), every spring is compressed linearly, the notation overlaps and the slur end anchor lands
before its start anchor. Looking into this showed that the minimum itself was wrong in two ways, and that the
effect slurs were anchored at positions which collide as soon as space gets tight.

Spacing minimum (BarLayoutingInfo)

  • The first beat's pre-beat width (the start-of-bar spacing for accidentals etc.) was counted twice in the
    minimum stretch force. A single accidental on the first beat inflated every beat of the bar (the Slur of a legato slide / hammer-on between two-note chords is not drawn in the standard notation staff when beats are tightly spaced (path with NaN) #2904 bar
    needed 809px instead of 451px). It is a fixed offset in front of the springs and is no longer stretched in.
  • The minimum only reserved the ink of adjacent beats: at the minimum spacing an accidental or grace group
    touched the previous notehead and flags touched the bar line. Two new engraving settings add the clearances
    Behind Bars asks for, only where content would come closer:
    • EngravingSettings.beatContentPadding = ½ stave-space between adjacent beats (Behind Bars p. 41), including
      trailing grace groups.
    • EngravingSettings.barlineContentPadding = 1 stave-space between the last beat and the bar line (p. 43).
  • Rhythmic spacing is unchanged: all beats of a bar still share one force, so notes of equal duration stay
    equally spaced (p. 40). Bars which were only wide because of the double count get narrower, bars with
    colliding content get the room they need.
  • Multi-bar rests are now centered in their bar (p. 564) instead of keeping the extra width on the right.

Squeezed bars

  • Ties and slurs keep a minimum span (the beat content padding), centered on their anchors. Anchors which
    crossed in a squeezed bar produced the NaN path (log of a negative span) or an inverted curve.
  • When bars still have to be compressed below their minimum, the layout logs one warning per layout pass, e.g.
    Bar 1 does not fit into the available width and had to be compressed below the minimum size (up to 311px missing).
    No public API for the required width yet; that needs a proper design first.

Hammer-on, pull-off and legato slide slurs (cross-checked against Guitar Pro, MuseScore and Behind Bars)

  • Standard notation: the slur started at whichever note of a chord was processed first, often an inner note,
    and ran from that notehead to the accidentals of the next chord (the slur of Slur of a legato slide / hammer-on between two-note chords is not drawn in the standard notation staff when beats are tightly spaced (path with NaN) #2904). All notes on one stem
    now take a single slur outside the chord, connecting the outer noteheads (p. 114). It spans from the beat
    where note chains start to their furthest destination, so a chain continuing across a beat on another
    string stays one slur.
  • In multi-voice bars the slur goes to the stem end of its voice (p. 117: never on the notehead side in
    double-stemmed writing), so the slurs of two voices no longer collide between them.
  • Tab: the arcs of a chord were merged by comparing against the tie direction before it was computed. Lower
    string arcs were merged into the upper arc and lost, arcs on the lower strings were never merged. A beat now
    gets one arc per side (above the upper strings, below the lower strings) from the outer note to the furthest
    chain end, matching Guitar Pro. Numbered notation keeps its previous behavior.

Tab staff lines

  • The staff line gaps behind fret numbers baked the note position into a cache that was not updated when the
    notes moved afterwards (centered full-bar notes, shared pre-beat sizes). Lines were cut next to the numbers
    and ran through them. The gaps now read the current position.

Not in this PR

  • The H/P/sl. labels still sit on the arcs. Guitar Pro and MuseScore place them above the staff, stacked once
    per chord segment; that is a larger layout change and will be explored separately.
  • Overflow handling for bars which cannot fit at all (overflow/scale modes, splitting bars across systems).

Tests

Checklist

  • I consent that this change becomes part of alphaTab under its current or any future open source license
  • This PR is linked to an accepted issue (see above)
  • Changes are implemented
  • New tests were added
  • I have read AGENTS.md if an AI helped draft any part of this PR

AI authorship disclosure

  • No AI agent authored any part of this PR (description, code, tests, or commit messages)
  • An AI agent contributed to this PR. The AI-authored disclosure block
    (alphatab-ai-authored-v1) is present at the top of this body, and I have
    personally reviewed every change and can explain each one

Further details

  • This is a breaking change
  • This change will require update of the documentation/website

…minimum stretch force

The pre-spring width of a bar's first spring is a fixed offset in front of the
springs (calculateVoiceWidth, spaceToForce, buildOnTimePositions). Adding it to
the first spring's required space as well inflated minStretchForce, and with it
every beat of the bar, whenever the first beat carried accidentals or other
pre-beat content.
…nt beats

The minimum stretch force only reserved the ink of adjacent beats, so at the
minimum spacing an accidental or grace group touched the previous notehead.
The new EngravingSettings.beatContentPadding (1/2 stave-space, Behind Bars) is
added between adjacent springs and before trailing grace groups.
… bar line

The last spring only reserved the ink of the last beat, so flags and stems
could touch the bar line at the minimum spacing. The new
EngravingSettings.barlineContentPadding (1 stave-space, Behind Bars) is added
to the last spring's required space.
The H-bar kept its natural width anchored at the start of the bar, so any
additional bar width (e.g. the bar line padding) ended up on the right side
only. Behind Bars shows the multiple-rest bar centred within the bar.
When a bar is squeezed below its minimum width, the end anchor of a tie or
slur can end up before its start anchor. The slur height then took the log
of a negative span and produced NaN control points (#2904). Ties and slurs
now keep at least the beat content padding as span, centered on their
anchors.
Bars which do not fit into the available width are compressed below the
width their content needs, which leads to overlapping notation. The layout
now logs one warning per layout pass listing the affected bars and the
missing width.
…e the chord

Effect slurs in standard notation started at whichever note of a chord was
processed first, so they were often drawn inside the chord from the side of
a notehead to the accidentals of the next chord. Like Guitar Pro, MuseScore
and Behind Bars (all notes on one stem take a single slur) the slur now
connects the outer notes of the start and end beat of the chain, centered on
the noteheads.
…n ports

Avoid a comparer sort on number arrays (unsupported in Kotlin), clamp the
slice of reported bars (C# throws on out of range) and use a logger class
instead of an object literal in the test.
… the stem end in multi-voice bars

The beat-level effect slur chain breaks on beats without an effect (e.g. a
tapped note on another string), which split a single hammer-on/pull-off
slur into two. The slur now spans from the beat where note chains start to
the furthest destination of these chains.

In multi-voice bars the slur is placed on the stem side at the stem end
(Behind Bars: never on the notehead side in double-stemmed writing), so the
slurs of two voices no longer collide between them.
The staff line gap cache baked the x of the on-notes glyph into the gap
positions. This x changes after the cache is built, e.g. when centered
full-bar notes are shifted or shared pre-beat sizes are applied, which left
the gaps at stale positions: lines were cut next to the fret numbers and ran
through them. The gaps now read the current on-notes position.
…nd legato slides

The arcs of a chord were merged by comparing against the tie direction before
it was computed, which merged lower string arcs into the upper arc (losing
them) and never merged arcs on the lower strings. Like Guitar Pro, a beat now
gets one arc per side (above the upper strings, below the lower strings) from
the outer note to the furthest destination of the chains starting on that
side. Numbered notation keeps its previous merging.
@Danielku15
Danielku15 merged commit 412e85d into develop Oct 9, 2026
6 checks passed
@Danielku15
Danielku15 deleted the fix/2904-first-spring-min-force branch October 9, 2026 10:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slur of a legato slide / hammer-on between two-note chords is not drawn in the standard notation staff when beats are tightly spaced (path with NaN)

1 participant