Skip to content

feat(tab): add H/P text labels on hammer-on / pull-off arcs#2609

Open
rafaelsales wants to merge 2 commits intoCoderLine:developfrom
rafaelsales:feature/issue-2608
Open

feat(tab): add H/P text labels on hammer-on / pull-off arcs#2609
rafaelsales wants to merge 2 commits intoCoderLine:developfrom
rafaelsales:feature/issue-2608

Conversation

@rafaelsales
Copy link
Copy Markdown
Contributor

@rafaelsales rafaelsales commented Mar 23, 2026

Summary

Fixes #2608.

In the Tab (and TabMixed) renderer, hammer-on and pull-off connections are drawn as arcs between notes but the conventional H / P text labels were never rendered.

This PR adds H/P label rendering and correctly handles edge cases like H/P chains and multiple H/P on the same beat.

Changes

Commit 1feat(tab): add H/P text labels to hammer-on and pull-off arcs

  • TieGlyph.ts — adds a getSlurText() virtual method (returns undefined by default) and draws the label text at the arc midpoint in paint() when present.
  • TabSlurGlyph.ts — accepts an optional slurText?: string constructor parameter and overrides getSlurText() to return it.
  • TabBeatContainerGlyph.ts — passes 'H' or 'P' when creating effect slurs for hammer-pull origin notes.

Commit 2fix(tab): handle H/P chain and same-beat edge cases

  • TabSlurGlyph.tstryExpand() now accepts an optional slurText parameter and rejects merging slurs with different labels (prevents label loss when multiple H/P on same beat share beam direction).
  • TabBeatContainerGlyph.ts — creates individual arcs per H/P pair using hammerPullOrigin/hammerPullDestination links (instead of the collapsed effectSlurOrigin/effectSlurDestination). This renders chains like 5{h} 7{h} 5 as two separate arcs (H then P) instead of one collapsed arc. Non-H/P effect slurs (e.g. legato slides) continue to use the existing effectSlur path, guarded by !n.isHammerPullOrigin / !n.isHammerPullDestination.

How it looks

hp-labels-screenshot

Test plan

  • Simple hammer-on (ascending fret) → "H" label
  • Simple pull-off (descending fret) → "P" label
  • H then P in same bar → separate arcs with correct labels
  • H and P on different strings → H above, P below
  • H/P chain (5{h} 7{h} 5) → individual H and P arcs
  • Long chain (5{h} 7{h} 5{h} 7) → alternating H/P/H arcs
  • Legato slide ({sl}) → arc without label (regression check)
  • Plain notes → no arcs, no labels (regression check)
Running the local test page

