@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.
package/dist/alphaTab.js CHANGED
@@ -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
  *
@@ -209,9 +209,9 @@
209
209
  * @internal
210
210
  */
211
211
  class VersionInfo {
212
- static version = '1.9.0-alpha.1785';
213
- static date = '2026-04-27T03:55:02.662Z';
214
- static commit = '760ed909a3d8dc36b159d23b4ff6780e95a3daf1';
212
+ static version = '1.9.0-alpha.1804';
213
+ static date = '2026-05-16T03:54:50.685Z';
214
+ static commit = '6e757c0cbec66598a7fa5327abc9d05616984486';
215
215
  static print(print) {
216
216
  print(`alphaTab ${VersionInfo.version}`);
217
217
  print(`commit: ${VersionInfo.commit}`);
@@ -998,6 +998,38 @@
998
998
  SlideOutType[SlideOutType["PickSlideUp"] = 6] = "PickSlideUp";
999
999
  })(SlideOutType || (SlideOutType = {}));
1000
1000
 
1001
+ /**
1002
+ * A slur arc spanning two notes, optionally with inner articulation
1003
+ * segments. Corresponds conceptually to a MusicXML `<slur>` element
1004
+ * plus the technique spans inside it.
1005
+ *
1006
+ * For this PR only effect slurs (hammer-pull + legato-slide chains)
1007
+ * are derived in `Note.finish()`. Phrase and legato slurs may join
1008
+ * this type in a future PR; a discriminator will be added at that
1009
+ * point.
1010
+ * @internal
1011
+ */
1012
+ class Slur {
1013
+ originNote;
1014
+ destinationNote;
1015
+ segments = [];
1016
+ }
1017
+
1018
+ /**
1019
+ * Articulation kind for an inner span of a {@link Slur}.
1020
+ *
1021
+ * Drives the renderer's font selection (which {@link NotationElement} to
1022
+ * use) and the default label text when {@link SlurSegment.text} is null.
1023
+ * `Note.finish()` classifies the kind once when building the slur; the
1024
+ * renderer never re-derives it.
1025
+ * @internal
1026
+ */
1027
+ var SlurSegmentKind;
1028
+ (function (SlurSegmentKind) {
1029
+ SlurSegmentKind[SlurSegmentKind["HammerPull"] = 0] = "HammerPull";
1030
+ SlurSegmentKind[SlurSegmentKind["LegatoSlide"] = 1] = "LegatoSlide";
1031
+ })(SlurSegmentKind || (SlurSegmentKind = {}));
1032
+
1001
1033
  /**
1002
1034
  * This public enum lists all vibrato types that can be performed.
1003
1035
  * @public
@@ -1338,6 +1370,14 @@
1338
1370
  * The slurs shown on bend effects within the score staff.
1339
1371
  */
1340
1372
  NotationElement[NotationElement["ScoreBendSlur"] = 55] = "ScoreBendSlur";
1373
+ /**
1374
+ * The hammer-on pull-off text shown on slurs.
1375
+ */
1376
+ NotationElement[NotationElement["EffectHammerOnPullOffText"] = 56] = "EffectHammerOnPullOffText";
1377
+ /**
1378
+ * The slide text shown on slurs.
1379
+ */
1380
+ NotationElement[NotationElement["EffectSlideText"] = 57] = "EffectSlideText";
1341
1381
  })(exports.NotationElement || (exports.NotationElement = {}));
1342
1382
  /**
1343
1383
  * The notation settings control how various music notation elements are shown and behaving
@@ -6134,6 +6174,16 @@
6134
6174
  * @json_ignore
6135
6175
  */
6136
6176
  effectSlurDestination = null;
6177
+ /**
6178
+ * The {@link Slur} object whose origin is this note. Populated by
6179
+ * `finish()`; non-null only on the chain-origin note of an effect
6180
+ * slur. Carries the inner articulation segments used by the
6181
+ * renderer to paint H/P/sl. labels along the arc.
6182
+ * @clone_ignore
6183
+ * @json_ignore
6184
+ * @internal
6185
+ */
6186
+ effectSlur = null;
6137
6187
  /**
6138
6188
  * The ornament applied on the note.
6139
6189
  */
