@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.
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.1803 (develop, build 1803)
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.1803';
213
+ static date = '2026-05-15T04:13:04.606Z';
214
+ static commit = 'a87a8635a0a0306cdab1c7fe2e9e72576ed6f795';
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
  */
@@ -31948,7 +32042,9 @@
31948
32042
  [exports.NotationElement.RepeatCount, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31949
32043
  [exports.NotationElement.BarNumber, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31950
32044
  [exports.NotationElement.ScoreBendSlur, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
31951
- [exports.NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)]
32045
+ [exports.NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)],
32046
+ [exports.NotationElement.EffectHammerOnPullOffText, RenderingResources._effectFont],
32047
+ [exports.NotationElement.EffectSlideText, RenderingResources._effectFont]
31952
32048
  ]);
31953
32049
  /**
31954
32050
  * The name of the SMuFL Font to use for rendering music symbols.
@@ -32239,9 +32335,16 @@
32239
32335
  notationElement = exports.NotationElement.ScoreWords;
32240
32336
  break;
32241
32337
  }
32338
+ return this.getFontForNotationElement(notationElement);
32339
+ }
32340
+ /**
32341
+ * @internal
32342
+ * @param element
32343
+ */
32344
+ getFontForNotationElement(notationElement) {
32242
32345
  return this.elementFonts.has(notationElement)
32243
32346
  ? this.elementFonts.get(notationElement)
32244
- : RenderingResources.defaultFonts.get(exports.NotationElement.ScoreWords);
32347
+ : RenderingResources.defaultFonts.get(notationElement);
32245
32348
  }
32246
32349
  }
32247
32350
 
@@ -49167,6 +49270,21 @@
49167
49270
  }
49168
49271
  }
49169
49272
 
49273
+ /**
49274
+ * Represents a range of the song that should be played.
49275
+ * @public
49276
+ */
49277
+ class PlaybackRange {
49278
+ /**
49279
+ * The position in midi ticks from where the song should start.
49280
+ */
49281
+ startTick = 0;
49282
+ /**
49283
+ * The position in midi ticks to where the song should be played.
49284
+ */
49285
+ endTick = 0;
49286
+ }
49287
+
49170
49288
  /**
49171
49289
  * Describes how a cursor should be moving.
49172
49290
  * @public
@@ -49327,6 +49445,13 @@
49327
49445
  * @internal
49328
49446
  */
49329
49447
  masterBarLookup = new Map();
49448
+ /**
49449
+ * A dictionary of all beat played. The index is the id to {@link Beat.id}.
49450
+ * The value is the bar relative tick time at which the beat was registered during midi generation.
49451
+ * This lookup only contains the first time a Beat is played.
49452
+ * @internal
49453
+ */
49454
+ beatLookup = new Map();
49330
49455
  /**
49331
49456
  * A list of all {@link MasterBarTickLookup} sorted by time.
49332
49457
  */
@@ -49693,10 +49818,22 @@
49693
49818
  * @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
49694
49819
  */
