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

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.1803 (develop, build 1803)
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.1803';
207
+ static date = '2026-05-15T04:13:04.606Z';
208
+ static commit = 'a87a8635a0a0306cdab1c7fe2e9e72576ed6f795';
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
  */
@@ -31942,7 +32036,9 @@ class RenderingResources {
31942
32036
  [NotationElement.RepeatCount, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31943
32037
  [NotationElement.BarNumber, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31944
32038
  [NotationElement.ScoreBendSlur, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31945
- [NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)]
32039
+ [NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)],
32040
+ [NotationElement.EffectHammerOnPullOffText, RenderingResources._effectFont],
32041
+ [NotationElement.EffectSlideText, RenderingResources._effectFont]
31946
32042
  ]);
31947
32043
  /**
31948
32044
  * The name of the SMuFL Font to use for rendering music symbols.
@@ -32233,9 +32329,16 @@ class RenderingResources {
32233
32329
  notationElement = NotationElement.ScoreWords;
32234
32330
  break;
32235
32331
  }
32332
+ return this.getFontForNotationElement(notationElement);
32333
+ }
32334
+ /**
32335
+ * @internal
32336
+ * @param element
32337
+ */
32338
+ getFontForNotationElement(notationElement) {
32236
32339
  return this.elementFonts.has(notationElement)
32237
32340
  ? this.elementFonts.get(notationElement)
32238
- : RenderingResources.defaultFonts.get(NotationElement.ScoreWords);
32341
+ : RenderingResources.defaultFonts.get(notationElement);
32239
32342
  }
32240
32343
  }
32241
32344
 
@@ -49161,6 +49264,21 @@ class MasterBarTickLookup {
49161
49264
  }
49162
49265
  }
49163
49266
 
49267
+ /**
49268
+ * Represents a range of the song that should be played.
49269
+ * @public
49270
+ */
49271
+ class PlaybackRange {
49272
+ /**
49273
+ * The position in midi ticks from where the song should start.
49274
+ */
49275
+ startTick = 0;
49276
+ /**
49277
+ * The position in midi ticks to where the song should be played.
49278
+ */
49279
+ endTick = 0;
49280
+ }
49281
+
49164
49282
  /**
49165
49283
  * Describes how a cursor should be moving.
49166
49284
  * @public
@@ -49321,6 +49439,13 @@ class MidiTickLookup {
49321
49439
  * @internal
49322
49440
  */
49323
49441
  masterBarLookup = new Map();
49442
+ /**
49443
+ * A dictionary of all beat played. The index is the id to {@link Beat.id}.
49444
+ * The value is the bar relative tick time at which the beat was registered during midi generation.
49445
+ * This lookup only contains the first time a Beat is played.
49446
+ * @internal
49447
+ */
49448
+ beatLookup = new Map();
49324
49449
  /**
49325
49450
  * A list of all {@link MasterBarTickLookup} sorted by time.
49326
49451
  */