@@ -6418,23 +6468,50 @@
6418
6468
  break;
6419
6469
  }
6420
6470
  let effectSlurDestination = null;
6471
+ let effectSlurSegmentKind = null;
6421
6472
  if (this.isHammerPullOrigin && this.hammerPullDestination) {
6422
6473
  effectSlurDestination = this.hammerPullDestination;
6474
+ effectSlurSegmentKind = SlurSegmentKind.HammerPull;
6423
6475
  }
6424
6476
  else if (this.slideOutType === SlideOutType.Legato && this.slideTarget) {
6425
6477
  effectSlurDestination = this.slideTarget;
6478
+ effectSlurSegmentKind = SlurSegmentKind.LegatoSlide;
6426
6479
  }
6427
6480
  if (effectSlurDestination) {
6428
6481
  this.hasEffectSlur = true;
6429
6482
  if (this.effectSlurOrigin && this.beat.pickStroke === PickStroke.None) {
6430
- this.effectSlurOrigin.effectSlurDestination = effectSlurDestination;
6431
- this.effectSlurOrigin.effectSlurDestination.effectSlurOrigin = this.effectSlurOrigin;
6483
+ const chainOrigin = this.effectSlurOrigin;
6484
+ chainOrigin.effectSlurDestination = effectSlurDestination;
6485
+ effectSlurDestination.effectSlurOrigin = chainOrigin;
6432
6486
  this.effectSlurOrigin = null;
6487
+ if (effectSlurSegmentKind !== null && chainOrigin.effectSlur !== null) {
6488
+ chainOrigin.effectSlur.destinationNote = effectSlurDestination;
6489
+ chainOrigin.effectSlur.segments.push({
6490
+ fromNote: this,
6491
+ toNote: effectSlurDestination,
6492
+ kind: effectSlurSegmentKind,
6493
+ text: null
6494
+ });
6495
+ }
6433
6496
  }
6434
6497
  else {
6435
6498
  this.isEffectSlurOrigin = true;
6436
6499
  this.effectSlurDestination = effectSlurDestination;
6437
- this.effectSlurDestination.effectSlurOrigin = this;
6500
+ effectSlurDestination.effectSlurOrigin = this;
6501
+ // Always allocate a fresh Slur — finish() may run twice (worker re-finish);
6502
+ // overwriting unconditionally keeps the derivation idempotent.
6503
+ const slur = new Slur();
6504
+ slur.originNote = this;
6505
+ slur.destinationNote = effectSlurDestination;
6506
+ if (effectSlurSegmentKind !== null) {
6507
+ slur.segments.push({
6508
+ fromNote: this,
6509
+ toNote: effectSlurDestination,
6510
+ kind: effectSlurSegmentKind,
6511
+ text: null
6512
+ });
6513
+ }
6514
+ this.effectSlur = slur;
6438
6515
  }
6439
6516
  }
6440
6517
  // try to detect what kind of bend was used and cleans unneeded points if required
@@ -7684,6 +7761,23 @@
7684
7761
  * @json_ignore
7685
7762
  */
7686
7763
  effectSlurDestination = null;
7764
+ /**
7765
+ * Convenience accessor for the {@link Slur} of this beat. Returns
7766
+ * the effect slur of whichever note in this beat owns it (the
7767
+ * chain-origin note populated during `Note.finish()`), or `null`
7768
+ * when no note in the beat is an effect-slur origin.
7769
+ * @clone_ignore
7770
+ * @json_ignore
7771
+ * @internal
7772
+ */
7773
+ get effectSlur() {
7774
+ for (const n of this.notes) {
7775
+ if (n.effectSlur !== null) {
7776
+ return n.effectSlur;
7777
+ }
7778
+ }
7779
+ return null;
7780
+ }
7687
7781
  /**
7688
7782
  * Gets or sets how the beaming should be done for this beat.
7689
7783
  */
@@ -10227,7 +10321,7 @@
10227
10321
  encoding = 'utf-8';
10228
10322
  }
10229
10323
  const decoder = new TextDecoder(encoding);
10230
- return decoder.decode(data.buffer);
10324
+ return decoder.decode(data);
10231
10325
  }