49695
49820
  getBeatStart(beat) {
49696
- if (!this.masterBarLookup.has(beat.voice.bar.index)) {
49821
+ if (!this.masterBarLookup.has(beat.voice.bar.index) || !this.beatLookup.has(beat.id)) {
49697
49822
  return 0;
49698
49823
  }
49699
- return this.masterBarLookup.get(beat.voice.bar.index).start + beat.playbackStart;
49824
+ const mb = this.masterBarLookup.get(beat.voice.bar.index);
49825
+ return mb.start + this.beatLookup.get(beat.id).startTick;
49826
+ }
49827
+ /**
49828
+ * Gets the playback range in midi ticks for a given beat.
49829
+ * @param beat The beat to find the time period for.
49830
+ * @returns The relative playback range within the parent masterbar at which the beat start and ends playing
49831
+ */
49832
+ getRelativeBeatPlaybackRange(beat) {
49833
+ if (!this.beatLookup.has(beat.id)) {
49834
+ return undefined;
49835
+ }
49836
+ return this.beatLookup.get(beat.id);
49700
49837
  }
49701
49838
  /**
49702
49839
  * Adds a new {@link MasterBarTickLookup} to the lookup table.
@@ -49714,6 +49851,12 @@
49714
49851
  }
49715
49852
  }
49716
49853
  addBeat(beat, start, duration) {
49854
+ if (!this.beatLookup.has(beat.id)) {
49855
+ const playbackRange = new PlaybackRange();
49856
+ playbackRange.startTick = start;
49857
+ playbackRange.endTick = start + duration;
49858
+ this.beatLookup.set(beat.id, playbackRange);
49859
+ }
49717
49860
  const currentMasterBar = this._currentMasterBar;
49718
49861
  if (currentMasterBar) {
49719
49862
  // pre-beat grace notes at the start of the bar we also add the beat to the previous bar
@@ -52297,21 +52440,6 @@
52297
52440
  }
52298
52441
  }
52299
52442
 
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
52443
  /**
52316
52444
  * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
52317
52445
  * allowing dynamic changing of the underlying instance without loosing aspects like the
@@ -55512,23 +55640,24 @@
55512
55640
  if (this._selectionStart && this._tickCache) {
55513
55641
  // get the start and stop ticks (which consider properly repeats)
55514
55642
  const tickCache = this._tickCache;
55515
- const realMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
55643
+ const realStartMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
55644
+ const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionStart.beat);
55645
+ const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? this._selectionStart.beat.playbackStart;
55516
55646
  // move to selection start
55517
55647
  this._currentBeat = null; // reset current beat so it is updating the cursor
55518
55648
  if (this._player.state === PlayerState.Paused) {
55519
- this._cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
55649
+ this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1);
55520
55650
  }
55521
- this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
55651
+ this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart;
55522
55652
  // set playback range
55523
55653
  if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) {
55524
- const realMasterBarEnd = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
55654
+ const realEndMasterBarStart = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
55655
+ const endBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionEnd.beat);
55656
+ const endBeatPlaybackEnd = endBeatPlaybackRange?.endTick ??
55657
+ this._selectionEnd.beat.playbackStart + this._selectionEnd.beat.playbackDuration;
55525
55658
  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;
55659
+ range.startTick = realStartMasterBarStart + startBeatPlaybackStart;
55660
+ range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50;
55532
55661
  this.playbackRange = range;
55533
55662
  }
55534
55663
  else {
@@ -61575,6 +61704,12 @@
61575
61704
  _tieHeight = 0;
61576
61705
  _boundingBox;
61577
61706
  _shouldPaint = false;
61707
+ // Resolved per-label paint state. Lazily grown; re-layouts mutate
61708
+ // existing entries in place and update `_resolvedLabelCount` to
61709
+ // signal how many of them are valid this pass.
61710
+ _resolvedLabels = [];
61711
+ _resolvedLabelCount = 0;
61712
+ _labelBaselineOffset = 0;
61578
61713
  get checkForOverflow() {
61579
61714
  return this._shouldPaint && this._boundingBox !== undefined;
61580
61715
  }
@@ -61644,16 +61779,88 @@
61644
61779
  }
61645
61780
  this._boundingBox = undefined;
61646
61781
  this.y = Math.min(this._startY, this._endY);
61782
+ const down = this.tieDirection === BeamDirection.Down;
61647
61783
  let tieBoundingBox;
61784
+ // Bezier control points for the tie. Computed once and reused
61785
+ // for both the bounding box (via _calculateActualTieHeightFromCps)
61786
+ // and label-apex sampling further below — avoids a redundant
61787
+ // call to _computeBezierControlPoints (and its 14-element array
61788
+ // allocation) per labeled slur per layout.
61789
+ let cps = [];
61648
61790
  if (this.shouldDrawBendSlur()) {
61649
61791
  this._tieHeight = 0;
61650
- tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, this.tieDirection === BeamDirection.Down, this.renderer.smuflMetrics.tieHeight);
61792
+ tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, down, this.renderer.smuflMetrics.tieHeight);
61651
61793
  }
61652
61794
  else {
61653
61795
  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);
61796
+ const tieThickness = this.renderer.smuflMetrics.tieMidpointThickness;
61797
+ cps = TieGlyph._computeBezierControlPoints(1, this._startX, this._startY, this._endX, this._endY, down, this._tieHeight, tieThickness);
61798
+ tieBoundingBox = TieGlyph._calculateActualTieHeightFromCps(cps, this._startX, this._startY, this._endX, this._endY, down, tieThickness);
61655
61799
  }
61656
61800
  this._boundingBox = tieBoundingBox;
61801
+ this._resolvedLabelCount = 0;
61802
+ const labels = this.getSlurLabels();
61803
+ if (labels !== null && labels.length > 0 && this.shouldPaintLabels()) {
61804
+ const res = this.renderer.settings.display.resources;
61805
+ const padding = this.renderer.smuflMetrics.oneStaffSpace * 0.25;
61806
+ let maxTextHeight = 0;
61807
+ // Single Y line for all labels — the outer arc apex.
61808
+ // Painted offset adds `padding` on the outward side, so
61809
+ // every label sits the same fixed distance from its arc.
61810
+ const labelLineY = cps.length > 0
61811
+ ? 0.125 * cps[7] + 0.375 * cps[9] + 0.375 * cps[11] + 0.125 * cps[13]
61812
+ : (this._startY + this._endY) / 2;
61813
+ for (const label of labels) {
61814
+ const fromX = this.resolveLabelAnchorX(label.fromNote);
61815
+ const toX = this.resolveLabelAnchorX(label.toNote);
61816
+ if (fromX === null || toX === null) {
61817
+ continue;
61818
+ }
61819
+ const midX = (fromX + toX) / 2;
61820
+ if (midX < this._startX || midX > this._endX) {
61821
+ continue;
61822
+ }
61823
+ // Per-element font.size as an upper bound on glyph
61824
+ // height — avoids per-label measureText calls. All H/P
61825
+ // and sl. labels use the same _effectFont, so this is
61826
+ // typically computed once.
61827
+ const font = res.getFontForNotationElement(label.element);
61828
+ if (font.size > maxTextHeight) {
61829
+ maxTextHeight = font.size;
61830
+ }
61831
+ // grow cache lazily; mutate existing slot in place otherwise
61832
+ let slot;
61833
+ if (this._resolvedLabelCount < this._resolvedLabels.length) {
61834
+ slot = this._resolvedLabels[this._resolvedLabelCount];
61835
+ slot.x = midX;
61836
+ slot.y = labelLineY;
61837
+ slot.text = label.text;
61838
+ slot.element = label.element;
61839
+ }
61840
+ else {
61841
+ slot = {
61842
+ x: midX,
61843
+ y: labelLineY,
61844
+ text: label.text,
61845
+ element: label.element
61846
+ };
61847
+ this._resolvedLabels.push(slot);
61848
+ }
61849
+ this._resolvedLabelCount++;
61850
+ }
61851
+ if (this._resolvedLabelCount > 0) {
61852
+ // canvas.textBaseline is 'hanging' (TextBaseline.Top), so
61853
+ // fillText positions `y` at the glyph's top edge.
61854
+ if (this.tieDirection === BeamDirection.Up) {
61855
+ tieBoundingBox.y -= maxTextHeight + padding;
61856
+ this._labelBaselineOffset = -(maxTextHeight + padding);
61857
+ }
61858
+ else {
61859
+ this._labelBaselineOffset = padding;
61860
+ }
61861
+ tieBoundingBox.h += maxTextHeight + padding;
61862
+ }
61863
+ }
61657
61864
  this.height = tieBoundingBox.h;
61658
61865
  if (this.tieDirection === BeamDirection.Up) {
61659
61866
  // the tie might go above `this.y` due to its shape
@@ -61669,12 +61876,76 @@
61669
61876
  if (!this._shouldPaint) {
61670
61877
  return;
61671
61878
  }
61879
+ const isDown = this.tieDirection === BeamDirection.Down;
61672
61880
  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);
61881
+ TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this.renderer.smuflMetrics.tieHeight);
61882
+ }
61883
+ else {
61884
+ TieGlyph.paintTie(canvas, 1, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
61885
+ }
61886
+ if (this._resolvedLabelCount > 0) {
61887
+ const ta = canvas.textAlign;
61888
+ const tb = canvas.textBaseline;
61889
+ canvas.textAlign = TextAlign.Center;
61890
+ canvas.textBaseline = TextBaseline.Top;
61891
+ const res = this.renderer.resources;
61892
+ let lastElement = -1;
61893
+ for (let i = 0; i < this._resolvedLabelCount; i++) {
61894
+ const label = this._resolvedLabels[i];
61895
+ if (label.element !== lastElement) {
61896
+ canvas.font = res.getFontForNotationElement(label.element);
61897
+ lastElement = label.element;
61898
+ }
61899
+ canvas.fillText(label.text, cx + label.x, cy + label.y + this._labelBaselineOffset);
61900
+ }
61901
+ canvas.textAlign = ta;
61902
+ canvas.textBaseline = tb;
61903
+ }
61904
+ }
61905
+ /**
61906
+ * Returns the labels to paint along this slur, or `null` when there
61907
+ * are none. Override in subclasses.
61908
+ */
61909
+ getSlurLabels() {
61910
+ return null;
61911
+ }
61912
+ /**
61913
+ * Whether label painting is enabled. Defaults to `true`. Subclasses
61914
+ * may override to disable labels on the bend-slur path or other
61915
+ * special cases.
61916
+ */
61917
+ shouldPaintLabels() {
61918
+ return !this.shouldDrawBendSlur();
61919
+ }
61920
+ /**
61921
+ * Looks up the absolute X coordinate of an anchor note. Reuses
61922
+ * the start/end bar renderers already resolved by the subclass
61923
+ * (NoteTieGlyph) when the note's bar matches — most labels live
61924
+ * in the slur's start or end bar, so this avoids the double Map
61925
+ * lookup in `getRendererForBar` per label per layout. Returns
61926
+ * `null` when the note's bar is not rendered on this glyph's
61927
+ * staff (cross-system case).
61928
+ */
61929
+ resolveLabelAnchorX(note) {
61930
+ const bar = note.beat.voice.bar;
61931
+ let renderer = null;
61932
+ const start = this.lookupStartBeatRenderer();
61933
+ if (start !== null && start.bar === bar) {
61934
+ renderer = start;
61674
61935
  }
61675
61936
  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);
61937
+ const end = this.lookupEndBeatRenderer();
61938
+ if (end !== null && end.bar === bar) {
61939
+ renderer = end;
61940
+ }
61941
+ else {
61942
+ renderer = this.renderer.scoreRenderer.layout.getRendererForBar(this.renderer.staff.staffId, bar);
61943
+ }
61944
+ }
61945
+ if (renderer === null) {
61946
+ return null;
61677
61947
  }