@@ -49687,10 +49812,22 @@ class MidiTickLookup {
49687
49812
  * @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
49688
49813
  */
49689
49814
  getBeatStart(beat) {
49690
- if (!this.masterBarLookup.has(beat.voice.bar.index)) {
49815
+ if (!this.masterBarLookup.has(beat.voice.bar.index) || !this.beatLookup.has(beat.id)) {
49691
49816
  return 0;
49692
49817
  }
49693
- return this.masterBarLookup.get(beat.voice.bar.index).start + beat.playbackStart;
49818
+ const mb = this.masterBarLookup.get(beat.voice.bar.index);
49819
+ return mb.start + this.beatLookup.get(beat.id).startTick;
49820
+ }
49821
+ /**
49822
+ * Gets the playback range in midi ticks for a given beat.
49823
+ * @param beat The beat to find the time period for.
49824
+ * @returns The relative playback range within the parent masterbar at which the beat start and ends playing
49825
+ */
49826
+ getRelativeBeatPlaybackRange(beat) {
49827
+ if (!this.beatLookup.has(beat.id)) {
49828
+ return undefined;
49829
+ }
49830
+ return this.beatLookup.get(beat.id);
49694
49831
  }
49695
49832
  /**
49696
49833
  * Adds a new {@link MasterBarTickLookup} to the lookup table.
@@ -49708,6 +49845,12 @@ class MidiTickLookup {
49708
49845
  }
49709
49846
  }
49710
49847
  addBeat(beat, start, duration) {
49848
+ if (!this.beatLookup.has(beat.id)) {
49849
+ const playbackRange = new PlaybackRange();
49850
+ playbackRange.startTick = start;
49851
+ playbackRange.endTick = start + duration;
49852
+ this.beatLookup.set(beat.id, playbackRange);
49853
+ }
49711
49854
  const currentMasterBar = this._currentMasterBar;
49712
49855
  if (currentMasterBar) {
49713
49856
  // pre-beat grace notes at the start of the bar we also add the beat to the previous bar
@@ -52291,21 +52434,6 @@ class ActiveBeatsChangedEventArgs {
52291
52434
  }
52292
52435
  }
52293
52436
 
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
52437
  /**
52310
52438
  * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
52311
52439
  * allowing dynamic changing of the underlying instance without loosing aspects like the
@@ -55506,23 +55634,24 @@ class AlphaTabApiBase {
55506
55634
  if (this._selectionStart && this._tickCache) {
55507
55635
  // get the start and stop ticks (which consider properly repeats)
55508
55636
  const tickCache = this._tickCache;
55509
- const realMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
55637
+ const realStartMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
55638
+ const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionStart.beat);
55639
+ const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? this._selectionStart.beat.playbackStart;
55510
55640
  // move to selection start
55511
55641
  this._currentBeat = null; // reset current beat so it is updating the cursor
55512
55642
  if (this._player.state === PlayerState.Paused) {
55513
- this._cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
55643
+ this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1);
55514
55644
  }
55515
- this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
55645
+ this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart;
55516
55646
  // set playback range
55517
55647
  if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) {
55518
- const realMasterBarEnd = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
55648
+ const realEndMasterBarStart = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
55649
+ const endBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionEnd.beat);
55650
+ const endBeatPlaybackEnd = endBeatPlaybackRange?.endTick ??
55651
+ this._selectionEnd.beat.playbackStart + this._selectionEnd.beat.playbackDuration;
55519
55652
  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;
55653
+ range.startTick = realStartMasterBarStart + startBeatPlaybackStart;
55654
+ range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50;
55526
55655
  this.playbackRange = range;
55527
55656
  }
55528
55657
  else {
@@ -61569,6 +61698,12 @@ class TieGlyph extends Glyph {
61569
61698
  _tieHeight = 0;
61570
61699
  _boundingBox;
61571
61700
  _shouldPaint = false;
61701
+ // Resolved per-label paint state. Lazily grown; re-layouts mutate
61702
+ // existing entries in place and update `_resolvedLabelCount` to
61703
+ // signal how many of them are valid this pass.
61704
+ _resolvedLabels = [];
61705
+ _resolvedLabelCount = 0;
61706
+ _labelBaselineOffset = 0;
61572
61707
  get checkForOverflow() {
61573
61708
  return this._shouldPaint && this._boundingBox !== undefined;
61574
61709
  }
@@ -61638,16 +61773,88 @@ class TieGlyph extends Glyph {
61638
61773
  }
61639
61774
  this._boundingBox = undefined;
61640
61775
  this.y = Math.min(this._startY, this._endY);
61776
+ const down = this.tieDirection === BeamDirection.Down;
61641
61777
  let tieBoundingBox;
61778
+ // Bezier control points for the tie. Computed once and reused
61779
+ // for both the bounding box (via _calculateActualTieHeightFromCps)
61780
+ // and label-apex sampling further below — avoids a redundant
61781
+ // call to _computeBezierControlPoints (and its 14-element array
61782
+ // allocation) per labeled slur per layout.
61783
+ let cps = [];
61642
61784
  if (this.shouldDrawBendSlur()) {
61643
61785
  this._tieHeight = 0;
61644
- tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, this.tieDirection === BeamDirection.Down, this.renderer.smuflMetrics.tieHeight);
61786
+ tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, down, this.renderer.smuflMetrics.tieHeight);
61645
61787
  }
61646
61788
  else {
61647
61789
  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);
61790
+ const tieThickness = this.renderer.smuflMetrics.tieMidpointThickness;
61791
+ cps = TieGlyph._computeBezierControlPoints(1, this._startX, this._startY, this._endX, this._endY, down, this._tieHeight, tieThickness);
61792
+ tieBoundingBox = TieGlyph._calculateActualTieHeightFromCps(cps, this._startX, this._startY, this._endX, this._endY, down, tieThickness);
61649
61793
  }
61650
61794
  this._boundingBox = tieBoundingBox;
61795
+ this._resolvedLabelCount = 0;
61796
+ const labels = this.getSlurLabels();
61797
+ if (labels !== null && labels.length > 0 && this.shouldPaintLabels()) {
61798
+ const res = this.renderer.settings.display.resources;
61799
+ const padding = this.renderer.smuflMetrics.oneStaffSpace * 0.25;
61800
+ let maxTextHeight = 0;
61801
+ // Single Y line for all labels — the outer arc apex.
61802
+ // Painted offset adds `padding` on the outward side, so
61803
+ // every label sits the same fixed distance from its arc.
61804
+ const labelLineY = cps.length > 0
61805
+ ? 0.125 * cps[7] + 0.375 * cps[9] + 0.375 * cps[11] + 0.125 * cps[13]
61806
+ : (this._startY + this._endY) / 2;
61807
+ for (const label of labels) {
61808
+ const fromX = this.resolveLabelAnchorX(label.fromNote);
61809
+ const toX = this.resolveLabelAnchorX(label.toNote);
61810
+ if (fromX === null || toX === null) {
61811
+ continue;
61812
+ }
61813
+ const midX = (fromX + toX) / 2;
61814
+ if (midX < this._startX || midX > this._endX) {
61815
+ continue;
61816
+ }
61817
+ // Per-element font.size as an upper bound on glyph
61818
+ // height — avoids per-label measureText calls. All H/P
61819
+ // and sl. labels use the same _effectFont, so this is
61820
+ // typically computed once.
61821
+ const font = res.getFontForNotationElement(label.element);
61822
+ if (font.size > maxTextHeight) {
61823
+ maxTextHeight = font.size;
61824
+ }
61825
+ // grow cache lazily; mutate existing slot in place otherwise
61826
+ let slot;
61827
+ if (this._resolvedLabelCount < this._resolvedLabels.length) {
61828
+ slot = this._resolvedLabels[this._resolvedLabelCount];
61829
+ slot.x = midX;
61830
+ slot.y = labelLineY;
61831
+ slot.text = label.text;
61832
+ slot.element = label.element;
61833
+ }
61834
+ else {
61835
+ slot = {
61836
+ x: midX,
61837
+ y: labelLineY,
61838
+ text: label.text,
61839
+ element: label.element
61840
+ };
61841
+ this._resolvedLabels.push(slot);
61842
+ }
61843
+ this._resolvedLabelCount++;
61844
+ }
61845
+ if (this._resolvedLabelCount > 0) {
61846
+ // canvas.textBaseline is 'hanging' (TextBaseline.Top), so
61847
+ // fillText positions `y` at the glyph's top edge.
61848
+ if (this.tieDirection === BeamDirection.Up) {
61849
+ tieBoundingBox.y -= maxTextHeight + padding;
61850
+ this._labelBaselineOffset = -(maxTextHeight + padding);
61851
+ }
61852
+ else {
61853
+ this._labelBaselineOffset = padding;
61854
+ }
61855
+ tieBoundingBox.h += maxTextHeight + padding;
61856
+ }
61857
+ }
61651
61858
  this.height = tieBoundingBox.h;
61652
61859
  if (this.tieDirection === BeamDirection.Up) {
61653
61860
  // the tie might go above `this.y` due to its shape
@@ -61663,12 +61870,76 @@ class TieGlyph extends Glyph {
61663
61870
  if (!this._shouldPaint) {
61664
61871
  return;
61665
61872
  }
61873
+ const isDown = this.tieDirection === BeamDirection.Down;
61666
61874
  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);
61875
+ TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this.renderer.smuflMetrics.tieHeight);
61876
+ }
61877
+ else {
61878
+ TieGlyph.paintTie(canvas, 1, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
61879
+ }
61880
+ if (this._resolvedLabelCount > 0) {
61881
+ const ta = canvas.textAlign;
61882
+ const tb = canvas.textBaseline;
61883
+ canvas.textAlign = TextAlign.Center;
61884
+ canvas.textBaseline = TextBaseline.Top;
61885
+ const res = this.renderer.resources;
61886
+ let lastElement = -1;
61887
+ for (let i = 0; i < this._resolvedLabelCount; i++) {
61888
+ const label = this._resolvedLabels[i];
61889
+ if (label.element !== lastElement) {
61890
+ canvas.font = res.getFontForNotationElement(label.element);
61891
+ lastElement = label.element;
61892
+ }
61893
+ canvas.fillText(label.text, cx + label.x, cy + label.y + this._labelBaselineOffset);
61894
+ }
61895
+ canvas.textAlign = ta;
61896
+ canvas.textBaseline = tb;
61897
+ }
61898
+ }
61899
+ /**
61900
+ * Returns the labels to paint along this slur, or `null` when there
61901
+ * are none. Override in subclasses.
61902
+ */
61903
+ getSlurLabels() {
61904
+ return null;
61905
+ }
61906
+ /**
61907
+ * Whether label painting is enabled. Defaults to `true`. Subclasses
61908
+ * may override to disable labels on the bend-slur path or other
61909
+ * special cases.
61910
+ */
61911
+ shouldPaintLabels() {
61912
+ return !this.shouldDrawBendSlur();
61913
+ }
61914
+ /**
61915
+ * Looks up the absolute X coordinate of an anchor note. Reuses
61916
+ * the start/end bar renderers already resolved by the subclass
61917
+ * (NoteTieGlyph) when the note's bar matches — most labels live
61918
+ * in the slur's start or end bar, so this avoids the double Map
61919
+ * lookup in `getRendererForBar` per label per layout. Returns
61920
+ * `null` when the note's bar is not rendered on this glyph's
61921
+ * staff (cross-system case).
61922
+ */
61923
+ resolveLabelAnchorX(note) {
61924
+ const bar = note.beat.voice.bar;
61925
+ let renderer = null;
61926
+ const start = this.lookupStartBeatRenderer();
61927
+ if (start !== null && start.bar === bar) {
61928
+ renderer = start;
61668
61929
  }
61669
61930
  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);
61931
+ const end = this.lookupEndBeatRenderer();
61932
+ if (end !== null && end.bar === bar) {
61933
+ renderer = end;
61934
+ }
61935
+ else {
61936
+ renderer = this.renderer.scoreRenderer.layout.getRendererForBar(this.renderer.staff.staffId, bar);
61937
+ }
61938
+ }
61939
+ if (renderer === null) {
61940
+ return null;
61671
61941
  }
61942
+ return renderer.x + renderer.getNoteX(note, NoteXPosition.Center);
61672
61943
  }
