Skip to content

Commit aa535df

Browse files
committed
fix(tab): handle H/P chain and same-beat edge cases
- 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
1 parent 6901cd3 commit aa535df

2 files changed

Lines changed: 58 additions & 11 deletions

File tree

packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,22 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph {
5656
const tapSlur: TabTieGlyph = new TabTieGlyph(`tab.tie.leftHandTap.${n.id}`, n, n, false);
5757
this.addTie(tapSlur);
5858
}
59-
// start effect slur on first beat
60-
if (n.isEffectSlurOrigin && n.effectSlurDestination) {
59+
// H/P arc start-side: create individual arc per hammer-pull pair
60+
if (n.isHammerPullOrigin && n.hammerPullDestination) {
61+
const dest = n.hammerPullDestination;
62+
const slurText = dest.fret >= n.fret ? 'H' : 'P';
6163
let expanded: boolean = false;
6264
for (const slur of this._effectSlurs) {
63-
if (slur.tryExpand(n, n.effectSlurDestination, false, false)) {
65+
if (slur.tryExpand(n, dest, false, false, slurText)) {
6466
expanded = true;
6567
break;
6668
}
6769
}
6870
if (!expanded) {
69-
let slurText: string | undefined = undefined;
70-
if (n.isHammerPullOrigin && n.hammerPullDestination) {
71-
slurText = n.hammerPullDestination.fret >= n.fret ? 'H' : 'P';
72-
}
7371
const effectSlur: TabSlurGlyph = new TabSlurGlyph(
7472
`tab.slur.effect.${n.id}`,
7573
n,
76-
n.effectSlurDestination,
74+
dest,
7775
false,
7876
false,
7977
slurText
@@ -82,8 +80,53 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph {
8280
this.addTie(effectSlur);
8381
}
8482
}
85-
// end effect slur on last beat
86-
if (n.isEffectSlurDestination && n.effectSlurOrigin) {
83+
// H/P arc end-side: for cross-bar rendering
84+
if (n.isHammerPullDestination && n.hammerPullOrigin) {
85+
const origin = n.hammerPullOrigin;
86+
const slurText = n.fret >= origin.fret ? 'H' : 'P';
87+
let expanded: boolean = false;
88+
for (const slur of this._effectSlurs) {
89+
if (slur.tryExpand(origin, n, false, true, slurText)) {
90+
expanded = true;
91+
break;
92+
}
93+
}
94+
if (!expanded) {
95+
const effectSlur: TabSlurGlyph = new TabSlurGlyph(
96+
`tab.slur.effect.${origin.id}`,
97+
origin,
98+
n,
99+
false,
100+
true,
101+
slurText
102+
);
103+
this._effectSlurs.push(effectSlur);
104+
this.addTie(effectSlur);
105+
}
106+
}
107+
// start non-H/P effect slur (e.g. legato slide)
108+
if (n.isEffectSlurOrigin && n.effectSlurDestination && !n.isHammerPullOrigin) {
109+
let expanded: boolean = false;
110+
for (const slur of this._effectSlurs) {
111+
if (slur.tryExpand(n, n.effectSlurDestination, false, false)) {
112+
expanded = true;
113+
break;
114+
}
115+
}
116+
if (!expanded) {
117+
const effectSlur: TabSlurGlyph = new TabSlurGlyph(
118+
`tab.slur.effect.${n.id}`,
119+
n,
120+
n.effectSlurDestination,
121+
false,
122+
false
123+
);
124+
this._effectSlurs.push(effectSlur);
125+
this.addTie(effectSlur);
126+
}
127+
}
128+
// end non-H/P effect slur
129+
if (n.isEffectSlurDestination && n.effectSlurOrigin && !n.isHammerPullDestination) {
87130
let expanded: boolean = false;
88131
for (const slur of this._effectSlurs) {
89132
if (slur.tryExpand(n.effectSlurOrigin, n, false, true)) {

packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ export class TabSlurGlyph extends TabTieGlyph {
2323
return this._slurText;
2424
}
2525

26-
public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean): boolean {
26+
public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean, slurText?: string): boolean {
27+
// same label required (when provided)
28+
if (slurText !== undefined && this._slurText !== slurText) {
29+
return false;
30+
}
2731
// same type required
2832
if (this._forSlide !== forSlide) {
2933
return false;

0 commit comments

Comments
 (0)