10232
10326
  static _detectEncoding(data) {
10233
10327
  if (data.length > 2 && data[0] === 0xfe && data[1] === 0xff) {
@@ -19657,7 +19751,7 @@
19657
19751
  static _versionString = 'FICHIER GUITAR PRO ';
19658
19752
  // NOTE: General Midi only defines percussion instruments from 35-81
19659
19753
  // Guitar Pro 5 allowed GS extensions (27-34 and 82-87)
19660
- // GP7-8 do not have all these definitions anymore, this lookup ensures some fallback
19754
+ // GP7-8 do not have all these definitions anymore, this lookup ensures some fallback
19661
19755
  // (even if they are not correct)
19662
19756
  // we can support this properly in future when we allow custom alphaTex articulation definitions
19663
19757
  // then we don't need to rely on GP specifics anymore but handle things on export/import
@@ -21048,12 +21142,13 @@
21048
21142
  * @returns
21049
21143
  */
21050
21144
  static gpReadStringByteLength(data, length, encoding) {
21145
+ // Fixed-width string field: 1 length byte + `length` data bytes, decoded
21146
+ // up to min(stringLength, length). Always consumes 1 + length bytes.
21051
21147
  const stringLength = data.readByte();
21052
- const s = GpBinaryHelpers.gpReadString(data, stringLength, encoding);
21053
- if (stringLength < length) {
21054
- data.skip(length - stringLength);
21055
- }
21056
- return s;
21148
+ const fieldBytes = new Uint8Array(length);
21149
+ data.read(fieldBytes, 0, length);
21150
+ const effectiveLength = Math.min(stringLength, length);
21151
+ return IOHelper.toString(fieldBytes.subarray(0, effectiveLength), encoding);
21057
21152
  }
21058
21153
  }
21059
21154
  /**
@@ -24273,6 +24368,10 @@
24273
24368
  }
24274
24369
  // build masterbar automations
24275
24370
  for (const [barNumber, automations] of this._masterTrackAutomations) {
24371
+ if (barNumber < 0 || barNumber >= this.score.masterBars.length) {
24372
+ // automation references a bar that is not in the score's masterBars list
24373
+ continue;
24374
+ }
24276
24375
  const masterBar = this.score.masterBars[barNumber];
24277
24376
  for (let i = 0, j = automations.length; i < j; i++) {
24278
24377
  const automation = automations[i];
@@ -31948,7 +32047,9 @@
31948
32047
  [exports.NotationElement.RepeatCount, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31949
32048
  [exports.NotationElement.BarNumber, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31950
32049
  [exports.NotationElement.ScoreBendSlur, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31951
- [exports.NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)]
32050
+ [exports.NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)],
32051
+ [exports.NotationElement.EffectHammerOnPullOffText, RenderingResources._effectFont],
32052
+ [exports.NotationElement.EffectSlideText, RenderingResources._effectFont]
31952
32053
  ]);
31953
32054
  /**
31954
32055
  * The name of the SMuFL Font to use for rendering music symbols.
@@ -32239,9 +32340,16 @@
32239
32340
  notationElement = exports.NotationElement.ScoreWords;
32240
32341
  break;
32241
32342
  }
32343
+ return this.getFontForNotationElement(notationElement);
32344
+ }
32345
+ /**
32346
+ * @internal
32347
+ * @param element
32348
+ */
32349
+ getFontForNotationElement(notationElement) {
32242
32350
  return this.elementFonts.has(notationElement)
32243
32351
  ? this.elementFonts.get(notationElement)
32244
- : RenderingResources.defaultFonts.get(exports.NotationElement.ScoreWords);
32352
+ : RenderingResources.defaultFonts.get(notationElement);
32245
32353
  }
32246
32354
  }
32247
32355
 
@@ -49167,6 +49275,21 @@
49167
49275
  }
49168
49276
  }
49169
49277
 
49278
+ /**
49279
+ * Represents a range of the song that should be played.
49280
+ * @public
49281
+ */
49282
+ class PlaybackRange {
49283
+ /**
49284
+ * The position in midi ticks from where the song should start.
49285
+ */
49286
+ startTick = 0;
49287
+ /**
49288
+ * The position in midi ticks to where the song should be played.
49289
+ */
49290
+ endTick = 0;
49291
+ }
49292
+
49170
49293
  /**
49171
49294
  * Describes how a cursor should be moving.
49172
49295
  * @public
@@ -49327,6 +49450,13 @@
49327
49450
  * @internal
49328
49451
  */