61948
+ return renderer.x + renderer.getNoteX(note, NoteXPosition.Center);
61678
61949
  }
61679
61950
  getTieHeight(_startX, _startY, _endX, _endY) {
61680
61951
  return this.renderer.smuflMetrics.tieHeight;
@@ -61694,11 +61965,18 @@
61694
61965
  }
61695
61966
  static calculateActualTieHeight(scale, x1, y1, x2, y2, down, offset, size) {
61696
61967
  const cp = TieGlyph._computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size);
61968
+ return TieGlyph._calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size);
61969
+ }
61970
+ /**
61971
+ * Derives the bounding box for a tie from already-computed control
61972
+ * points. Splits the bbox math from cps generation so callers that
61973
+ * need BOTH cps and bbox (e.g. multi-label slur layout) avoid a
61974
+ * second call to `_computeBezierControlPoints`.
61975
+ */
61976
+ static _calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size) {
61697
61977
  if (cp.length === 0) {
61698
61978
  return new Bounds(x1, y1, x2 - x1, y2 - y1);
61699
61979
  }
61700
- // For a musical tie/slur, the extrema occur predictably near the midpoint
61701
- // Evaluate at midpoint (t=0.5) and check endpoints
61702
61980
  const p0x = cp[0];
61703
61981
  const p0y = cp[1];
61704
61982
  const c1x = cp[2];
@@ -61707,15 +61985,12 @@
61707
61985
  const c2y = cp[5];
61708
61986
  const p1x = cp[6];
61709
61987
  const p1y = cp[7];
61710
- // Evaluate at t=0.5 for midpoint
61711
61988
  const midX = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x;
61712
61989
  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
61990
  const xMin = Math.min(p0x, p1x, midX);
61715
61991
  const xMax = Math.max(p0x, p1x, midX);
61716
61992
  let yMin = Math.min(p0y, p1y, midY);
61717
61993
  let yMax = Math.max(p0y, p1y, midY);
61718
- // Account for thickness of the tie/slur
61719
61994
  if (down) {
61720
61995
  yMax += size;
61721
61996
  }
@@ -68904,11 +69179,43 @@
68904
69179
  }