61673
61944
  getTieHeight(_startX, _startY, _endX, _endY) {
61674
61945
  return this.renderer.smuflMetrics.tieHeight;
@@ -61688,11 +61959,18 @@ class TieGlyph extends Glyph {
61688
61959
  }
61689
61960
  static calculateActualTieHeight(scale, x1, y1, x2, y2, down, offset, size) {
61690
61961
  const cp = TieGlyph._computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size);
61962
+ return TieGlyph._calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size);
61963
+ }
61964
+ /**
61965
+ * Derives the bounding box for a tie from already-computed control
61966
+ * points. Splits the bbox math from cps generation so callers that
61967
+ * need BOTH cps and bbox (e.g. multi-label slur layout) avoid a
61968
+ * second call to `_computeBezierControlPoints`.
61969
+ */
61970
+ static _calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size) {
61691
61971
  if (cp.length === 0) {
61692
61972
  return new Bounds(x1, y1, x2 - x1, y2 - y1);
61693
61973
  }
61694
- // For a musical tie/slur, the extrema occur predictably near the midpoint
61695
- // Evaluate at midpoint (t=0.5) and check endpoints
61696
61974
  const p0x = cp[0];
61697
61975
  const p0y = cp[1];
61698
61976
  const c1x = cp[2];
@@ -61701,15 +61979,12 @@ class TieGlyph extends Glyph {
61701
61979
  const c2y = cp[5];
61702
61980
  const p1x = cp[6];
61703
61981
  const p1y = cp[7];
61704
- // Evaluate at t=0.5 for midpoint
61705
61982
  const midX = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x;
61706
61983
  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
61984
  const xMin = Math.min(p0x, p1x, midX);
61709
61985
  const xMax = Math.max(p0x, p1x, midX);
61710
61986
  let yMin = Math.min(p0y, p1y, midY);
61711
61987
  let yMax = Math.max(p0y, p1y, midY);
61712
- // Account for thickness of the tie/slur
61713
61988
  if (down) {
61714
61989
  yMax += size;
61715
61990
  }
@@ -68898,11 +69173,43 @@ class TabTieGlyph extends NoteTieGlyph {
68898
69173
  }
68899
69174
  }
68900
69175
 
69176
+ /**
69177
+ * Helpers for building `TieGlyphLabel` instances from model-side
69178
+ * {@link SlurSegment}s.
69179
+ * @internal
69180
+ */
69181
+ class TieGlyphLabels {
69182
+ /**
69183
+ * Builds a `TieGlyphLabel` for one segment of a slur. The
69184
+ * `isAscending` flag selects between the H/P glyph for hammer-on
69185
+ * vs. pull-off — score side passes a comparison on `realValue`,
69186
+ * tab side passes a comparison on `fret`.
69187
+ */
69188
+ static build(s, isAscending) {
69189
+ if (s.kind === SlurSegmentKind.LegatoSlide) {
69190
+ return {
69191
+ fromNote: s.fromNote,
69192
+ toNote: s.toNote,
69193
+ text: s.text !== null ? s.text : 'sl.',
69194
+ element: NotationElement.EffectSlideText
69195
+ };
69196
+ }
69197
+ // HammerPull
69198
+ return {
69199
+ fromNote: s.fromNote,
69200
+ toNote: s.toNote,
69201
+ text: s.text !== null ? s.text : isAscending ? 'H' : 'P',
69202
+ element: NotationElement.EffectHammerOnPullOffText
69203
+ };
69204
+ }
69205
+ }
69206
+
68901
69207
  /**
68902
69208
  * @internal
68903
69209
  */
68904
69210
  class TabSlurGlyph extends TabTieGlyph {
68905
69211
  _forSlide;
69212
+ _labels = null;
68906
69213
  constructor(slurEffectId, startNote, endNote, forSlide, forEnd) {
68907
69214
  super(slurEffectId, startNote, endNote, forEnd);
68908
69215
  this._forSlide = forSlide;
@@ -68910,6 +69217,22 @@ class TabSlurGlyph extends TabTieGlyph {
68910
69217
  getTieHeight(startX, _startY, endX, _endY) {
68911
69218
  return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
68912
69219
  }
69220
+ getSlurLabels() {
69221
+ if (this._labels === null) {
69222
+ this._labels = [];
69223
+ const slur = this.startNote.effectSlur;
69224
+ if (slur !== null) {
69225
+ const notationSettings = this.renderer.settings.notation;
69226
+ for (const s of slur.segments) {
69227
+ const label = TieGlyphLabels.build(s, s.toNote.fret >= s.fromNote.fret);
69228
+ if (notationSettings.isNotationElementVisible(label.element)) {
69229
+ this._labels.push(label);
69230
+ }
69231
+ }
69232
+ }
69233
+ }
69234
+ return this._labels.length > 0 ? this._labels : null;
69235
+ }
68913
69236
  tryExpand(startNote, endNote, forSlide, forEnd) {
68914
69237
  // same type required
68915
69238
  if (this._forSlide !== forSlide) {
@@ -68935,6 +69258,7 @@ class TabSlurGlyph extends TabTieGlyph {
68935
69258
  case BeamDirection.Up:
68936
69259
  if (startNote.realValue > this.startNote.realValue) {
68937
69260
  this.startNote = startNote;
69261
+ this._labels = null; // invalidate cache — labels live on startNote
68938
69262
  }
68939
69263
  if (endNote.realValue > this.endNote.realValue) {
68940
69264
  this.endNote = endNote;
@@ -68943,6 +69267,7 @@ class TabSlurGlyph extends TabTieGlyph {
68943
69267
  case BeamDirection.Down:
68944
69268
  if (startNote.realValue < this.startNote.realValue) {
68945
69269
  this.startNote = startNote;
69270
+ this._labels = null;
68946
69271
  }
68947
69272
  if (endNote.realValue < this.endNote.realValue) {
68948
69273
  this.endNote = endNote;
@@ -73680,9 +74005,26 @@ class ScoreTieGlyph extends NoteTieGlyph {
73680
74005
  * @internal
73681
74006
  */
73682
74007
  class ScoreSlurGlyph extends ScoreTieGlyph {
74008
+ _labels = null;
73683
74009
  getTieHeight(startX, _startY, endX, _endY) {
73684
74010
  return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
73685
74011
  }
74012
+ getSlurLabels() {
74013
+ if (this._labels === null) {
74014
+ this._labels = [];
74015
+ const slur = this.startNote.beat.effectSlur;
74016
+ if (slur !== null) {
74017
+ const notationSettings = this.renderer.settings.notation;
74018
+ for (const s of slur.segments) {
74019
+ const label = TieGlyphLabels.build(s, s.toNote.realValue >= s.fromNote.realValue);
74020
+ if (notationSettings.isNotationElementVisible(label.element)) {
74021
+ this._labels.push(label);
74022
+ }
74023
+ }
74024
+ }
74025
+ }
74026
+ return this._labels.length > 0 ? this._labels : null;
74027
+ }
73686
74028
  calculateStartX() {
73687
74029
  return (this.renderer.x +
73688
74030
  (this._isStartCentered()
@@ -11515,6 +11515,12 @@ declare class MidiTickLookup {
11515
11515
  * @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
11516
11516
  */
11517
11517
  getBeatStart(beat: Beat): number;
11518
+ /**
11519
+ * Gets the playback range in midi ticks for a given beat.
11520
+ * @param beat The beat to find the time period for.
11521
+ * @returns The relative playback range within the parent masterbar at which the beat start and ends playing
11522
+ */
11523
+ getRelativeBeatPlaybackRange(beat: Beat): PlaybackRange | undefined;
11518
11524
  /**
11519
11525
  * Adds a new {@link MasterBarTickLookup} to the lookup table.
11520
11526
  * @param masterBar The item to add.
@@ -12232,7 +12238,15 @@ export declare enum NotationElement {
12232
12238
  /**
12233
12239
  * The slurs shown on bend effects within the score staff.
12234
12240
  */
12235
- ScoreBendSlur = 55
12241
+ ScoreBendSlur = 55,
12242
+ /**
12243
+ * The hammer-on pull-off text shown on slurs.
12244
+ */
12245
+ EffectHammerOnPullOffText = 56,
12246
+ /**
12247
+ * The slide text shown on slurs.
12248
+ */
12249
+ EffectSlideText = 57
12236
12250
  }
12237
12251
 
12238
12252
  /**