49329
49452
  masterBarLookup = new Map();
49453
+ /**
49454
+ * A dictionary of all beat played. The index is the id to {@link Beat.id}.
49455
+ * The value is the bar relative tick time at which the beat was registered during midi generation.
49456
+ * This lookup only contains the first time a Beat is played.
49457
+ * @internal
49458
+ */
49459
+ beatLookup = new Map();
49330
49460
  /**
49331
49461
  * A list of all {@link MasterBarTickLookup} sorted by time.
49332
49462
  */
@@ -49693,10 +49823,22 @@
49693
49823
  * @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
49694
49824
  */
49695
49825
  getBeatStart(beat) {
49696
- if (!this.masterBarLookup.has(beat.voice.bar.index)) {
49826
+ if (!this.masterBarLookup.has(beat.voice.bar.index) || !this.beatLookup.has(beat.id)) {
49697
49827
  return 0;
49698
49828
  }
49699
- return this.masterBarLookup.get(beat.voice.bar.index).start + beat.playbackStart;
49829
+ const mb = this.masterBarLookup.get(beat.voice.bar.index);
49830
+ return mb.start + this.beatLookup.get(beat.id).startTick;
49831
+ }
49832
+ /**
49833
+ * Gets the playback range in midi ticks for a given beat.
49834
+ * @param beat The beat to find the time period for.
49835
+ * @returns The relative playback range within the parent masterbar at which the beat start and ends playing
49836
+ */
49837
+ getRelativeBeatPlaybackRange(beat) {
49838
+ if (!this.beatLookup.has(beat.id)) {
49839
+ return undefined;
49840
+ }
49841
+ return this.beatLookup.get(beat.id);
49700
49842
  }
49701
49843
  /**
49702
49844
  * Adds a new {@link MasterBarTickLookup} to the lookup table.
@@ -49714,6 +49856,12 @@
49714
49856
  }
49715
49857
  }
49716
49858
  addBeat(beat, start, duration) {
49859
+ if (!this.beatLookup.has(beat.id)) {
49860
+ const playbackRange = new PlaybackRange();
49861
+ playbackRange.startTick = start;
49862
+ playbackRange.endTick = start + duration;
49863
+ this.beatLookup.set(beat.id, playbackRange);
49864
+ }
49717
49865
  const currentMasterBar = this._currentMasterBar;
49718
49866
  if (currentMasterBar) {
49719
49867
  // pre-beat grace notes at the start of the bar we also add the beat to the previous bar
@@ -52297,21 +52445,6 @@
52297
52445
  }
52298
52446
  }
52299
52447
 
52300
- /**
52301
- * Represents a range of the song that should be played.
52302
- * @public
52303
- */
52304
- class PlaybackRange {
52305
- /**
52306
- * The position in midi ticks from where the song should start.
52307
- */
52308
- startTick = 0;
52309
- /**
52310
- * The position in midi ticks to where the song should be played.
52311
- */
52312
- endTick = 0;
52313
- }
52314
-
52315
52448
  /**
52316
52449
  * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
52317
52450
  * allowing dynamic changing of the underlying instance without loosing aspects like the
@@ -55512,23 +55645,24 @@
55512
55645
  if (this._selectionStart && this._tickCache) {
55513
55646
  // get the start and stop ticks (which consider properly repeats)
55514
55647
  const tickCache = this._tickCache;
55515
- const realMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
55648
+ const realStartMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
55649
+ const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionStart.beat);
55650
+ const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? this._selectionStart.beat.playbackStart;
55516
55651
  // move to selection start
55517
55652
  this._currentBeat = null; // reset current beat so it is updating the cursor
55518
55653
  if (this._player.state === PlayerState.Paused) {
55519
- this._cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
55654
+ this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1);
55520
55655
  }
55521
- this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
55656
+ this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart;
55522
55657
  // set playback range
55523
55658
  if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) {
55524
- const realMasterBarEnd = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
55659
+ const realEndMasterBarStart = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
55660
+ const endBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionEnd.beat);
55661
+ const endBeatPlaybackEnd = endBeatPlaybackRange?.endTick ??
55662
+ this._selectionEnd.beat.playbackStart + this._selectionEnd.beat.playbackDuration;
55525
55663
  const range = new PlaybackRange();
55526
- range.startTick = realMasterBarStart + this._selectionStart.beat.playbackStart;
55527
- range.endTick =
55528
- realMasterBarEnd +
55529
- this._selectionEnd.beat.playbackStart +
55530
- this._selectionEnd.beat.playbackDuration -
55531
- 50;
55664
+ range.startTick = realStartMasterBarStart + startBeatPlaybackStart;
55665
+ range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50;
55532
55666
  this.playbackRange = range;
55533
55667
  }
55534
55668
  else {
@@ -61575,6 +61709,12 @@
61575
61709
  _tieHeight = 0;
61576
61710
  _boundingBox;
61577
61711
  _shouldPaint = false;
61712
+ // Resolved per-label paint state. Lazily grown; re-layouts mutate
61713
+ // existing entries in place and update `_resolvedLabelCount` to
61714
+ // signal how many of them are valid this pass.
61715
+ _resolvedLabels = [];
61716
+ _resolvedLabelCount = 0;
61717
+ _labelBaselineOffset = 0;
61578
61718
  get checkForOverflow() {
61579
61719
  return this._shouldPaint && this._boundingBox !== undefined;
61580
61720
  }
@@ -61644,16 +61784,88 @@
61644
61784
  }
61645
61785
  this._boundingBox = undefined;
61646
61786
  this.y = Math.min(this._startY, this._endY);
61787
+ const down = this.tieDirection === BeamDirection.Down;
61647
61788
  let tieBoundingBox;
61789
+ // Bezier control points for the tie. Computed once and reused
61790
+ // for both the bounding box (via _calculateActualTieHeightFromCps)
61791
+ // and label-apex sampling further below — avoids a redundant
61792
+ // call to _computeBezierControlPoints (and its 14-element array
61793
+ // allocation) per labeled slur per layout.
61794
+ let cps = [];
61648
61795
  if (this.shouldDrawBendSlur()) {
61649
61796
  this._tieHeight = 0;
61650
- tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, this.tieDirection === BeamDirection.Down, this.renderer.smuflMetrics.tieHeight);
61797
+ tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, down, this.renderer.smuflMetrics.tieHeight);
61651
61798
  }
61652
61799
  else {
61653
61800
  this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY);
61654
- tieBoundingBox = TieGlyph.calculateActualTieHeight(1, this._startX, this._startY, this._endX, this._endY, this.tieDirection === BeamDirection.Down, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
61801
+ const tieThickness = this.renderer.smuflMetrics.tieMidpointThickness;
61802
+ cps = TieGlyph._computeBezierControlPoints(1, this._startX, this._startY, this._endX, this._endY, down, this._tieHeight, tieThickness);
61803
+ tieBoundingBox = TieGlyph._calculateActualTieHeightFromCps(cps, this._startX, this._startY, this._endX, this._endY, down, tieThickness);
61655
61804
  }
61656
61805
  this._boundingBox = tieBoundingBox;
61806
+ this._resolvedLabelCount = 0;
61807
+ const labels = this.getSlurLabels();
61808
+ if (labels !== null && labels.length > 0 && this.shouldPaintLabels()) {
61809
+ const res = this.renderer.settings.display.resources;
61810
+ const padding = this.renderer.smuflMetrics.oneStaffSpace * 0.25;
61811
+ let maxTextHeight = 0;
61812
+ // Single Y line for all labels — the outer arc apex.
61813
+ // Painted offset adds `padding` on the outward side, so
61814
+ // every label sits the same fixed distance from its arc.
61815
+ const labelLineY = cps.length > 0
61816
+ ? 0.125 * cps[7] + 0.375 * cps[9] + 0.375 * cps[11] + 0.125 * cps[13]
61817
+ : (this._startY + this._endY) / 2;
61818
+ for (const label of labels) {
61819
+ const fromX = this.resolveLabelAnchorX(label.fromNote);
61820
+ const toX = this.resolveLabelAnchorX(label.toNote);
61821
+ if (fromX === null || toX === null) {
61822
+ continue;
61823
+ }
61824
+ const midX = (fromX + toX) / 2;
61825
+ if (midX < this._startX || midX > this._endX) {
61826
+ continue;
61827
+ }
61828
+ // Per-element font.size as an upper bound on glyph
61829
+ // height — avoids per-label measureText calls. All H/P
61830
+ // and sl. labels use the same _effectFont, so this is
61831
+ // typically computed once.
61832
+ const font = res.getFontForNotationElement(label.element);
61833
+ if (font.size > maxTextHeight) {
61834
+ maxTextHeight = font.size;
61835
+ }
61836
+ // grow cache lazily; mutate existing slot in place otherwise
61837
+ let slot;
61838
+ if (this._resolvedLabelCount < this._resolvedLabels.length) {
61839
+ slot = this._resolvedLabels[this._resolvedLabelCount];
61840
+ slot.x = midX;
61841
+ slot.y = labelLineY;
61842
+ slot.text = label.text;
61843
+ slot.element = label.element;
61844
+ }
61845
+ else {
61846
+ slot = {
61847
+ x: midX,
61848
+ y: labelLineY,
61849
+ text: label.text,
61850
+ element: label.element
61851
+ };
61852
+ this._resolvedLabels.push(slot);
61853
+ }
61854
+ this._resolvedLabelCount++;
61855
+ }
61856
+ if (this._resolvedLabelCount > 0) {
61857
+ // canvas.textBaseline is 'hanging' (TextBaseline.Top), so
61858
+ // fillText positions `y` at the glyph's top edge.
61859
+ if (this.tieDirection === BeamDirection.Up) {
61860
+ tieBoundingBox.y -= maxTextHeight + padding;
61861
+ this._labelBaselineOffset = -(maxTextHeight + padding);
61862
+ }
61863
+ else {
61864
+ this._labelBaselineOffset = padding;
61865
+ }
61866
+ tieBoundingBox.h += maxTextHeight + padding;
61867
+ }
61868
+ }
61657
61869
  this.height = tieBoundingBox.h;
61658
61870
  if (this.tieDirection === BeamDirection.Up) {
61659
61871
  // the tie might go above `this.y` due to its shape
@@ -61669,12 +61881,76 @@
61669
61881
  if (!this._shouldPaint) {
61670
61882
  return;
61671
61883
  }
61884
+ const isDown = this.tieDirection === BeamDirection.Down;
61672
61885
  if (this.shouldDrawBendSlur()) {
61673
- TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, this.tieDirection === BeamDirection.Down, this.renderer.smuflMetrics.tieHeight);
61886
+ TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this.renderer.smuflMetrics.tieHeight);
61887
+ }
61888
+ else {
61889
+ TieGlyph.paintTie(canvas, 1, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
61890
+ }
61891
+ if (this._resolvedLabelCount > 0) {
61892
+ const ta = canvas.textAlign;
61893
+ const tb = canvas.textBaseline;
61894
+ canvas.textAlign = TextAlign.Center;
61895
+ canvas.textBaseline = TextBaseline.Top;
61896
+ const res = this.renderer.resources;
61897
+ let lastElement = -1;
61898
+ for (let i = 0; i < this._resolvedLabelCount; i++) {
61899
+ const label = this._resolvedLabels[i];
61900
+ if (label.element !== lastElement) {
61901
+ canvas.font = res.getFontForNotationElement(label.element);
61902
+ lastElement = label.element;
61903
+ }
61904
+ canvas.fillText(label.text, cx + label.x, cy + label.y + this._labelBaselineOffset);
61905
+ }
61906
+ canvas.textAlign = ta;
61907
+ canvas.textBaseline = tb;
61908
+ }
61909
+ }
61910
+ /**
61911
+ * Returns the labels to paint along this slur, or `null` when there
61912
+ * are none. Override in subclasses.
61913
+ */
61914
+ getSlurLabels() {
61915
+ return null;
61916
+ }
61917
+ /**
61918
+ * Whether label painting is enabled. Defaults to `true`. Subclasses
61919
+ * may override to disable labels on the bend-slur path or other
61920
+ * special cases.
61921
+ */
61922
+ shouldPaintLabels() {
61923
+ return !this.shouldDrawBendSlur();
61924
+ }
61925
+ /**
61926
+ * Looks up the absolute X coordinate of an anchor note. Reuses
61927
+ * the start/end bar renderers already resolved by the subclass
61928
+ * (NoteTieGlyph) when the note's bar matches — most labels live
61929
+ * in the slur's start or end bar, so this avoids the double Map
61930
+ * lookup in `getRendererForBar` per label per layout. Returns
61931
+ * `null` when the note's bar is not rendered on this glyph's
61932
+ * staff (cross-system case).
61933
+ */
61934
+ resolveLabelAnchorX(note) {
61935
+ const bar = note.beat.voice.bar;
61936
+ let renderer = null;
61937
+ const start = this.lookupStartBeatRenderer();
61938
+ if (start !== null && start.bar === bar) {
61939
+ renderer = start;
61674
61940
  }
61675
61941
  else {
61676
- 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);
61942
+ const end = this.lookupEndBeatRenderer();
61943
+ if (end !== null && end.bar === bar) {
61944
+ renderer = end;
61945
+ }
61946
+ else {
61947
+ renderer = this.renderer.scoreRenderer.layout.getRendererForBar(this.renderer.staff.staffId, bar);
61948
+ }
61677
61949
  }
61950
+ if (renderer === null) {
61951
+ return null;
61952
+ }
61953
+ return renderer.x + renderer.getNoteX(note, NoteXPosition.Center);
61678
61954
  }
