@coderline/alphatab 1.9.0-alpha.1785 → 1.9.0-alpha.1804

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * alphaTab v1.9.0-alpha.1785 (develop, build 1785)
2
+ * alphaTab v1.9.0-alpha.1804 (develop, build 1804)
3
3
  *
4
4
  * Copyright © 2026, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -203,9 +203,9 @@ class AlphaTabError extends Error {
203
203
  * @internal
204
204
  */
205
205
  class VersionInfo {
206
- static version = '1.9.0-alpha.1785';
207
- static date = '2026-04-27T03:55:02.662Z';
208
- static commit = '760ed909a3d8dc36b159d23b4ff6780e95a3daf1';
206
+ static version = '1.9.0-alpha.1804';
207
+ static date = '2026-05-16T03:54:50.685Z';
208
+ static commit = '6e757c0cbec66598a7fa5327abc9d05616984486';
209
209
  static print(print) {
210
210
  print(`alphaTab ${VersionInfo.version}`);
211
211
  print(`commit: ${VersionInfo.commit}`);
@@ -992,6 +992,38 @@ var SlideOutType;
992
992
  SlideOutType[SlideOutType["PickSlideUp"] = 6] = "PickSlideUp";
993
993
  })(SlideOutType || (SlideOutType = {}));
994
994
 
995
+ /**
996
+ * A slur arc spanning two notes, optionally with inner articulation
997
+ * segments. Corresponds conceptually to a MusicXML `<slur>` element
998
+ * plus the technique spans inside it.
999
+ *
1000
+ * For this PR only effect slurs (hammer-pull + legato-slide chains)
1001
+ * are derived in `Note.finish()`. Phrase and legato slurs may join
1002
+ * this type in a future PR; a discriminator will be added at that
1003
+ * point.
1004
+ * @internal
1005
+ */
1006
+ class Slur {
1007
+ originNote;
1008
+ destinationNote;
1009
+ segments = [];
1010
+ }
1011
+
1012
+ /**
1013
+ * Articulation kind for an inner span of a {@link Slur}.
1014
+ *
1015
+ * Drives the renderer's font selection (which {@link NotationElement} to
1016
+ * use) and the default label text when {@link SlurSegment.text} is null.
1017
+ * `Note.finish()` classifies the kind once when building the slur; the
1018
+ * renderer never re-derives it.
1019
+ * @internal
1020
+ */
1021
+ var SlurSegmentKind;
1022
+ (function (SlurSegmentKind) {
1023
+ SlurSegmentKind[SlurSegmentKind["HammerPull"] = 0] = "HammerPull";
1024
+ SlurSegmentKind[SlurSegmentKind["LegatoSlide"] = 1] = "LegatoSlide";
1025
+ })(SlurSegmentKind || (SlurSegmentKind = {}));
1026
+
995
1027
  /**
996
1028
  * This public enum lists all vibrato types that can be performed.
997
1029
  * @public
@@ -1332,6 +1364,14 @@ var NotationElement;
1332
1364
  * The slurs shown on bend effects within the score staff.
1333
1365
  */
1334
1366
  NotationElement[NotationElement["ScoreBendSlur"] = 55] = "ScoreBendSlur";
1367
+ /**
1368
+ * The hammer-on pull-off text shown on slurs.
1369
+ */
1370
+ NotationElement[NotationElement["EffectHammerOnPullOffText"] = 56] = "EffectHammerOnPullOffText";
1371
+ /**
1372
+ * The slide text shown on slurs.
1373
+ */
1374
+ NotationElement[NotationElement["EffectSlideText"] = 57] = "EffectSlideText";
1335
1375
  })(NotationElement || (NotationElement = {}));
1336
1376
  /**
1337
1377
  * The notation settings control how various music notation elements are shown and behaving
@@ -6128,6 +6168,16 @@ class Note {
6128
6168
  * @json_ignore
6129
6169
  */
6130
6170
  effectSlurDestination = null;
6171
+ /**
6172
+ * The {@link Slur} object whose origin is this note. Populated by
6173
+ * `finish()`; non-null only on the chain-origin note of an effect
6174
+ * slur. Carries the inner articulation segments used by the
6175
+ * renderer to paint H/P/sl. labels along the arc.
6176
+ * @clone_ignore
6177
+ * @json_ignore
6178
+ * @internal
6179
+ */
6180
+ effectSlur = null;
6131
6181
  /**
6132
6182
  * The ornament applied on the note.
6133
6183
  */