Step 1 — Save the HTML below as test-hp-labels.html in the repo root.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>AlphaTab – Hammer-on / Pull-off H/P Labels</title>
    <style>
        body { font-family: sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; background: #fff; }
        h2 { margin-top: 32px; font-size: 1em; color: #333; }
        p { color: #555; font-size: 0.9em; }
        .at-wrap { border: 1px solid #ddd; border-radius: 4px; margin-top: 8px; }
        .at-footer { display: none; }
    </style>
</head>
<body>
    <h1>Hammer-on / Pull-off H/P Labels</h1>
    <p>
        Each section renders a different H/P scenario in the Tab stave profile.
        Each arc should display an <strong>H</strong> or <strong>P</strong> label above it.
    </p>

    <h2>1. Simple hammer-on (5→7 on string 3)</h2>
    <div id="at1" class="at-wrap"></div>

    <h2>2. Simple pull-off (7→5 on string 3)</h2>
    <div id="at2" class="at-wrap"></div>

    <h2>3. Hammer-on then pull-off (5→7, 7→5) in the same bar</h2>
    <div id="at3" class="at-wrap"></div>

    <h2>4. H and P on different strings (string 3 H, string 4 P)</h2>
    <div id="at4" class="at-wrap"></div>

    <h2>5. H/P chain (5→7→5) — individual H then P arcs</h2>
    <div id="at5" class="at-wrap"></div>

    <h2>6. Long chain (5→7→5→7) — alternating H/P arcs</h2>
    <div id="at6" class="at-wrap"></div>

    <h2>7. Legato slide — arc without H/P label</h2>
    <div id="at7" class="at-wrap"></div>

    <h2>8. Plain notes — no arc, no label</h2>
    <div id="at8" class="at-wrap"></div>

    <script src="packages/alphatab/dist/alphaTab.js"></script>
    <script>
        const apis = [];
        const settings = {
            core: {
                engine: 'html5',
                fontDirectory: 'packages/alphatab/dist/font/',
                logLevel: 0,
            },
            display: {
                staveProfile: 'Tab',
                scale: 1.5,
            },
            notation: {
                elements: {
                    guitarTuning: false,
                    trackNames: false,
                }
            },
            player: { enablePlayer: false },
        };

        function renderTex(containerId, tex) {
            const api = new alphaTab.AlphaTabApi(
                document.getElementById(containerId),
                settings
            );
            api.tex(tex, [0]);
            apis.push(api);
        }

        const queue = [
            ['at1', ':4 5.3{h} 7.3 r r'],
            ['at2', ':4 7.3{h} 5.3 r r'],
            ['at3', ':4 5.3{h} 7.3 7.3{h} 5.3'],
            ['at4', ':4 5.3{h} 7.3 8.4{h} 5.4'],
            ['at5', ':4 5.3{h} 7.3{h} 5.3 r'],
            ['at6', ':8 5.3{h} 7.3{h} 5.3{h} 7.3 r r r r'],
            ['at7', ':4 5.3{sl} 7.3 r r'],
            ['at8', ':4 5.3 7.3 5.3 7.3'],
        ];

        queue.forEach(([id, tex]) => {
            renderTex(id, tex);
        });
    </script>
</body>
</html>

Step 2 — Build the web bundle and start a local server from the repo root:

npm install
npm run build-web
python3 -m http.server 8765

Step 3 — Open http://localhost:8765/test-hp-labels.html in your browser.

@rafaelsales rafaelsales force-pushed the feature/issue-2608 branch 2 times, most recently from ee5fd63 to f23e3bd Compare March 23, 2026 03:39
In the Tab renderer, the arc connecting hammer-on and pull-off notes
is now annotated with an "H" (ascending fret = hammer-on) or "P"
(descending fret = pull-off) label above the arc midpoint.

The label is drawn via an overridden paint() in TabSlurGlyph, reusing
the same canvas.fillText path already used for whammy/bend slurText.
TieGlyph's coordinate fields (_startX/Y, _endX/Y, _tieHeight,
_shouldPaint) are widened from private to protected to allow the
subclass to read them during paint.

Fixes CoderLine#2608
@Danielku15
Copy link
Copy Markdown
Member

Great addition, thanks for the change. We might need to check some additional edge cases like:

  • Having multiple hammer-on/pull-offs on the same beat.
  • Having Direct hammer-on/pull-off chains

- Individual arcs per H/P pair in chains (e.g. 5{h} 7{h} 5 now
  renders separate H and P arcs instead of one collapsed arc)
- Prevent tryExpand from merging slurs with different H/P labels
  (fixes label loss when multiple H/P on same beat share beam direction)
- Guard existing effectSlur blocks to skip H/P notes, keeping legato
  slide rendering unaffected
@rafaelsales
Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've pushed a follow-up commit addressing both edge cases:

1. Direct H/P chains

The model (Note.finish()) collapses effect slur chains into a single arc from first to last note. For 5{h} 7{h} 5, this produced one arc labeled "H" instead of two separate arcs.

Fix: The renderer now uses hammerPullOrigin/hammerPullDestination links directly (instead of the collapsed effectSlurOrigin/effectSlurDestination) to create individual arcs per H/P pair, each with the correct label. Non-H/P effect slurs (legato slides) continue using the existing effectSlur path.

2. Multiple H/P on the same beat

When two notes on the same beat pair had H/P effects with the same beam direction, tryExpand() merged them into one slur, silently discarding the second label.

Fix: tryExpand() now accepts an optional slurText parameter and rejects merging when the labels differ.

Both are covered by new test cases in the test page (sections 5 and 6).

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.

Tab renderer: Hammer-on / Pull-off arcs missing H/P text labels

2 participants