68905
69180
  }
68906
69181
 
69182
+ /**
69183
+ * Helpers for building `TieGlyphLabel` instances from model-side
69184
+ * {@link SlurSegment}s.
69185
+ * @internal
69186
+ */
69187
+ class TieGlyphLabels {
69188
+ /**
69189
+ * Builds a `TieGlyphLabel` for one segment of a slur. The
69190
+ * `isAscending` flag selects between the H/P glyph for hammer-on
69191
+ * vs. pull-off — score side passes a comparison on `realValue`,
69192
+ * tab side passes a comparison on `fret`.
69193
+ */
69194
+ static build(s, isAscending) {
69195
+ if (s.kind === SlurSegmentKind.LegatoSlide) {
69196
+ return {
69197
+ fromNote: s.fromNote,
69198
+ toNote: s.toNote,
69199
+ text: s.text !== null ? s.text : 'sl.',
69200
+ element: exports.NotationElement.EffectSlideText
69201
+ };
69202
+ }
69203
+ // HammerPull
69204
+ return {
69205
+ fromNote: s.fromNote,
69206
+ toNote: s.toNote,
69207
+ text: s.text !== null ? s.text : isAscending ? 'H' : 'P',
69208
+ element: exports.NotationElement.EffectHammerOnPullOffText
69209
+ };
69210
+ }
69211
+ }
69212
+
68907
69213
  /**
68908
69214
  * @internal
68909
69215
  */