@@ -6412,23 +6462,50 @@ class Note {
6412
6462
  break;
6413
6463
  }
6414
6464
  let effectSlurDestination = null;
6465
+ let effectSlurSegmentKind = null;
6415
6466
  if (this.isHammerPullOrigin && this.hammerPullDestination) {
6416
6467
  effectSlurDestination = this.hammerPullDestination;
6468
+ effectSlurSegmentKind = SlurSegmentKind.HammerPull;
6417
6469
  }
6418
6470
  else if (this.slideOutType === SlideOutType.Legato && this.slideTarget) {
6419
6471
  effectSlurDestination = this.slideTarget;
6472
+ effectSlurSegmentKind = SlurSegmentKind.LegatoSlide;
6420
6473
  }
6421
6474
  if (effectSlurDestination) {
6422
6475
  this.hasEffectSlur = true;
6423
6476
  if (this.effectSlurOrigin && this.beat.pickStroke === PickStroke.None) {
6424
- this.effectSlurOrigin.effectSlurDestination = effectSlurDestination;
6425
- this.effectSlurOrigin.effectSlurDestination.effectSlurOrigin = this.effectSlurOrigin;
6477
+ const chainOrigin = this.effectSlurOrigin;
6478
+ chainOrigin.effectSlurDestination = effectSlurDestination;
6479
+ effectSlurDestination.effectSlurOrigin = chainOrigin;
6426
6480
  this.effectSlurOrigin = null;
6481
+ if (effectSlurSegmentKind !== null && chainOrigin.effectSlur !== null) {
6482
+ chainOrigin.effectSlur.destinationNote = effectSlurDestination;
6483
+ chainOrigin.effectSlur.segments.push({
6484
+ fromNote: this,
6485
+ toNote: effectSlurDestination,
6486
+ kind: effectSlurSegmentKind,
6487
+ text: null
6488
+ });
6489
+ }
6427
6490
  }
6428
6491
  else {
6429
6492
  this.isEffectSlurOrigin = true;
6430
6493
  this.effectSlurDestination = effectSlurDestination;
6431
- this.effectSlurDestination.effectSlurOrigin = this;
6494
+ effectSlurDestination.effectSlurOrigin = this;
6495
+ // Always allocate a fresh Slur — finish() may run twice (worker re-finish);
6496
+ // overwriting unconditionally keeps the derivation idempotent.
6497
+ const slur = new Slur();
6498
+ slur.originNote = this;
6499
+ slur.destinationNote = effectSlurDestination;
6500
+ if (effectSlurSegmentKind !== null) {
6501
+ slur.segments.push({
6502
+ fromNote: this,
6503
+ toNote: effectSlurDestination,
6504
+ kind: effectSlurSegmentKind,
6505
+ text: null
6506
+ });
6507
+ }
6508
+ this.effectSlur = slur;
6432
6509
  }
6433
6510
  }
6434
6511
  // try to detect what kind of bend was used and cleans unneeded points if required
@@ -7678,6 +7755,23 @@ class Beat {
7678
7755
  * @json_ignore
7679
7756
  */
7680
7757
  effectSlurDestination = null;
7758
+ /**
7759
+ * Convenience accessor for the {@link Slur} of this beat. Returns
7760
+ * the effect slur of whichever note in this beat owns it (the
7761
+ * chain-origin note populated during `Note.finish()`), or `null`
7762
+ * when no note in the beat is an effect-slur origin.
7763
+ * @clone_ignore
7764
+ * @json_ignore
7765
+ * @internal
7766
+ */
7767
+ get effectSlur() {
7768
+ for (const n of this.notes) {
7769
+ if (n.effectSlur !== null) {
7770
+ return n.effectSlur;
7771
+ }
7772
+ }
7773
+ return null;
7774
+ }
7681
7775
  /**
7682
7776
  * Gets or sets how the beaming should be done for this beat.
7683
7777
  */
@@ -10221,7 +10315,7 @@ class IOHelper {
10221
10315
  encoding = 'utf-8';
10222
10316
  }
10223
10317
  const decoder = new TextDecoder(encoding);
10224
- return decoder.decode(data.buffer);
10318
+ return decoder.decode(data);
10225
10319
  }