61679
61955
  getTieHeight(_startX, _startY, _endX, _endY) {
61680
61956
  return this.renderer.smuflMetrics.tieHeight;
@@ -61694,11 +61970,18 @@
61694
61970
  }
61695
61971
  static calculateActualTieHeight(scale, x1, y1, x2, y2, down, offset, size) {
61696
61972
  const cp = TieGlyph._computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size);
61973
+ return TieGlyph._calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size);
61974
+ }
61975
+ /**
61976
+ * Derives the bounding box for a tie from already-computed control
61977
+ * points. Splits the bbox math from cps generation so callers that
61978
+ * need BOTH cps and bbox (e.g. multi-label slur layout) avoid a
61979
+ * second call to `_computeBezierControlPoints`.
61980
+ */
61981
+ static _calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size) {
61697
61982
  if (cp.length === 0) {
61698
61983
  return new Bounds(x1, y1, x2 - x1, y2 - y1);
61699
61984
  }
61700
- // For a musical tie/slur, the extrema occur predictably near the midpoint
61701
- // Evaluate at midpoint (t=0.5) and check endpoints
61702
61985
  const p0x = cp[0];
61703
61986
  const p0y = cp[1];
61704
61987
  const c1x = cp[2];
@@ -61707,15 +61990,12 @@
61707
61990
  const c2y = cp[5];
61708
61991
  const p1x = cp[6];
61709
61992
  const p1y = cp[7];
61710
- // Evaluate at t=0.5 for midpoint
61711
61993
  const midX = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x;
61712
61994
  const midY = 0.125 * p0y + 0.375 * c1y + 0.375 * c2y + 0.125 * p1y;
61713
- // Bounds are simply min/max of start, end, and midpoint
61714
61995
  const xMin = Math.min(p0x, p1x, midX);
61715
61996
  const xMax = Math.max(p0x, p1x, midX);
61716
61997
  let yMin = Math.min(p0y, p1y, midY);
61717
61998
  let yMax = Math.max(p0y, p1y, midY);
61718
- // Account for thickness of the tie/slur
61719
61999
  if (down) {
61720
62000
  yMax += size;
61721
62001
  }
@@ -68904,11 +69184,43 @@
68904
69184
  }
