@coderline/alphatab 1.9.0-alpha.1768 → 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.1768 (develop, build 1768)
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.1768';
207
- static date = '2026-04-10T03:35:57.433Z';
208
- static commit = '759bd788fc094ab5cb4db380f6a7657e459c14bf';
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
 
@@ -32628,9 +32731,16 @@ class DisplaySettings {
32628
32731
  *
32629
32732
  * The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display.
32630
32733
  *
32631
- * Also note that the sizing is including any glyphs and notation elements within the bar. e.g. if there are clefs in the bar, they are still "squeezed" into the available size.
32632
- * It is not the case that the actual notes with their lengths are sized accordingly. This fits the sizing system of Guitar Pro and when files are customized there,
32633
- * alphaTab will match this layout quite close.
32734
+ * In both modes, prefix and postfix glyphs (clef, key signature, time signature, barlines) are treated as fixed overhead: they keep their
32735
+ * natural size and the remaining staff width is distributed across bars by a per-bar weight. This matches the convention used by
32736
+ * Guitar Pro, Dorico, Finale, Sibelius and MuseScore. Bars that carry a system-start prefix or a mid-line clef/key/time-signature change
32737
+ * are therefore visibly wider than plain bars with the same weight. The weight source depends on the mode:
32738
+ *
32739
+ * * `Automatic` (default for `page` layout): weights come from the built-in spacing engine (the natural content width of each bar).
32740
+ * `displayScale` on the model is ignored.
32741
+ * * `UseModelLayout` (and the `parchment` layout): weights come from `bar.displayScale` / `masterBar.displayScale`. An unset
32742
+ * `displayScale` defaults to `1` and behaves identically to an explicit `1`, matching Guitar Pro (which omits the value when the
32743
+ * author hasn't customized it).
32634
32744
  *
32635
32745
  * ### Horizontal Layout
32636
32746
  *
@@ -38829,14 +38939,25 @@ class StaffSystemBounds {
38829
38939
  */
38830
38940
  boundsLookup;
38831
38941
  /**
38832
- * Finished the lookup for optimized access.
38942
+ * Whether this system's bounds have already been scaled via `finish`. Prevents double-scaling
38943
+ * when the parent `BoundsLookup` is preserved across partial renders and `finish` is invoked
38944
+ * again on a mix of already-scaled (preserved) and newly-registered (natural-coordinate) systems.
38945
+ */
38946
+ isFinished = false;
38947
+ /**
38948
+ * Finished the lookup for optimized access. Idempotent: once finished, further calls are no-ops
38949
+ * so preserved systems survive partial renders without being re-scaled.
38833
38950
  */
38834
38951
  finish(scale = 1) {
38952
+ if (this.isFinished) {
38953
+ return;
38954
+ }
38835
38955
  this.realBounds.scaleWith(scale);
38836
38956
  this.visualBounds.scaleWith(scale);
38837
38957
  for (const t of this.bars) {
38838
38958
  t.finish(scale);
38839
38959
  }
38960
+ this.isFinished = true;
38840
38961
  }
38841
38962
  /**
38842
38963
  * Adds a new master bar to this lookup.
@@ -39009,6 +39130,58 @@ class BoundsLookup {
39009
39130
  }
39010
39131
  this.isFinished = true;
39011
39132
  }
39133
+ /**
39134
+ * Re-opens the lookup for registrations without discarding previously registered bounds.
39135
+ * Used by the renderer when it preserves this lookup across a partial render so that new
39136
+ * bounds for the re-layouted range can be added while preserved systems stay intact.
39137
+ * @internal
39138
+ */
39139
+ resetForPartialUpdate() {
39140
+ this.isFinished = false;
39141
+ }
39142
+ /**
39143
+ * Removes all entries belonging to the given master bar index and any bars after it.
39144
+ * Used before a partial render re-registers bounds for the re-layouted range, so the
39145
+ * preserved lookup ends up with only the unchanged entries when registration begins.
39146
+ *
39147
+ * Assumes the layout aligns its re-layouted range to system boundaries - i.e. the first
39148
+ * system to clear starts exactly at `masterBarIndex`. Caller is responsible for passing
39149
+ * the first master-bar-index of the first re-layouted system.
39150
+ * @internal
39151
+ */
39152
+ clearFromMasterBar(masterBarIndex) {
39153
+ // drop staff systems whose bars start at or after the cleared range.
39154
+ let firstRemovedSystem = -1;
39155
+ for (let i = 0; i < this.staffSystems.length; i++) {
39156
+ const systemBars = this.staffSystems[i].bars;
39157
+ if (systemBars.length > 0 && systemBars[0].index >= masterBarIndex) {
39158
+ firstRemovedSystem = i;
39159
+ break;
39160
+ }
39161
+ }
39162
+ if (firstRemovedSystem !== -1) {
39163
+ this.staffSystems.splice(firstRemovedSystem, this.staffSystems.length - firstRemovedSystem);
39164
+ }
39165
+ // drop master bar entries at or beyond the cleared range.
39166
+ for (const key of Array.from(this._masterBarLookup.keys())) {
39167
+ if (key >= masterBarIndex) {
39168
+ this._masterBarLookup.delete(key);
39169
+ }
39170
+ }
39171
+ // drop beat entries whose beats belong to cleared bars.
39172
+ for (const key of Array.from(this._beatLookup.keys())) {
39173
+ const list = this._beatLookup.get(key);
39174
+ const filtered = list.filter(b => b.beat.voice.bar.index < masterBarIndex);
39175
+ if (filtered.length === 0) {
39176
+ this._beatLookup.delete(key);
39177
+ }
39178
+ else if (filtered.length !== list.length) {
39179
+ this._beatLookup.set(key, filtered);
39180
+ }
39181
+ }
39182
+ // drop the in-progress pointer - the next addStaffSystem call will replace it.
39183
+ this._currentStaffSystem = null;
39184
+ }
39012
39185
  /**
39013
39186
  * Adds a new staff sytem to the lookup.
39014
39187
  * @param bounds The staff system bounds to add.
@@ -46840,7 +47013,15 @@ class ScoreRenderer {
46840
47013
  Logger.warning('Rendering', 'AlphaTab skipped rendering because of width=0 (element invisible)', null);
46841
47014
  return;
46842
47015
  }
46843
- this.boundsLookup = new BoundsLookup();
47016
+ // For partial renders we preserve the existing lookup so bars outside the re-layouted
47017
+ // range keep their already-scaled bounds - the layout will clear the changed range
47018
+ // before the paint pass re-registers fresh entries for it.
47019
+ if (renderHints?.firstChangedMasterBar !== undefined && this.boundsLookup) {
47020
+ this.boundsLookup.resetForPartialUpdate();
47021
+ }
47022
+ else {
47023
+ this.boundsLookup = new BoundsLookup();
47024
+ }
46844
47025
  this._recreateCanvas();
46845
47026
  this.canvas.lineWidth = 1;
46846
47027
  this.canvas.settings = this.settings;
@@ -48714,9 +48895,6 @@ class BeatTickLookup {
48714
48895
  * @param beat The beat to add.
48715
48896
  */
48716
48897
  highlightBeat(beat, playbackStart) {
48717
- if (beat.isEmpty && !beat.voice.isEmpty) {
48718
- return;
48719
- }
48720
48898
  if (!this._highlightedBeats.has(beat.id)) {
48721
48899
  this._highlightedBeats.set(beat.id, true);
48722
48900
  this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart));
@@ -49086,6 +49264,21 @@ class MasterBarTickLookup {
49086
49264
  }
49087
49265
  }
49088
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
+
49089
49282
  /**
49090
49283
  * Describes how a cursor should be moving.
49091
49284
  * @public
@@ -49246,6 +49439,13 @@ class MidiTickLookup {
49246
49439
  * @internal
49247
49440
  */
49248
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();
49249
49449
  /**
49250
49450
  * A list of all {@link MasterBarTickLookup} sorted by time.
49251
49451
  */
@@ -49612,10 +49812,22 @@ class MidiTickLookup {
49612
49812
  * @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
49613
49813
  */
49614
49814
  getBeatStart(beat) {
49615
- if (!this.masterBarLookup.has(beat.voice.bar.index)) {
49815
+ if (!this.masterBarLookup.has(beat.voice.bar.index) || !this.beatLookup.has(beat.id)) {
49616
49816
  return 0;
49617
49817
  }
49618
- 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);
49619
49831
  }
49620
49832
  /**
49621
49833
  * Adds a new {@link MasterBarTickLookup} to the lookup table.
@@ -49633,6 +49845,12 @@ class MidiTickLookup {
49633
49845
  }
49634
49846
  }
49635
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
+ }
49636
49854
  const currentMasterBar = this._currentMasterBar;
49637
49855
  if (currentMasterBar) {
49638
49856
  // pre-beat grace notes at the start of the bar we also add the beat to the previous bar
@@ -50184,7 +50402,13 @@ class MidiFileGenerator {
50184
50402
  let beatStart = beat.playbackStart;
50185
50403
  let audioDuration = beat.playbackDuration;
50186
50404
  const masterBarDuration = beat.voice.bar.masterBar.calculateDuration();
50187
- if (beat.voice.bar.isEmpty) {
50405
+ // For a bar whose voice contains a single empty beat (the typical "whole-bar rest"
50406
+ // placeholder inserted during score.finish), extend the beat's audio duration to cover
50407
+ // the full bar so cursor navigation has a beat to follow across the whole bar. Don't
50408
+ // apply this when the voice has multiple beats: those represent explicit rhythmic
50409
+ // subdivisions even when each beat is empty (e.g. a recording grid of placeholder
50410
+ // slots), and overriding would make every beat overlap the whole bar.
50411
+ if (beat.voice.bar.isEmpty && beat.voice.beats.length === 1) {
50188
50412
  audioDuration = masterBarDuration;
50189
50413
  }
50190
50414
  else if (beat.voice.bar.masterBar.tripletFeel !== TripletFeel.NoTripletFeel &&
@@ -52210,21 +52434,6 @@ class ActiveBeatsChangedEventArgs {
52210
52434
  }
52211
52435
  }
52212
52436
 
52213
- /**
52214
- * Represents a range of the song that should be played.
52215
- * @public
52216
- */
52217
- class PlaybackRange {
52218
- /**
52219
- * The position in midi ticks from where the song should start.
52220
- */
52221
- startTick = 0;
52222
- /**
52223
- * The position in midi ticks to where the song should be played.
52224
- */
52225
- endTick = 0;
52226
- }
52227
-
52228
52437
  /**
52229
52438
  * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
52230
52439
  * allowing dynamic changing of the underlying instance without loosing aspects like the
@@ -54805,7 +55014,8 @@ class AlphaTabApiBase {
54805
55014
  this._isInitialBeatCursorUpdate ||
54806
55015
  barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
54807
55016
  startBeatX < previousBeatBounds.onNotesX ||
54808
- barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
55017
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1 ||
55018
+ barBounds.h !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.h;
54809
55019
  if (jumpCursor) {
54810
55020
  cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
54811
55021
  }
@@ -55424,23 +55634,24 @@ class AlphaTabApiBase {
55424
55634
  if (this._selectionStart && this._tickCache) {
55425
55635
  // get the start and stop ticks (which consider properly repeats)
55426
55636
  const tickCache = this._tickCache;
55427
- 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;
55428
55640
  // move to selection start
55429
55641
  this._currentBeat = null; // reset current beat so it is updating the cursor
55430
55642
  if (this._player.state === PlayerState.Paused) {
55431
- this._cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
55643
+ this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1);
55432
55644
  }
55433
- this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
55645
+ this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart;
55434
55646
  // set playback range
55435
55647
  if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) {
55436
- 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;
55437
55652
  const range = new PlaybackRange();
55438
- range.startTick = realMasterBarStart + this._selectionStart.beat.playbackStart;
55439
- range.endTick =
55440
- realMasterBarEnd +
55441
- this._selectionEnd.beat.playbackStart +
55442
- this._selectionEnd.beat.playbackDuration -
55443
- 50;
55653
+ range.startTick = realStartMasterBarStart + startBeatPlaybackStart;
55654
+ range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50;
55444
55655
  this.playbackRange = range;
55445
55656
  }
55446
55657
  else {
@@ -58045,8 +58256,9 @@ class AlphaTabWebWorker {
58045
58256
  break;
58046
58257
  case 'alphaTab.renderScore':
58047
58258
  this._updateFontSizes(data.fontSizes);
58259
+ const renderHints = data.renderHints;
58048
58260
  const score = data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings);
58049
- this._renderMultiple(score, data.trackIndexes);
58261
+ this._renderMultiple(score, data.trackIndexes, renderHints);
58050
58262
  break;
58051
58263
  case 'alphaTab.updateSettings':
58052
58264
  this._updateSettings(data.settings);
@@ -61486,6 +61698,12 @@ class TieGlyph extends Glyph {
61486
61698
  _tieHeight = 0;
61487
61699
  _boundingBox;
61488
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;
61489
61707
  get checkForOverflow() {
61490
61708
  return this._shouldPaint && this._boundingBox !== undefined;
61491
61709
  }
@@ -61555,16 +61773,88 @@ class TieGlyph extends Glyph {
61555
61773
  }
61556
61774
  this._boundingBox = undefined;
61557
61775
  this.y = Math.min(this._startY, this._endY);
61776
+ const down = this.tieDirection === BeamDirection.Down;
61558
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 = [];
61559
61784
  if (this.shouldDrawBendSlur()) {
61560
61785
  this._tieHeight = 0;
61561
- 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);
61562
61787
  }
61563
61788
  else {
61564
61789
  this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY);
61565
- 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);
61566
61793
  }
61567
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
+ }
61568
61858
  this.height = tieBoundingBox.h;
61569
61859
  if (this.tieDirection === BeamDirection.Up) {
61570
61860
  // the tie might go above `this.y` due to its shape
@@ -61580,13 +61870,77 @@ class TieGlyph extends Glyph {
61580
61870
  if (!this._shouldPaint) {
61581
61871
  return;
61582
61872
  }
61873
+ const isDown = this.tieDirection === BeamDirection.Down;
61583
61874
  if (this.shouldDrawBendSlur()) {
61584
- 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);
61585
61876
  }
61586
61877
  else {
61587
- 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);
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;
61588
61897
  }
61589
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;
61929
+ }
61930
+ else {
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;
61941
+ }
61942
+ return renderer.x + renderer.getNoteX(note, NoteXPosition.Center);
61943
+ }
61590
61944
  getTieHeight(_startX, _startY, _endX, _endY) {
61591
61945
  return this.renderer.smuflMetrics.tieHeight;
61592
61946
  }
@@ -61605,11 +61959,18 @@ class TieGlyph extends Glyph {
61605
61959
  }
61606
61960
  static calculateActualTieHeight(scale, x1, y1, x2, y2, down, offset, size) {
61607
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) {
61608
61971
  if (cp.length === 0) {
61609
61972
  return new Bounds(x1, y1, x2 - x1, y2 - y1);
61610
61973
  }
61611
- // For a musical tie/slur, the extrema occur predictably near the midpoint
61612
- // Evaluate at midpoint (t=0.5) and check endpoints
61613
61974
  const p0x = cp[0];
61614
61975
  const p0y = cp[1];
61615
61976
  const c1x = cp[2];
@@ -61618,15 +61979,12 @@ class TieGlyph extends Glyph {
61618
61979
  const c2y = cp[5];
61619
61980
  const p1x = cp[6];
61620
61981
  const p1y = cp[7];
61621
- // Evaluate at t=0.5 for midpoint
61622
61982
  const midX = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x;
61623
61983
  const midY = 0.125 * p0y + 0.375 * c1y + 0.375 * c2y + 0.125 * p1y;
61624
- // Bounds are simply min/max of start, end, and midpoint
61625
61984
  const xMin = Math.min(p0x, p1x, midX);
61626
61985
  const xMax = Math.max(p0x, p1x, midX);
61627
61986
  let yMin = Math.min(p0y, p1y, midY);
61628
61987
  let yMax = Math.max(p0y, p1y, midY);
61629
- // Account for thickness of the tie/slur
61630
61988
  if (down) {
61631
61989
  yMax += size;
61632
61990
  }
@@ -62919,6 +63277,15 @@ class BarRendererBase {
62919
63277
  }
62920
63278
  return false;
62921
63279
  }
63280
+ /**
63281
+ * The fixed-overhead width of this renderer: glyphs that do not stretch when
63282
+ * the bar is scaled (clef, key signature, time signature, barlines, courtesy
63283
+ * accidentals, etc). Treated as a fixed allocation by the system-level layout
63284
+ * before distributing remaining width across bars by {@link Bar.displayScale}.
63285
+ */
63286
+ get fixedOverhead() {
63287
+ return this._preBeatGlyphs.width + this._postBeatGlyphs.width;
63288
+ }
62922
63289
  scaleToWidth(width) {
62923
63290
  // preBeat and postBeat glyphs do not get resized
62924
63291
  const containerWidth = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width;
@@ -65477,6 +65844,22 @@ class BarLayoutingInfo {
65477
65844
  postBeatSize = 0;
65478
65845
  minStretchForce = 0;
65479
65846
  totalSpringConstant = 0;
65847
+ /**
65848
+ * The smallest note duration encountered within this bar's springs, used as the reference in
65849
+ * the Gourlay stretch formula. Read by the owning {@link StaffSystem} so that the system can
65850
+ * aggregate a shared minimum across all bars and trigger a reconcile if an added bar introduces
65851
+ * a shorter duration than previously seen.
65852
+ */
65853
+ get localMinDuration() {
65854
+ return this._minDuration;
65855
+ }
65856
+ /**
65857
+ * The minimum-duration reference against which the spring constants currently held by this info
65858
+ * were computed. Set by {@link finish} and {@link recomputeSpringConstants}. The owning
65859
+ * StaffSystem compares this against its system-wide minimum to decide whether spring constants
65860
+ * need re-derivation.
65861
+ */
65862
+ computedWithMinDuration = 0;
65480
65863
  _updateMinStretchForce(force) {
65481
65864
  if (this.minStretchForce < force) {
65482
65865
  this.minStretchForce = force;
@@ -65646,10 +66029,26 @@ class BarLayoutingInfo {
65646
66029
  this._incompleteGraceRodsWidth += sp.preBeatWidth + sp.postSpringWidth;
65647
66030
  }
65648
66031
  }
65649
- this._calculateSpringConstants();
66032
+ this._calculateSpringConstants(this._minDuration);
66033
+ this.computedWithMinDuration = this._minDuration;
65650
66034
  this.version++;
65651
66035
  }
65652
- _calculateSpringConstants() {
66036
+ /**
66037
+ * Re-derives the spring constants (and {@link minStretchForce} / {@link totalSpringConstant})
66038
+ * using a caller-supplied minimum-duration reference rather than this bar's local minimum.
66039
+ *
66040
+ * Called by {@link StaffSystem.reconcileMinDurationIfDirty} when a bar added later to the
66041
+ * system introduced a shorter note than previously seen, invalidating this bar's spring
66042
+ * constants. Grace-rod data is not recomputed — it is independent of the minimum-duration
66043
+ * reference. The internal {@link version} is bumped so downstream consumers (e.g.
66044
+ * {@link BarRendererBase.applyLayoutingInfo}) pick up the refreshed positions.
66045
+ */
66046
+ recomputeSpringConstants(minDuration) {
66047
+ this._calculateSpringConstants(minDuration);
66048
+ this.computedWithMinDuration = minDuration;
66049
+ this.version++;
66050
+ }
66051
+ _calculateSpringConstants(minDuration) {
65653
66052
  let totalSpringConstant = 0;
65654
66053
  const sortedSprings = this._timeSortedSprings;
65655
66054
  if (sortedSprings.length === 0) {
@@ -65667,7 +66066,7 @@ class BarLayoutingInfo {
65667
66066
  const nextSpring = sortedSprings[i + 1];
65668
66067
  duration = Math.abs(nextSpring.timePosition - currentSpring.timePosition);
65669
66068
  }
65670
- currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration);
66069
+ currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration, minDuration);
65671
66070
  totalSpringConstant += 1 / currentSpring.springConstant;
65672
66071
  }
65673
66072
  this.totalSpringConstant = 1 / totalSpringConstant;
@@ -65727,7 +66126,7 @@ class BarLayoutingInfo {
65727
66126
  // springX += this.calculateWidth(force, spring.springConstant);
65728
66127
  // }
65729
66128
  // }
65730
- _calculateSpringConstant(spring, duration) {
66129
+ _calculateSpringConstant(spring, duration, minDuration) {
65731
66130
  if (duration <= 0) {
65732
66131
  duration = MidiUtils.toTicks(Duration.TwoHundredFiftySixth);
65733
66132
  }
@@ -65735,7 +66134,6 @@ class BarLayoutingInfo {
65735
66134
  spring.smallestDuration = duration;
65736
66135
  }
65737
66136
  const smallestDuration = spring.smallestDuration;
65738
- const minDuration = this._minDuration;
65739
66137
  const minDurationWidth = BarLayoutingInfo._defaultMinDurationWidth;
65740
66138
  const phi = 1 + 0.85 * Math.log2(duration / minDuration);
65741
66139
  return (smallestDuration / duration) * (1 / (phi * minDurationWidth));
@@ -65798,6 +66196,18 @@ class MasterBarsRenderers {
65798
66196
  canWrap = true;
65799
66197
  masterBar;
65800
66198
  additionalMultiBarRestIndexes = null;
66199
+ /**
66200
+ * Max fixed overhead (prefix + postfix glyph width) across all staves of this bar.
66201
+ * Used by the layout-mode horizontal scaling pass to carve out the fixed-overhead bucket
66202
+ * before distributing staff width across bars.
66203
+ */
66204
+ maxFixedOverhead = 0;
66205
+ /**
66206
+ * Max natural content width (computedWidth - fixedOverhead) across all staves of this bar.
66207
+ * Used as the bar weight when the layout ignores {@link MasterBar.displayScale} (e.g.
66208
+ * Page layout with `SystemsLayoutMode.Automatic`).
66209
+ */
66210
+ maxContentWidth = 0;
65801
66211
  get lastMasterBarIndex() {
65802
66212
  if (this.additionalMultiBarRestIndexes) {
65803
66213
  return this.additionalMultiBarRestIndexes[this.additionalMultiBarRestIndexes.length - 1];
@@ -65979,6 +66389,45 @@ class StaffSystem {
65979
66389
  * This value is mainly used in the parchment style layout for correct scaling of the bars.
65980
66390
  */
65981
66391
  totalBarDisplayScale = 0;
66392
+ /**
66393
+ * Sum of per-bar {@link MasterBarsRenderers.maxFixedOverhead} across the system. The layout-mode
66394
+ * horizontal scaling pass subtracts this from the available staff width before distributing the
66395
+ * remainder across bars.
66396
+ */
66397
+ totalFixedOverhead = 0;
66398
+ /**
66399
+ * Sum of per-bar {@link MasterBarsRenderers.maxContentWidth} across the system. Used as the
66400
+ * denominator when distributing staff width in modes that weight bars by natural content width
66401
+ * (Page layout with `SystemsLayoutMode.Automatic`).
66402
+ */
66403
+ totalContentWidth = 0;
66404
+ /**
66405
+ * Shortest note duration (in ticks) across every bar that has been added to this system, used
66406
+ * as the common reference in the Gourlay stretch formula so that rhythmically-equivalent beats
66407
+ * in different bars of the same system align column-wise.
66408
+ *
66409
+ * `-1` means "no bar added yet". The value only moves downward during system assembly; when a
66410
+ * new bar introduces a shorter minimum, {@link isMinDurationDirty} is set so that
66411
+ * {@link reconcileMinDurationIfDirty} can re-derive spring constants on the previously-added
66412
+ * bars before layout distribution runs.
66413
+ */
66414
+ minDuration = -1;
66415
+ /**
66416
+ * Set when a bar added to this system introduced a shorter {@link minDuration} than previously
66417
+ * seen, leaving earlier bars' spring constants stale. Consumed by
66418
+ * {@link reconcileMinDurationIfDirty} which is called from `VerticalLayoutBase._fitSystem`
66419
+ * once the system is fully assembled.
66420
+ */
66421
+ isMinDurationDirty = false;
66422
+ /**
66423
+ * Whether this system coordinates a shared minimum-duration reference across its bars for the
66424
+ * Gourlay stretch formula. Defaults to `true` for page-style and parchment layouts where bars
66425
+ * of a system fight for a common staff width. Set to `false` for horizontal layouts where each
66426
+ * bar is sized independently (by `bar.displayWidth` or its intrinsic width) and there is no
66427
+ * column-alignment concern - each bar keeps its local minimum so pre-existing rendering is
66428
+ * preserved.
66429
+ */
66430
+ shareMinDurationAcrossBars = true;
65982
66431
  isLast = false;
65983
66432
  masterBarsRenderers = [];
65984
66433
  staves = [];
@@ -66040,6 +66489,9 @@ class StaffSystem {
66040
66489
  }
66041
66490
  this.firstVisibleStaff = firstVisibleStaff;
66042
66491
  this._calculateAccoladeSpacing(tracks);
66492
+ // On the resize path the layoutingInfo was finalized in a previous layout pass, so we
66493
+ // only need to check whether its min-duration reference still matches the new system's.
66494
+ this._trackSystemMinDuration(renderers.layoutingInfo);
66043
66495
  this._applyLayoutAndUpdateWidth();
66044
66496
  return renderers;
66045
66497
  }
@@ -66095,10 +66547,89 @@ class StaffSystem {
66095
66547
  this.firstVisibleStaff = firstVisibleStaff;
66096
66548
  this._calculateAccoladeSpacing(tracks);
66097
66549
  barLayoutingInfo.finish();
66550
+ // Reconcile against the system-wide minimum-duration reference now that springs are
66551
+ // finalized. If this bar introduced a shorter note, earlier bars become stale (flagged
66552
+ // for bulk reconcile at fit time). If the system already had a shorter min than this
66553
+ // bar's local one, this bar's spring constants are recomputed immediately so the width
66554
+ // we return below reflects the shared reference.
66555
+ this._trackSystemMinDuration(barLayoutingInfo);
66098
66556
  // ensure same widths of new renderer
66099
66557
  result.width = this._applyLayoutAndUpdateWidth();
66100
66558
  return result;
66101
66559
  }
66560
+ /**
66561
+ * Updates {@link minDuration} and {@link isMinDurationDirty} when a bar is added, and brings
66562
+ * the just-added bar's {@link BarLayoutingInfo} in line with the current system minimum if the
66563
+ * system already saw a shorter reference. The bulk reconcile over previously-added bars is
66564
+ * deferred to {@link reconcileMinDurationIfDirty} (called from `_fitSystem`) to avoid
66565
+ * re-iterating the system every time a bar is appended.
66566
+ */
66567
+ _trackSystemMinDuration(info) {
66568
+ if (!this.shareMinDurationAcrossBars) {
66569
+ return;
66570
+ }
66571
+ const localMin = info.localMinDuration;
66572
+ if (this.minDuration === -1 || localMin < this.minDuration) {
66573
+ // this bar shortens the system minimum; earlier bars (if any) are now stale
66574
+ if (this.masterBarsRenderers.length > 1 && localMin !== this.minDuration) {
66575
+ this.isMinDurationDirty = true;
66576
+ }
66577
+ this.minDuration = localMin;
66578
+ }
66579
+ if (info.computedWithMinDuration > this.minDuration) {
66580
+ // this bar was initialized against a larger (local) min than the system carries; pull
66581
+ // it down to the system reference so its computedWidth reflects the shared spacing.
66582
+ info.recomputeSpringConstants(this.minDuration);
66583
+ }
66584
+ }
66585
+ /**
66586
+ * Re-derives spring constants on bars whose {@link BarLayoutingInfo.computedWithMinDuration}
66587
+ * is out of sync with the current {@link minDuration}, and rebuilds the cached system totals
66588
+ * (widths, {@link totalFixedOverhead}, {@link totalContentWidth}) from the refreshed bar
66589
+ * widths. Called from `VerticalLayoutBase._fitSystem` after the system is fully assembled and
66590
+ * before distribution runs. No-op when {@link isMinDurationDirty} is false.
66591
+ */
66592
+ reconcileMinDurationIfDirty() {
66593
+ if (!this.isMinDurationDirty) {
66594
+ return;
66595
+ }
66596
+ let systemWidth = this.accoladeWidth;
66597
+ let totalFixedOverhead = 0;
66598
+ let totalContentWidth = 0;
66599
+ for (const mb of this.masterBarsRenderers) {
66600
+ if (mb.layoutingInfo.computedWithMinDuration > this.minDuration) {
66601
+ mb.layoutingInfo.recomputeSpringConstants(this.minDuration);
66602
+ }
66603
+ let maxPrefix = 0;
66604
+ let maxContent = 0;
66605
+ let realWidth = 0;
66606
+ for (const r of mb.renderers) {
66607
+ r.applyLayoutingInfo();
66608
+ if (r.computedWidth > realWidth) {
66609
+ realWidth = r.computedWidth;
66610
+ }
66611
+ const overhead = r.fixedOverhead;
66612
+ if (overhead > maxPrefix) {
66613
+ maxPrefix = overhead;
66614
+ }
66615
+ const content = Math.max(0, r.computedWidth - overhead);
66616
+ if (content > maxContent) {
66617
+ maxContent = content;
66618
+ }
66619
+ }
66620
+ mb.maxFixedOverhead = maxPrefix;
66621
+ mb.maxContentWidth = maxContent;
66622
+ mb.width = realWidth;
66623
+ systemWidth += realWidth;
66624
+ totalFixedOverhead += maxPrefix;
66625
+ totalContentWidth += maxContent;
66626
+ }
66627
+ this.width = systemWidth;
66628
+ this.computedWidth = systemWidth;
66629
+ this.totalFixedOverhead = totalFixedOverhead;
66630
+ this.totalContentWidth = totalContentWidth;
66631
+ this.isMinDurationDirty = false;
66632
+ }
66102
66633
  getBarDisplayScale(renderer) {
66103
66634
  return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale;
66104
66635
  }
@@ -66137,12 +66668,16 @@ class StaffSystem {
66137
66668
  this.width -= width;
66138
66669
  this.computedWidth -= width;
66139
66670
  this.totalBarDisplayScale -= barDisplayScale;
66671
+ this.totalFixedOverhead -= toRemove.maxFixedOverhead;
66672
+ this.totalContentWidth -= toRemove.maxContentWidth;
66140
66673
  return toRemove;
66141
66674
  }
66142
66675
  return null;
66143
66676
  }
66144
66677
  _applyLayoutAndUpdateWidth() {
66145
66678
  let realWidth = 0;
66679
+ let maxFixedOverhead = 0;
66680
+ let maxContentWidth = 0;
66146
66681
  let barDisplayScale = 0;
66147
66682
  for (const s of this.allStaves) {
66148
66683
  const last = s.barRenderers[s.barRenderers.length - 1];
@@ -66151,8 +66686,21 @@ class StaffSystem {
66151
66686
  if (last.computedWidth > realWidth) {
66152
66687
  realWidth = last.computedWidth;
66153
66688
  }
66689
+ const overhead = last.fixedOverhead;
66690
+ if (overhead > maxFixedOverhead) {
66691
+ maxFixedOverhead = overhead;
66692
+ }
66693
+ const content = Math.max(0, last.computedWidth - overhead);
66694
+ if (content > maxContentWidth) {
66695
+ maxContentWidth = content;
66696
+ }
66154
66697
  }
66698
+ const renderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1];
66699
+ renderers.maxFixedOverhead = maxFixedOverhead;
66700
+ renderers.maxContentWidth = maxContentWidth;
66155
66701
  this.totalBarDisplayScale += barDisplayScale;
66702
+ this.totalFixedOverhead += maxFixedOverhead;
66703
+ this.totalContentWidth += maxContentWidth;
66156
66704
  this.width += realWidth;
66157
66705
  this.computedWidth += realWidth;
66158
66706
  return realWidth;
@@ -66650,17 +67198,6 @@ class StaffSystem {
66650
67198
  }
66651
67199
  }
66652
67200
 
66653
- /**
66654
- * @internal
66655
- */
66656
- class LazyPartial {
66657
- args;
66658
- renderCallback;
66659
- constructor(args, renderCallback) {
66660
- this.args = args;
66661
- this.renderCallback = renderCallback;
66662
- }
66663
- }
66664
67201
  /**
66665
67202
  * This is the base class for creating new layouting engines for the score renderer.
66666
67203
  * @internal
@@ -66691,15 +67228,21 @@ class ScoreLayout {
66691
67228
  this.doResize();
66692
67229
  }
66693
67230
  layoutAndRender(renderHints) {
66694
- this._lazyPartials.clear();
66695
67231
  this.slurRegistry.clear();
66696
- this.beamingRuleLookups.clear();
66697
- this._barRendererLookup.clear();
66698
- this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66699
67232
  const score = this.renderer.score;
66700
67233
  this.firstBarIndex = ModelUtils.computeFirstDisplayedBarIndex(score, this.renderer.settings);
66701
67234
  this.lastBarIndex = ModelUtils.computeLastDisplayedBarIndex(score, this.renderer.settings, this.firstBarIndex);
66702
67235
  this.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo(this.renderer.tracks, this.firstBarIndex, this.lastBarIndex);
67236
+ const firstChangedMasterBar = renderHints?.firstChangedMasterBar;
67237
+ if (firstChangedMasterBar !== undefined) {
67238
+ if (this.doUpdateForBars(renderHints)) {
67239
+ return;
67240
+ }
67241
+ }
67242
+ this._lazyPartials.clear();
67243
+ this.beamingRuleLookups.clear();
67244
+ this._barRendererLookup.clear();
67245
+ this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66703
67246
  this.pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale);
66704
67247
  if (!this.pagePadding) {
66705
67248
  this.pagePadding = [0, 0, 0, 0];
@@ -66714,6 +67257,9 @@ class ScoreLayout {
66714
67257
  this.doLayoutAndRender(renderHints);
66715
67258
  }
66716
67259
  _lazyPartials = new Map();
67260
+ getExistingPartialArgs(id) {
67261
+ return this._lazyPartials.has(id) ? this._lazyPartials.get(id).args : undefined;
67262
+ }
66717
67263
  registerPartial(args, callback) {
66718
67264
  if (args.height === 0) {
66719
67265
  return;
@@ -66732,7 +67278,11 @@ class ScoreLayout {
66732
67278
  }
66733
67279
  else {
66734
67280
  // in case of lazy loading -> first register lazy, then notify
66735
- this._lazyPartials.set(args.id, new LazyPartial(args, callback));
67281
+ const partial = {
67282
+ args,
67283
+ renderCallback: callback
67284
+ };
67285
+ this._lazyPartials.set(args.id, partial);
66736
67286
  this.renderer.partialLayoutFinished.trigger(args);
66737
67287
  }
66738
67288
  }
@@ -67027,7 +67577,7 @@ class ScoreLayout {
67027
67577
  glyph.textAlign = TextAlign.Left;
67028
67578
  }
67029
67579
  }
67030
- layoutAndRenderAnnotation(y) {
67580
+ _layoutAndRenderAnnotation(y) {
67031
67581
  // attention, you are not allowed to remove change this notice within any version of this library without permission!
67032
67582
  const msg = 'rendered by alphaTab';
67033
67583
  const resources = this.renderer.settings.display.resources;
@@ -67091,6 +67641,12 @@ class HorizontalScreenLayout extends ScoreLayout {
67091
67641
  }
67092
67642
  doResize() {
67093
67643
  }
67644
+ doUpdateForBars(_renderHints) {
67645
+ // not supported yet, modifications likely cause anyhow full updates
67646
+ // as we do not optimize effect bands yet. with effect bands being more
67647
+ // isolated in bars we could try updating dynamically
67648
+ return false;
67649
+ }
67094
67650
  doLayoutAndRender(renderHints) {
67095
67651
  const score = this.renderer.score;
67096
67652
  let startIndex = this.renderer.settings.display.startBar;
@@ -67104,6 +67660,11 @@ class HorizontalScreenLayout extends ScoreLayout {
67104
67660
  endBarIndex = startIndex + endBarIndex - 1; // map count to array index
67105
67661
  endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex));
67106
67662
  this._system = this.createEmptyStaffSystem(0);
67663
+ // Each bar in horizontal layout is sized independently (by bar.displayWidth or the bar's
67664
+ // intrinsic width), so there is no shared staff width to distribute across bars. Keep each
67665
+ // bar's spring constants referenced against its own local minimum-duration so rendering
67666
+ // matches the historical per-bar behaviour.
67667
+ this._system.shareMinDurationAcrossBars = false;
67107
67668
  this._system.isLast = true;
67108
67669
  this._system.x = this.pagePadding[0];
67109
67670
  this._system.y = this.pagePadding[1];
@@ -67165,7 +67726,7 @@ class HorizontalScreenLayout extends ScoreLayout {
67165
67726
  currentBarIndex += partial.masterBars.length;
67166
67727
  }
67167
67728
  this.height = this.layoutAndRenderBottomScoreInfo(this.height);
67168
- this.height = this.layoutAndRenderAnnotation(this.height);
67729
+ this.height = this._layoutAndRenderAnnotation(this.height);
67169
67730
  this.height += this.pagePadding[3];
67170
67731
  this.height *= this.renderer.settings.display.scale;
67171
67732
  }
@@ -67232,11 +67793,16 @@ class VerticalLayoutBase extends ScoreLayout {
67232
67793
  _allMasterBarRenderers = [];
67233
67794
  _barsFromPreviousSystem = [];
67234
67795
  _reuseViewPort = false;
67796
+ _preSystemPartialIds = [];
67797
+ _systemPartialIds = [];
67235
67798
  doLayoutAndRender(renderHints) {
67236
67799
  let y = this.pagePadding[1];
67237
67800
  this.width = this.renderer.width;
67238
67801
  this._allMasterBarRenderers = [];
67802
+ this._preSystemPartialIds = [];
67803
+ this._systemPartialIds = [];
67239
67804
  this._reuseViewPort = renderHints?.reuseViewport ?? false;
67805
+ this._systems = [];
67240
67806
  //
67241
67807
  // 1. Score Info
67242
67808
  y = this._layoutAndRenderScoreInfo(y, -1);
@@ -67248,15 +67814,23 @@ class VerticalLayoutBase extends ScoreLayout {
67248
67814
  y = this._layoutAndRenderChordDiagrams(y, -1);
67249
67815
  //
67250
67816
  // 4. One result per StaffSystem
67251
- y = this._layoutAndRenderScore(y);
67817
+ y = this._layoutAndRenderScore(y, this.firstBarIndex);
67252
67818
  y = this.layoutAndRenderBottomScoreInfo(y);
67253
- y = this.layoutAndRenderAnnotation(y);
67819
+ y = this._layoutAndRenderAnnotation(y);
67254
67820
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67255
67821
  }
67256
67822
  registerPartial(args, callback) {
67257
67823
  args.reuseViewport = this._reuseViewPort;
67258
67824
  super.registerPartial(args, callback);
67259
67825
  }
67826
+ reregisterPartial(id) {
67827
+ const args = this.getExistingPartialArgs(id);
67828
+ if (!args) {
67829
+ return;
67830
+ }
67831
+ args.reuseViewport = this._reuseViewPort;
67832
+ this.renderer.partialLayoutFinished.trigger(args);
67833
+ }
67260
67834
  get supportsResize() {
67261
67835
  return true;
67262
67836
  }
@@ -67267,6 +67841,47 @@ class VerticalLayoutBase extends ScoreLayout {
67267
67841
  }
67268
67842
  return x;
67269
67843
  }
67844
+ doUpdateForBars(renderHints) {
67845
+ this._reuseViewPort = renderHints.reuseViewport ?? false;
67846
+ const firstModifiedMasterBar = renderHints.firstChangedMasterBar;
67847
+ // first update existing systems as needed
67848
+ const systemIndex = this._systems.findIndex(s => {
67849
+ const first = s.masterBarsRenderers[0].masterBar.index;
67850
+ const last = s.masterBarsRenderers[s.masterBarsRenderers.length - 1].masterBar.index;
67851
+ return first <= firstModifiedMasterBar && firstModifiedMasterBar <= last;
67852
+ });
67853
+ if (systemIndex === -1 || !this.renderer.settings.core.enableLazyLoading) {
67854
+ return false;
67855
+ }
67856
+ // Bars from the start of the re-layouted system onward will be re-registered during the
67857
+ // paint pass. Clear their old entries from the preserved BoundsLookup so registration
67858
+ // produces a clean, complete lookup after this render finishes.
67859
+ const firstRebuiltBarIndex = this._systems[systemIndex].masterBarsRenderers[0].masterBar.index;
67860
+ this.renderer.boundsLookup.clearFromMasterBar(firstRebuiltBarIndex);
67861
+ // for now we do a full relayout from the first modified masterbar
67862
+ // there is a lot of room for even more performant updates, but they come
67863
+ // at a risk that features break.
67864
+ // e.g. we could only shift systems where the content didn't change,
67865
+ // but we might still have ties/slurs which have to be updated.
67866
+ const removeSystems = this._systems.splice(systemIndex, this._systems.length - systemIndex);
67867
+ this._systemPartialIds.splice(systemIndex, this._systemPartialIds.length - systemIndex);
67868
+ const system = removeSystems[0];
67869
+ let y = system.y;
67870
+ const firstBarIndex = system.masterBarsRenderers[0].masterBar.index;
67871
+ // signal all partials which didn't change
67872
+ for (const preSystemPartial of this._preSystemPartialIds) {
67873
+ this.reregisterPartial(preSystemPartial);
67874
+ }
67875
+ for (let i = 0; i < systemIndex; i++) {
67876
+ this.reregisterPartial(this._systemPartialIds[i]);
67877
+ }
67878
+ // new partials for all other prats
67879
+ y = this._layoutAndRenderScore(y, firstBarIndex);
67880
+ y = this.layoutAndRenderBottomScoreInfo(y);
67881
+ y = this._layoutAndRenderAnnotation(y);
67882
+ this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67883
+ return true;
67884
+ }
67270
67885
  doResize() {
67271
67886
  let y = this.pagePadding[1];
67272
67887
  this.width = this.renderer.width;
@@ -67285,7 +67900,7 @@ class VerticalLayoutBase extends ScoreLayout {
67285
67900
  // 4. One result per StaffSystem
67286
67901
  y = this._resizeAndRenderScore(y, oldHeight);
67287
67902
  y = this.layoutAndRenderBottomScoreInfo(y);
67288
- y = this.layoutAndRenderAnnotation(y);
67903
+ y = this._layoutAndRenderAnnotation(y);
67289
67904
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67290
67905
  }
67291
67906
  _layoutAndRenderTunings(y, totalHeight = -1) {
@@ -67309,6 +67924,7 @@ class VerticalLayoutBase extends ScoreLayout {
67309
67924
  canvas.textAlign = TextAlign.Center;
67310
67925
  this.tuningGlyph.paint(0, 0, canvas);
67311
67926
  });
67927
+ this._preSystemPartialIds.push(e.id);
67312
67928
  return y + tuningHeight;
67313
67929
  }
67314
67930
  _layoutAndRenderChordDiagrams(y, totalHeight = -1) {
@@ -67332,6 +67948,7 @@ class VerticalLayoutBase extends ScoreLayout {
67332
67948
  canvas.textAlign = TextAlign.Center;
67333
67949
  this.chordDiagrams.paint(0, 0, canvas);
67334
67950
  });
67951
+ this._preSystemPartialIds.push(e.id);
67335
67952
  return y + diagramHeight;
67336
67953
  }
67337
67954
  _layoutAndRenderScoreInfo(y, totalHeight = -1) {
@@ -67374,12 +67991,14 @@ class VerticalLayoutBase extends ScoreLayout {
67374
67991
  g.paint(0, 0, canvas);
67375
67992
  }
67376
67993
  });
67994
+ this._preSystemPartialIds.push(e.id);
67377
67995
  }
67378
67996
  return y + infoHeight;
67379
67997
  }
67380
67998
  _resizeAndRenderScore(y, oldHeight) {
67381
67999
  // if we have a fixed number of bars per row, we only need to refit them.
67382
68000
  const barsPerRowActive = this.getBarsPerSystem(0) > 0;
68001
+ this._systemPartialIds = [];
67383
68002
  if (barsPerRowActive) {
67384
68003
  for (let i = 0; i < this._systems.length; i++) {
67385
68004
  const system = this._systems[i];
@@ -67442,11 +68061,9 @@ class VerticalLayoutBase extends ScoreLayout {
67442
68061
  }
67443
68062
  return y;
67444
68063
  }
67445
- _layoutAndRenderScore(y) {
67446
- const startIndex = this.firstBarIndex;
68064
+ _layoutAndRenderScore(y, startIndex) {
67447
68065
  let currentBarIndex = startIndex;
67448
68066
  const endBarIndex = this.lastBarIndex;
67449
- this._systems = [];
67450
68067
  while (currentBarIndex <= endBarIndex) {
67451
68068
  // create system and align set proper coordinates
67452
68069
  const system = this._createStaffSystem(currentBarIndex, endBarIndex);
@@ -67481,6 +68098,7 @@ class VerticalLayoutBase extends ScoreLayout {
67481
68098
  // since we use partial drawing
67482
68099
  system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas);
67483
68100
  });
68101
+ this._systemPartialIds.push(args.id);
67484
68102
  // calculate coordinates for next system
67485
68103
  return height;
67486
68104
  }
@@ -67488,6 +68106,10 @@ class VerticalLayoutBase extends ScoreLayout {
67488
68106
  * Realignes the bars in this line according to the available space
67489
68107
  */
67490
68108
  _fitSystem(system) {
68109
+ // If a bar added late in the assembly introduced a shorter note than earlier bars, the
68110
+ // earlier bars' spring constants (and the cached system widths / totals) are stale.
68111
+ // Reconcile now - it's a no-op when nothing changed.
68112
+ system.reconcileMinDurationIfDirty();
67491
68113
  if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) {
67492
68114
  this._scaleToWidth(system, this._maxWidth);
67493
68115
  }
@@ -67499,29 +68121,35 @@ class VerticalLayoutBase extends ScoreLayout {
67499
68121
  _scaleToWidth(system, width) {
67500
68122
  const staffWidth = width - system.accoladeWidth;
67501
68123
  const shouldApplyBarScale = this.shouldApplyBarScale;
67502
- const totalScale = system.totalBarDisplayScale;
67503
- // NOTE: it currently delivers best results if we evenly distribute the available space across bars
67504
- // scaling bars relatively to their computed width, rather causes distortions whenever bars have
67505
- // pre-beat glyphs.
67506
- // most precise scaling would come if we use the contents (voiceContainerGlyph) width as a calculation
67507
- // factor. but this would make the calculation additionally complex with not much gain.
67508
- const difference = width - system.computedWidth;
67509
- const spacePerBar = difference / system.masterBarsRenderers.length;
68124
+ // Industry fixed-overhead model (Behind Bars, Dorico, Finale, Sibelius, MuseScore, Guitar Pro):
68125
+ // prefix/postfix glyphs (clef, key sig, time sig, barlines) are treated as fixed overhead and the
68126
+ // remaining staff width is distributed across bars by a per-bar weight.
68127
+ //
68128
+ // distributable = staffWidth - totalFixedOverhead
68129
+ // contentShare = distributable / sum(weight)
68130
+ // bar.width = bar.maxFixedOverhead + weight * contentShare
68131
+ //
68132
+ // The weight depends on the layout mode:
68133
+ // - shouldApplyBarScale=true -> weight = bar.displayScale (model-driven, matches Guitar Pro)
68134
+ // displayScale defaults to 1, so an unset value behaves identically
68135
+ // to an explicit 1 (GP omits the property when not customized).
68136
+ // - shouldApplyBarScale=false -> weight = natural content width (automatic, ignores displayScale)
68137
+ //
68138
+ // Per-bar maxFixedOverhead / maxContentWidth and the system-wide totals are maintained incrementally
68139
+ // in StaffSystem._applyLayoutAndUpdateWidth / revertLastBar so this pass can apply directly.
68140
+ const weightTotal = shouldApplyBarScale ? system.totalBarDisplayScale : system.totalContentWidth;
68141
+ const distributable = Math.max(0, staffWidth - system.totalFixedOverhead);
68142
+ const contentShare = weightTotal > 0 ? distributable / weightTotal : 0;
67510
68143
  for (const s of system.allStaves) {
67511
68144
  s.resetSharedLayoutData();
67512
- // scale the bars by keeping their respective ratio size
67513
68145
  let w = 0;
67514
- for (const renderer of s.barRenderers) {
68146
+ for (let i = 0; i < s.barRenderers.length; i++) {
68147
+ const renderer = s.barRenderers[i];
68148
+ const mb = system.masterBarsRenderers[i];
67515
68149
  renderer.x = w;
67516
68150
  renderer.y = s.topPadding + s.topOverflow;
67517
- let actualBarWidth;
67518
- if (shouldApplyBarScale) {
67519
- const barDisplayScale = system.getBarDisplayScale(renderer);
67520
- actualBarWidth = (barDisplayScale * staffWidth) / totalScale;
67521
- }
67522
- else {
67523
- actualBarWidth = renderer.computedWidth + spacePerBar;
67524
- }
68151
+ const weight = shouldApplyBarScale ? system.getBarDisplayScale(renderer) : mb.maxContentWidth;
68152
+ const actualBarWidth = mb.maxFixedOverhead + weight * contentShare;
67525
68153
  renderer.scaleToWidth(actualBarWidth);
67526
68154
  w += renderer.width;
67527
68155
  }
@@ -68545,11 +69173,43 @@ class TabTieGlyph extends NoteTieGlyph {
68545
69173
  }
68546
69174
  }
68547
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
+
68548
69207
  /**
68549
69208
  * @internal
68550
69209
  */
68551
69210
  class TabSlurGlyph extends TabTieGlyph {
68552
69211
  _forSlide;
69212
+ _labels = null;
68553
69213
  constructor(slurEffectId, startNote, endNote, forSlide, forEnd) {
68554
69214
  super(slurEffectId, startNote, endNote, forEnd);
68555
69215
  this._forSlide = forSlide;
@@ -68557,6 +69217,22 @@ class TabSlurGlyph extends TabTieGlyph {
68557
69217
  getTieHeight(startX, _startY, endX, _endY) {
68558
69218
  return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
68559
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
+ }
68560
69236
  tryExpand(startNote, endNote, forSlide, forEnd) {
68561
69237
  // same type required
68562
69238
  if (this._forSlide !== forSlide) {
@@ -68582,6 +69258,7 @@ class TabSlurGlyph extends TabTieGlyph {
68582
69258
  case BeamDirection.Up:
68583
69259
  if (startNote.realValue > this.startNote.realValue) {
68584
69260
  this.startNote = startNote;
69261
+ this._labels = null; // invalidate cache — labels live on startNote
68585
69262
  }
68586
69263
  if (endNote.realValue > this.endNote.realValue) {
68587
69264
  this.endNote = endNote;
@@ -68590,6 +69267,7 @@ class TabSlurGlyph extends TabTieGlyph {
68590
69267
  case BeamDirection.Down:
68591
69268
  if (startNote.realValue < this.startNote.realValue) {
68592
69269
  this.startNote = startNote;
69270
+ this._labels = null;
68593
69271
  }
68594
69272
  if (endNote.realValue < this.endNote.realValue) {
68595
69273
  this.endNote = endNote;
@@ -69345,12 +70023,24 @@ class LineBarRenderer extends BarRendererBase {
69345
70023
  const firstNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(firstNonRestBeat);
69346
70024
  const lastNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(lastNonRestBeat);
69347
70025
  const direction = this.getTupletBeamDirection(firstNonRestBeamingHelper);
69348
- let startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69349
- let endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
70026
+ let startY;
70027
+ let endY;
69350
70028
  if (isRestOnly) {
69351
- startY = Math.max(startY, endY);
70029
+ // rests have no stems, so anchor to the actual rest glyph bounds
70030
+ // instead of a stem-adjusted flag position (which would place the bracket
70031
+ // a full quarter-stem length away from the rests).
70032
+ if (direction === BeamDirection.Up) {
70033
+ startY = Math.min(this.getRestY(firstNonRestBeat, NoteYPosition.Top), this.getRestY(lastNonRestBeat, NoteYPosition.Top));
70034
+ }
70035
+ else {
70036
+ startY = Math.max(this.getRestY(firstNonRestBeat, NoteYPosition.Bottom), this.getRestY(lastNonRestBeat, NoteYPosition.Bottom));
70037
+ }
69352
70038
  endY = startY;
69353
70039
  }
70040
+ else {
70041
+ startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
70042
+ endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
70043
+ }
69354
70044
  // align line centered in available space
69355
70045
  if (direction === BeamDirection.Down) {
69356
70046
  startY += shift;
@@ -69720,7 +70410,30 @@ class LineBarRenderer extends BarRendererBase {
69720
70410
  let minNoteY = 0;
69721
70411
  for (const v of this.helpers.beamHelpers) {
69722
70412
  for (const h of v) {
69723
- if (!this.shouldPaintBeamingHelper(h)) ;
70413
+ if (!this.shouldPaintBeamingHelper(h)) {
70414
+ // beam is not drawn, but a rest-only tuplet still draws a bracket
70415
+ // anchored to the rest glyph bounds and needs overflow reserved.
70416
+ if (h.hasTuplet && h.isRestBeamHelper) {
70417
+ const tupletGroup = h.beats[0].tupletGroup;
70418
+ const tupletFirst = tupletGroup.beats[0];
70419
+ const tupletLast = tupletGroup.beats[tupletGroup.beats.length - 1];
70420
+ const tupletDirection = this.getTupletBeamDirection(h);
70421
+ if (tupletDirection === BeamDirection.Up) {
70422
+ const restTop = Math.min(this.getRestY(tupletFirst, NoteYPosition.Top), this.getRestY(tupletLast, NoteYPosition.Top));
70423
+ const topY = restTop - this.tupletSize - this.tupletOffset;
70424
+ if (topY < maxNoteY) {
70425
+ maxNoteY = topY;
70426
+ }
70427
+ }
70428
+ else {
70429
+ const restBottom = Math.max(this.getRestY(tupletFirst, NoteYPosition.Bottom), this.getRestY(tupletLast, NoteYPosition.Bottom));
70430
+ const bottomY = restBottom + this.tupletSize + this.tupletOffset;
70431
+ if (bottomY > minNoteY) {
70432
+ minNoteY = bottomY;
70433
+ }
70434
+ }
70435
+ }
70436
+ }
69724
70437
  else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) {
69725
70438
  const tupletDirection = this.getTupletBeamDirection(h);
69726
70439
  const direction = this.getBeamDirection(h);
@@ -73292,9 +74005,26 @@ class ScoreTieGlyph extends NoteTieGlyph {
73292
74005
  * @internal
73293
74006
  */
73294
74007
  class ScoreSlurGlyph extends ScoreTieGlyph {
74008
+ _labels = null;
73295
74009
  getTieHeight(startX, _startY, endX, _endY) {
73296
74010
  return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
73297
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
+ }
73298
74028
  calculateStartX() {
73299
74029
  return (this.renderer.x +
73300
74030
  (this._isStartCentered()