10226
10320
  static _detectEncoding(data) {
10227
10321
  if (data.length > 2 && data[0] === 0xfe && data[1] === 0xff) {
@@ -19651,7 +19745,7 @@ class Gp3To5Importer extends ScoreImporter {
19651
19745
  static _versionString = 'FICHIER GUITAR PRO ';
19652
19746
  // NOTE: General Midi only defines percussion instruments from 35-81
19653
19747
  // Guitar Pro 5 allowed GS extensions (27-34 and 82-87)
19654
- // GP7-8 do not have all these definitions anymore, this lookup ensures some fallback
19748
+ // GP7-8 do not have all these definitions anymore, this lookup ensures some fallback
19655
19749
  // (even if they are not correct)
19656
19750
  // we can support this properly in future when we allow custom alphaTex articulation definitions
19657
19751
  // then we don't need to rely on GP specifics anymore but handle things on export/import
@@ -21042,12 +21136,13 @@ class GpBinaryHelpers {
21042
21136
  * @returns
21043
21137
  */
21044
21138
  static gpReadStringByteLength(data, length, encoding) {
21139
+ // Fixed-width string field: 1 length byte + `length` data bytes, decoded
21140
+ // up to min(stringLength, length). Always consumes 1 + length bytes.
21045
21141
  const stringLength = data.readByte();
21046
- const s = GpBinaryHelpers.gpReadString(data, stringLength, encoding);
21047
- if (stringLength < length) {
21048
- data.skip(length - stringLength);
21049
- }
21050
- return s;
21142
+ const fieldBytes = new Uint8Array(length);
21143
+ data.read(fieldBytes, 0, length);
21144
+ const effectiveLength = Math.min(stringLength, length);
21145
+ return IOHelper.toString(fieldBytes.subarray(0, effectiveLength), encoding);
21051
21146
  }
21052
21147
  }
21053
21148
  /**
@@ -24267,6 +24362,10 @@ class GpifParser {
24267
24362
  }
24268
24363
  // build masterbar automations
24269
24364
  for (const [barNumber, automations] of this._masterTrackAutomations) {
24365
+ if (barNumber < 0 || barNumber >= this.score.masterBars.length) {
24366
+ // automation references a bar that is not in the score's masterBars list
24367
+ continue;
24368
+ }
24270
24369
  const masterBar = this.score.masterBars[barNumber];
24271
24370
  for (let i = 0, j = automations.length; i < j; i++) {
24272
24371
  const automation = automations[i];
@@ -31942,7 +32041,9 @@ class RenderingResources {
31942
32041
  [NotationElement.RepeatCount, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31943
32042
  [NotationElement.BarNumber, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31944
32043
  [NotationElement.ScoreBendSlur, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31945
- [NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)]
32044
+ [NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)],
32045
+ [NotationElement.EffectHammerOnPullOffText, RenderingResources._effectFont],
32046
+ [NotationElement.EffectSlideText, RenderingResources._effectFont]
31946
32047
  ]);
31947
32048
  /**
31948
32049
  * The name of the SMuFL Font to use for rendering music symbols.
@@ -32233,9 +32334,16 @@ class RenderingResources {
32233
32334
  notationElement = NotationElement.ScoreWords;
32234
32335
  break;
32235
32336
  }
32337
+ return this.getFontForNotationElement(notationElement);
32338
+ }
32339
+ /**
32340
+ * @internal
32341
+ * @param element
32342
+ */
32343
+ getFontForNotationElement(notationElement) {
32236
32344
  return this.elementFonts.has(notationElement)
32237
32345
  ? this.elementFonts.get(notationElement)
32238
- : RenderingResources.defaultFonts.get(NotationElement.ScoreWords);
32346
+ : RenderingResources.defaultFonts.get(notationElement);
32239
32347
  }
32240
32348
  }
32241
32349
 
@@ -49161,6 +49269,21 @@ class MasterBarTickLookup {
49161
49269
  }
49162
49270
  }
49163
49271
 
49272
+ /**
49273
+ * Represents a range of the song that should be played.
49274
+ * @public
49275
+ */
49276
+ class PlaybackRange {
49277
+ /**
49278
+ * The position in midi ticks from where the song should start.
49279
+ */
49280
+ startTick = 0;
49281
+ /**
49282
+ * The position in midi ticks to where the song should be played.
49283
+ */
49284
+ endTick = 0;
49285
+ }
49286
+
49164
49287
  /**
49165
49288
  * Describes how a cursor should be moving.
49166
49289
  * @public
@@ -49321,6 +49444,13 @@ class MidiTickLookup {
49321
49444
  * @internal
49322
49445
  */
49323
49446
  masterBarLookup = new Map();
49447
+ /**
49448
+ * A dictionary of all beat played. The index is the id to {@link Beat.id}.
49449
+ * The value is the bar relative tick time at which the beat was registered during midi generation.
49450
+ * This lookup only contains the first time a Beat is played.
49451
+ * @internal
49452
+ */
49453
+ beatLookup = new Map();
49324
49454
  /**
49325
49455
  * A list of all {@link MasterBarTickLookup} sorted by time.
49326
49456
  */
@@ -49687,10 +49817,22 @@ class MidiTickLookup {
49687
49817
  * @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
49688
49818
  */
49689
49819
  getBeatStart(beat) {
49690
- if (!this.masterBarLookup.has(beat.voice.bar.index)) {
49820
+ if (!this.masterBarLookup.has(beat.voice.bar.index) || !this.beatLookup.has(beat.id)) {
49691
49821
  return 0;
49692
49822
  }
49693
- return this.masterBarLookup.get(beat.voice.bar.index).start + beat.playbackStart;
49823
+ const mb = this.masterBarLookup.get(beat.voice.bar.index);
49824
+ return mb.start + this.beatLookup.get(beat.id).startTick;
49825
+ }
49826
+ /**
49827
+ * Gets the playback range in midi ticks for a given beat.
49828
+ * @param beat The beat to find the time period for.
49829
+ * @returns The relative playback range within the parent masterbar at which the beat start and ends playing
49830
+ */
49831
+ getRelativeBeatPlaybackRange(beat) {
49832
+ if (!this.beatLookup.has(beat.id)) {
49833
+ return undefined;
49834
+ }
49835
+ return this.beatLookup.get(beat.id);
49694
49836
  }
49695
49837
  /**
49696
49838
  * Adds a new {@link MasterBarTickLookup} to the lookup table.
@@ -49708,6 +49850,12 @@ class MidiTickLookup {
49708
49850
  }
49709
49851
  }
49710
49852
  addBeat(beat, start, duration) {
49853
+ if (!this.beatLookup.has(beat.id)) {
49854
+ const playbackRange = new PlaybackRange();
49855
+ playbackRange.startTick = start;
49856
+ playbackRange.endTick = start + duration;
49857
+ this.beatLookup.set(beat.id, playbackRange);
49858
+ }
49711
49859
  const currentMasterBar = this._currentMasterBar;
49712
49860
  if (currentMasterBar) {
49713
49861
  // pre-beat grace notes at the start of the bar we also add the beat to the previous bar
@@ -52291,21 +52439,6 @@ class ActiveBeatsChangedEventArgs {
52291
52439
  }
52292
52440
  }
52293
52441
 
52294
- /**
52295
- * Represents a range of the song that should be played.
52296
- * @public
52297
- */
52298
- class PlaybackRange {
52299
- /**
52300
- * The position in midi ticks from where the song should start.
52301
- */
52302
- startTick = 0;
52303
- /**
52304
- * The position in midi ticks to where the song should be played.
52305
- */
52306
- endTick = 0;
52307
- }
52308
-
52309
52442
  /**
52310
52443
  * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
52311
52444
  * allowing dynamic changing of the underlying instance without loosing aspects like the
@@ -55506,23 +55639,24 @@ class AlphaTabApiBase {
55506
55639
  if (this._selectionStart && this._tickCache) {
55507
55640
  // get the start and stop ticks (which consider properly repeats)
55508
55641
  const tickCache = this._tickCache;
55509
- const realMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
55642
+ const realStartMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
55643
+ const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionStart.beat);
55644
+ const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? this._selectionStart.beat.playbackStart;
55510
55645
  // move to selection start
55511
55646
  this._currentBeat = null; // reset current beat so it is updating the cursor
55512
55647
  if (this._player.state === PlayerState.Paused) {
55513
- this._cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
55648
+ this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1);
55514
55649
  }
55515
- this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
55650
+ this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart;
55516
55651
  // set playback range
55517
55652
  if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) {
55518
- const realMasterBarEnd = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
55653
+ const realEndMasterBarStart = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
55654
+ const endBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionEnd.beat);
55655
+ const endBeatPlaybackEnd = endBeatPlaybackRange?.endTick ??
55656
+ this._selectionEnd.beat.playbackStart + this._selectionEnd.beat.playbackDuration;
55519
55657
  const range = new PlaybackRange();
55520
- range.startTick = realMasterBarStart + this._selectionStart.beat.playbackStart;
55521
- range.endTick =
55522
- realMasterBarEnd +
55523
- this._selectionEnd.beat.playbackStart +
55524
- this._selectionEnd.beat.playbackDuration -
55525
- 50;
55658
+ range.startTick = realStartMasterBarStart + startBeatPlaybackStart;
55659
+ range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50;
55526
55660
  this.playbackRange = range;
55527
55661
  }
55528
55662
  else {
@@ -61569,6 +61703,12 @@ class TieGlyph extends Glyph {
61569
61703
  _tieHeight = 0;
61570
61704
  _boundingBox;
61571
61705
  _shouldPaint = false;
61706
+ // Resolved per-label paint state. Lazily grown; re-layouts mutate
61707
+ // existing entries in place and update `_resolvedLabelCount` to
61708
+ // signal how many of them are valid this pass.
61709
+ _resolvedLabels = [];
61710
+ _resolvedLabelCount = 0;
61711
+ _labelBaselineOffset = 0;
61572
61712
  get checkForOverflow() {
61573
61713
  return this._shouldPaint && this._boundingBox !== undefined;
61574
61714
  }
@@ -61638,16 +61778,88 @@ class TieGlyph extends Glyph {
61638
61778
  }
61639
61779
  this._boundingBox = undefined;
61640
61780
  this.y = Math.min(this._startY, this._endY);
61781
+ const down = this.tieDirection === BeamDirection.Down;
61641
61782
  let tieBoundingBox;
61783
+ // Bezier control points for the tie. Computed once and reused
61784
+ // for both the bounding box (via _calculateActualTieHeightFromCps)
61785
+ // and label-apex sampling further below — avoids a redundant
61786
+ // call to _computeBezierControlPoints (and its 14-element array
61787
+ // allocation) per labeled slur per layout.
61788
+ let cps = [];
61642
61789
  if (this.shouldDrawBendSlur()) {
61643
61790
  this._tieHeight = 0;
61644
- tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, this.tieDirection === BeamDirection.Down, this.renderer.smuflMetrics.tieHeight);
61791
+ tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, down, this.renderer.smuflMetrics.tieHeight);
61645
61792
  }
61646
61793
  else {
61647
61794
  this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY);
61648
- tieBoundingBox = TieGlyph.calculateActualTieHeight(1, this._startX, this._startY, this._endX, this._endY, this.tieDirection === BeamDirection.Down, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
61795
+ const tieThickness = this.renderer.smuflMetrics.tieMidpointThickness;
61796
+ cps = TieGlyph._computeBezierControlPoints(1, this._startX, this._startY, this._endX, this._endY, down, this._tieHeight, tieThickness);
61797
+ tieBoundingBox = TieGlyph._calculateActualTieHeightFromCps(cps, this._startX, this._startY, this._endX, this._endY, down, tieThickness);
61649
61798
  }
61650
61799
  this._boundingBox = tieBoundingBox;
61800
+ this._resolvedLabelCount = 0;
61801
+ const labels = this.getSlurLabels();
61802
+ if (labels !== null && labels.length > 0 && this.shouldPaintLabels()) {
61803
+ const res = this.renderer.settings.display.resources;
61804
+ const padding = this.renderer.smuflMetrics.oneStaffSpace * 0.25;
61805
+ let maxTextHeight = 0;
61806
+ // Single Y line for all labels — the outer arc apex.
61807
+ // Painted offset adds `padding` on the outward side, so
61808
+ // every label sits the same fixed distance from its arc.
61809
+ const labelLineY = cps.length > 0
61810
+ ? 0.125 * cps[7] + 0.375 * cps[9] + 0.375 * cps[11] + 0.125 * cps[13]
61811
+ : (this._startY + this._endY) / 2;
61812
+ for (const label of labels) {
61813
+ const fromX = this.resolveLabelAnchorX(label.fromNote);
61814
+ const toX = this.resolveLabelAnchorX(label.toNote);
61815
+ if (fromX === null || toX === null) {
61816
+ continue;
61817
+ }
61818
+ const midX = (fromX + toX) / 2;
61819
+ if (midX < this._startX || midX > this._endX) {
61820
+ continue;
61821
+ }
61822
+ // Per-element font.size as an upper bound on glyph
61823
+ // height — avoids per-label measureText calls. All H/P
61824
+ // and sl. labels use the same _effectFont, so this is
61825
+ // typically computed once.
61826
+ const font = res.getFontForNotationElement(label.element);
61827
+ if (font.size > maxTextHeight) {
61828
+ maxTextHeight = font.size;
61829
+ }
61830
+ // grow cache lazily; mutate existing slot in place otherwise
61831
+ let slot;
61832
+ if (this._resolvedLabelCount < this._resolvedLabels.length) {
61833
+ slot = this._resolvedLabels[this._resolvedLabelCount];
61834
+ slot.x = midX;
61835
+ slot.y = labelLineY;
61836
+ slot.text = label.text;
61837
+ slot.element = label.element;
61838
+ }
61839
+ else {
61840
+ slot = {
61841
+ x: midX,
61842
+ y: labelLineY,
61843
+ text: label.text,
61844
+ element: label.element
61845
+ };
61846
+ this._resolvedLabels.push(slot);
61847
+ }
61848
+ this._resolvedLabelCount++;
61849
+ }
61850
+ if (this._resolvedLabelCount > 0) {
61851
+ // canvas.textBaseline is 'hanging' (TextBaseline.Top), so
61852
+ // fillText positions `y` at the glyph's top edge.
61853
+ if (this.tieDirection === BeamDirection.Up) {
61854
+ tieBoundingBox.y -= maxTextHeight + padding;
61855
+ this._labelBaselineOffset = -(maxTextHeight + padding);
61856
+ }
61857
+ else {
61858
+ this._labelBaselineOffset = padding;
61859
+ }
61860
+ tieBoundingBox.h += maxTextHeight + padding;
61861
+ }
61862
+ }
61651
61863
  this.height = tieBoundingBox.h;
61652
61864
  if (this.tieDirection === BeamDirection.Up) {
61653
61865
  // the tie might go above `this.y` due to its shape
@@ -61663,12 +61875,76 @@ class TieGlyph extends Glyph {
61663
61875
  if (!this._shouldPaint) {
61664
61876
  return;
61665
61877
  }
61878
+ const isDown = this.tieDirection === BeamDirection.Down;
61666
61879
  if (this.shouldDrawBendSlur()) {
61667
- TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, this.tieDirection === BeamDirection.Down, this.renderer.smuflMetrics.tieHeight);
61880
+ TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this.renderer.smuflMetrics.tieHeight);
61881
+ }
61882
+ else {
61883
+ TieGlyph.paintTie(canvas, 1, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
61884
+ }
61885
+ if (this._resolvedLabelCount > 0) {
61886
+ const ta = canvas.textAlign;
61887
+ const tb = canvas.textBaseline;
61888
+ canvas.textAlign = TextAlign.Center;
61889
+ canvas.textBaseline = TextBaseline.Top;
61890
+ const res = this.renderer.resources;
61891
+ let lastElement = -1;
61892
+ for (let i = 0; i < this._resolvedLabelCount; i++) {
61893
+ const label = this._resolvedLabels[i];
61894
+ if (label.element !== lastElement) {
61895
+ canvas.font = res.getFontForNotationElement(label.element);
61896
+ lastElement = label.element;
61897
+ }
61898
+ canvas.fillText(label.text, cx + label.x, cy + label.y + this._labelBaselineOffset);
61899
+ }
61900
+ canvas.textAlign = ta;
61901
+ canvas.textBaseline = tb;
61902
+ }
61903
+ }
61904
+ /**
61905
+ * Returns the labels to paint along this slur, or `null` when there
61906
+ * are none. Override in subclasses.
61907
+ */
61908
+ getSlurLabels() {
61909
+ return null;
61910
+ }
61911
+ /**
61912
+ * Whether label painting is enabled. Defaults to `true`. Subclasses
61913
+ * may override to disable labels on the bend-slur path or other
61914
+ * special cases.
61915
+ */
61916
+ shouldPaintLabels() {
61917
+ return !this.shouldDrawBendSlur();
61918
+ }
61919
+ /**
61920
+ * Looks up the absolute X coordinate of an anchor note. Reuses
61921
+ * the start/end bar renderers already resolved by the subclass
61922
+ * (NoteTieGlyph) when the note's bar matches — most labels live
61923
+ * in the slur's start or end bar, so this avoids the double Map
61924
+ * lookup in `getRendererForBar` per label per layout. Returns
61925
+ * `null` when the note's bar is not rendered on this glyph's
61926
+ * staff (cross-system case).
61927
+ */
61928
+ resolveLabelAnchorX(note) {
61929
+ const bar = note.beat.voice.bar;
61930
+ let renderer = null;
61931
+ const start = this.lookupStartBeatRenderer();
61932
+ if (start !== null && start.bar === bar) {
61933
+ renderer = start;
61668
61934
  }
61669
61935
  else {
61670
- TieGlyph.paintTie(canvas, 1, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, this.tieDirection === BeamDirection.Down, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
61936
+ const end = this.lookupEndBeatRenderer();
61937
+ if (end !== null && end.bar === bar) {
61938
+ renderer = end;
61939
+ }
61940
+ else {
61941
+ renderer = this.renderer.scoreRenderer.layout.getRendererForBar(this.renderer.staff.staffId, bar);
61942
+ }
61671
61943
  }
61944
+ if (renderer === null) {
61945
+ return null;
61946
+ }
61947
+ return renderer.x + renderer.getNoteX(note, NoteXPosition.Center);
61672
61948
  }
61673
61949
  getTieHeight(_startX, _startY, _endX, _endY) {
61674
61950
  return this.renderer.smuflMetrics.tieHeight;
@@ -61688,11 +61964,18 @@ class TieGlyph extends Glyph {
61688
61964
  }
61689
61965
  static calculateActualTieHeight(scale, x1, y1, x2, y2, down, offset, size) {
61690
61966
  const cp = TieGlyph._computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size);
61967
+ return TieGlyph._calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size);
61968
+ }
61969
+ /**
61970
+ * Derives the bounding box for a tie from already-computed control
61971
+ * points. Splits the bbox math from cps generation so callers that
61972
+ * need BOTH cps and bbox (e.g. multi-label slur layout) avoid a
61973
+ * second call to `_computeBezierControlPoints`.
61974
+ */
61975
+ static _calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size) {
61691
61976
  if (cp.length === 0) {
61692
61977
  return new Bounds(x1, y1, x2 - x1, y2 - y1);
61693
61978
  }
61694
- // For a musical tie/slur, the extrema occur predictably near the midpoint
61695
- // Evaluate at midpoint (t=0.5) and check endpoints
61696
61979
  const p0x = cp[0];
61697
61980
  const p0y = cp[1];
61698
61981
  const c1x = cp[2];
@@ -61701,15 +61984,12 @@ class TieGlyph extends Glyph {
61701
61984
  const c2y = cp[5];
61702
61985
  const p1x = cp[6];
61703
61986
  const p1y = cp[7];
61704
- // Evaluate at t=0.5 for midpoint
61705
61987
  const midX = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x;
61706
61988
  const midY = 0.125 * p0y + 0.375 * c1y + 0.375 * c2y + 0.125 * p1y;
61707
- // Bounds are simply min/max of start, end, and midpoint
61708
61989
  const xMin = Math.min(p0x, p1x, midX);
61709
61990
  const xMax = Math.max(p0x, p1x, midX);
61710
61991
  let yMin = Math.min(p0y, p1y, midY);
61711
61992
  let yMax = Math.max(p0y, p1y, midY);
61712
- // Account for thickness of the tie/slur
61713
61993
  if (down) {
61714
61994
  yMax += size;
61715
61995
  }
@@ -68898,11 +69178,43 @@ class TabTieGlyph extends NoteTieGlyph {
68898
69178
  }
68899
69179
  }
68900
69180
 
69181
+ /**
69182
+ * Helpers for building `TieGlyphLabel` instances from model-side
69183
+ * {@link SlurSegment}s.
69184
+ * @internal
69185
+ */
69186
+ class TieGlyphLabels {
69187
+ /**
69188
+ * Builds a `TieGlyphLabel` for one segment of a slur. The
69189
+ * `isAscending` flag selects between the H/P glyph for hammer-on
69190
+ * vs. pull-off — score side passes a comparison on `realValue`,
69191
+ * tab side passes a comparison on `fret`.
69192
+ */
69193
+ static build(s, isAscending) {
69194
+ if (s.kind === SlurSegmentKind.LegatoSlide) {
69195
+ return {
69196
+ fromNote: s.fromNote,
69197
+ toNote: s.toNote,
69198
+ text: s.text !== null ? s.text : 'sl.',
69199
+ element: NotationElement.EffectSlideText
69200
+ };
69201
+ }
69202
+ // HammerPull
69203
+ return {
69204
+ fromNote: s.fromNote,
69205
+ toNote: s.toNote,
69206
+ text: s.text !== null ? s.text : isAscending ? 'H' : 'P',
69207
+ element: NotationElement.EffectHammerOnPullOffText
69208
+ };
69209
+ }
69210
+ }
69211
+
68901
69212
  /**
68902
69213
  * @internal
68903
69214
  */
68904
69215
  class TabSlurGlyph extends TabTieGlyph {
68905
69216
  _forSlide;
69217
+ _labels = null;
68906
69218
  constructor(slurEffectId, startNote, endNote, forSlide, forEnd) {
68907
69219
  super(slurEffectId, startNote, endNote, forEnd);
68908
69220
  this._forSlide = forSlide;
@@ -68910,6 +69222,22 @@ class TabSlurGlyph extends TabTieGlyph {
68910
69222
  getTieHeight(startX, _startY, endX, _endY) {
68911
69223
  return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
68912
69224
  }
69225
+ getSlurLabels() {
69226
+ if (this._labels === null) {
69227
+ this._labels = [];
69228
+ const slur = this.startNote.effectSlur;
69229
+ if (slur !== null) {
69230
+ const notationSettings = this.renderer.settings.notation;
69231
+ for (const s of slur.segments) {
69232
+ const label = TieGlyphLabels.build(s, s.toNote.fret >= s.fromNote.fret);
69233
+ if (notationSettings.isNotationElementVisible(label.element)) {
69234
+ this._labels.push(label);
69235
+ }
69236
+ }
69237
+ }
69238
+ }
69239
+ return this._labels.length > 0 ? this._labels : null;
69240
+ }
68913
69241
  tryExpand(startNote, endNote, forSlide, forEnd) {
68914
69242
  // same type required
68915
69243
  if (this._forSlide !== forSlide) {
@@ -68935,6 +69263,7 @@ class TabSlurGlyph extends TabTieGlyph {
68935
69263
  case BeamDirection.Up:
68936
69264
  if (startNote.realValue > this.startNote.realValue) {
68937
69265
  this.startNote = startNote;
69266
+ this._labels = null; // invalidate cache — labels live on startNote
68938
69267
  }
68939
69268
  if (endNote.realValue > this.endNote.realValue) {
68940
69269
  this.endNote = endNote;
@@ -68943,6 +69272,7 @@ class TabSlurGlyph extends TabTieGlyph {
68943
69272
  case BeamDirection.Down:
68944
69273
  if (startNote.realValue < this.startNote.realValue) {
68945
69274
  this.startNote = startNote;
69275
+ this._labels = null;
68946
69276
  }
68947
69277
  if (endNote.realValue < this.endNote.realValue) {
68948
69278
  this.endNote = endNote;
@@ -73680,9 +74010,26 @@ class ScoreTieGlyph extends NoteTieGlyph {
73680
74010
  * @internal
73681
74011
  */
73682
74012
  class ScoreSlurGlyph extends ScoreTieGlyph {
74013
+ _labels = null;
73683
74014
  getTieHeight(startX, _startY, endX, _endY) {
73684
74015
  return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
73685
74016
  }
74017
+ getSlurLabels() {
74018
+ if (this._labels === null) {
74019
+ this._labels = [];
74020
+ const slur = this.startNote.beat.effectSlur;
74021
+ if (slur !== null) {
74022
+ const notationSettings = this.renderer.settings.notation;
74023
+ for (const s of slur.segments) {
74024
+ const label = TieGlyphLabels.build(s, s.toNote.realValue >= s.fromNote.realValue);
74025
+ if (notationSettings.isNotationElementVisible(label.element)) {
74026
+ this._labels.push(label);
74027
+ }
74028
+ }
74029
+ }
74030
+ }
74031
+ return this._labels.length > 0 ? this._labels : null;
74032
+ }
73686
74033
  calculateStartX() {
73687
74034
  return (this.renderer.x +
73688
74035
  (this._isStartCentered()