68905
69185
  }
68906
69186
 
69187
+ /**
69188
+ * Helpers for building `TieGlyphLabel` instances from model-side
69189
+ * {@link SlurSegment}s.
69190
+ * @internal
69191
+ */
69192
+ class TieGlyphLabels {
69193
+ /**
69194
+ * Builds a `TieGlyphLabel` for one segment of a slur. The
69195
+ * `isAscending` flag selects between the H/P glyph for hammer-on
69196
+ * vs. pull-off — score side passes a comparison on `realValue`,
69197
+ * tab side passes a comparison on `fret`.
69198
+ */
69199
+ static build(s, isAscending) {
69200
+ if (s.kind === SlurSegmentKind.LegatoSlide) {
69201
+ return {
69202
+ fromNote: s.fromNote,
69203
+ toNote: s.toNote,
69204
+ text: s.text !== null ? s.text : 'sl.',
69205
+ element: exports.NotationElement.EffectSlideText
69206
+ };
69207
+ }
69208
+ // HammerPull
69209
+ return {
69210
+ fromNote: s.fromNote,
69211
+ toNote: s.toNote,
69212
+ text: s.text !== null ? s.text : isAscending ? 'H' : 'P',
69213
+ element: exports.NotationElement.EffectHammerOnPullOffText
69214
+ };
69215
+ }
69216
+ }
69217
+
68907
69218
  /**
68908
69219
  * @internal
68909
69220
  */