68910
69216
  class TabSlurGlyph extends TabTieGlyph {
68911
69217
  _forSlide;
69218
+ _labels = null;
68912
69219
  constructor(slurEffectId, startNote, endNote, forSlide, forEnd) {
68913
69220
  super(slurEffectId, startNote, endNote, forEnd);
68914
69221
  this._forSlide = forSlide;
@@ -68916,6 +69223,22 @@
68916
69223
  getTieHeight(startX, _startY, endX, _endY) {
68917
69224
  return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
68918
69225
  }
69226
+ getSlurLabels() {
69227
+ if (this._labels === null) {
69228
+ this._labels = [];
69229
+ const slur = this.startNote.effectSlur;
69230
+ if (slur !== null) {
69231
+ const notationSettings = this.renderer.settings.notation;
69232
+ for (const s of slur.segments) {
69233
+ const label = TieGlyphLabels.build(s, s.toNote.fret >= s.fromNote.fret);
69234
+ if (notationSettings.isNotationElementVisible(label.element)) {
69235
+ this._labels.push(label);
69236
+ }
69237
+ }
69238
+ }
69239
+ }
69240
+ return this._labels.length > 0 ? this._labels : null;
69241
+ }
68919
69242
  tryExpand(startNote, endNote, forSlide, forEnd) {
68920
69243
  // same type required
68921
69244
  if (this._forSlide !== forSlide) {
@@ -68941,6 +69264,7 @@
68941
69264
  case BeamDirection.Up:
68942
69265
  if (startNote.realValue > this.startNote.realValue) {
68943
69266
  this.startNote = startNote;
69267
+ this._labels = null; // invalidate cache — labels live on startNote
68944
69268
  }
68945
69269
  if (endNote.realValue > this.endNote.realValue) {
68946
69270
  this.endNote = endNote;
@@ -68949,6 +69273,7 @@
68949
69273
  case BeamDirection.Down:
68950
69274
  if (startNote.realValue < this.startNote.realValue) {
68951
69275
  this.startNote = startNote;
69276
+ this._labels = null;
68952
69277
  }
68953
69278
  if (endNote.realValue < this.endNote.realValue) {
68954
69279
  this.endNote = endNote;
@@ -73686,9 +74011,26 @@
73686
74011
  * @internal
73687
74012
  */
73688
74013
  class ScoreSlurGlyph extends ScoreTieGlyph {
74014
+ _labels = null;
73689
74015
  getTieHeight(startX, _startY, endX, _endY) {
73690
74016
  return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
73691
74017
  }
74018
+ getSlurLabels() {
74019
+ if (this._labels === null) {
74020
+ this._labels = [];
74021
+ const slur = this.startNote.beat.effectSlur;
74022
+ if (slur !== null) {
74023
+ const notationSettings = this.renderer.settings.notation;
74024
+ for (const s of slur.segments) {
74025
+ const label = TieGlyphLabels.build(s, s.toNote.realValue >= s.fromNote.realValue);
74026
+ if (notationSettings.isNotationElementVisible(label.element)) {
74027
+ this._labels.push(label);
74028
+ }
74029
+ }
74030
+ }
74031
+ }
74032
+ return this._labels.length > 0 ? this._labels : null;
74033
+ }
73692
74034
  calculateStartX() {
73693
74035
  return (this.renderer.x +
73694
74036
  (this._isStartCentered()