68910
69221
  class TabSlurGlyph extends TabTieGlyph {
68911
69222
  _forSlide;
69223
+ _labels = null;
68912
69224
  constructor(slurEffectId, startNote, endNote, forSlide, forEnd) {
68913
69225
  super(slurEffectId, startNote, endNote, forEnd);
68914
69226
  this._forSlide = forSlide;
@@ -68916,6 +69228,22 @@
68916
69228
  getTieHeight(startX, _startY, endX, _endY) {
68917
69229
  return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
68918
69230
  }
69231
+ getSlurLabels() {
69232
+ if (this._labels === null) {
69233
+ this._labels = [];
69234
+ const slur = this.startNote.effectSlur;
69235
+ if (slur !== null) {
69236
+ const notationSettings = this.renderer.settings.notation;
69237
+ for (const s of slur.segments) {
69238
+ const label = TieGlyphLabels.build(s, s.toNote.fret >= s.fromNote.fret);
69239
+ if (notationSettings.isNotationElementVisible(label.element)) {
69240
+ this._labels.push(label);
69241
+ }
69242
+ }
69243
+ }
69244
+ }
69245
+ return this._labels.length > 0 ? this._labels : null;
69246
+ }
68919
69247
  tryExpand(startNote, endNote, forSlide, forEnd) {
68920
69248
  // same type required
68921
69249
  if (this._forSlide !== forSlide) {
@@ -68941,6 +69269,7 @@
68941
69269
  case BeamDirection.Up:
68942
69270
  if (startNote.realValue > this.startNote.realValue) {
68943
69271
  this.startNote = startNote;
69272
+ this._labels = null; // invalidate cache — labels live on startNote
68944
69273
  }
68945
69274
  if (endNote.realValue > this.endNote.realValue) {
68946
69275
  this.endNote = endNote;
@@ -68949,6 +69278,7 @@
68949
69278
  case BeamDirection.Down:
68950
69279
  if (startNote.realValue < this.startNote.realValue) {
68951
69280
  this.startNote = startNote;
69281
+ this._labels = null;
68952
69282
  }
68953
69283
  if (endNote.realValue < this.endNote.realValue) {
68954
69284
  this.endNote = endNote;
@@ -73686,9 +74016,26 @@
73686
74016
  * @internal
73687
74017
  */
73688
74018
  class ScoreSlurGlyph extends ScoreTieGlyph {
74019
+ _labels = null;
73689
74020
  getTieHeight(startX, _startY, endX, _endY) {
73690
74021
  return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
73691
74022
  }
74023
+ getSlurLabels() {
74024
+ if (this._labels === null) {
74025
+ this._labels = [];
74026
+ const slur = this.startNote.beat.effectSlur;
74027
+ if (slur !== null) {
74028
+ const notationSettings = this.renderer.settings.notation;
74029
+ for (const s of slur.segments) {
74030
+ const label = TieGlyphLabels.build(s, s.toNote.realValue >= s.fromNote.realValue);
74031
+ if (notationSettings.isNotationElementVisible(label.element)) {
74032
+ this._labels.push(label);
74033
+ }
74034
+ }
74035
+ }
74036
+ }
74037
+ return this._labels.length > 0 ? this._labels : null;
74038
+ }
73692
74039
  calculateStartX() {
73693
74040
  return (this.renderer.x +
73694
74041
  (this._isStartCentered()