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

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.1768 (develop, build 1768)
2
+ * alphaTab v1.9.0-alpha.1785 (develop, build 1785)
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.1768';
213
- static date = '2026-04-10T03:35:57.433Z';
214
- static commit = '759bd788fc094ab5cb4db380f6a7657e459c14bf';
212
+ static version = '1.9.0-alpha.1785';
213
+ static date = '2026-04-27T03:55:02.662Z';
214
+ static commit = '760ed909a3d8dc36b159d23b4ff6780e95a3daf1';
215
215
  static print(print) {
216
216
  print(`alphaTab ${VersionInfo.version}`);
217
217
  print(`commit: ${VersionInfo.commit}`);
@@ -32634,9 +32634,16 @@
32634
32634
  *
32635
32635
  * The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display.
32636
32636
  *
32637
- * 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.
32638
- * 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,
32639
- * alphaTab will match this layout quite close.
32637
+ * In both modes, prefix and postfix glyphs (clef, key signature, time signature, barlines) are treated as fixed overhead: they keep their
32638
+ * natural size and the remaining staff width is distributed across bars by a per-bar weight. This matches the convention used by
32639
+ * Guitar Pro, Dorico, Finale, Sibelius and MuseScore. Bars that carry a system-start prefix or a mid-line clef/key/time-signature change
32640
+ * are therefore visibly wider than plain bars with the same weight. The weight source depends on the mode:
32641
+ *
32642
+ * * `Automatic` (default for `page` layout): weights come from the built-in spacing engine (the natural content width of each bar).
32643
+ * `displayScale` on the model is ignored.
32644
+ * * `UseModelLayout` (and the `parchment` layout): weights come from `bar.displayScale` / `masterBar.displayScale`. An unset
32645
+ * `displayScale` defaults to `1` and behaves identically to an explicit `1`, matching Guitar Pro (which omits the value when the
32646
+ * author hasn't customized it).
32640
32647
  *
32641
32648
  * ### Horizontal Layout
32642
32649
  *
@@ -38835,14 +38842,25 @@
38835
38842
  */
38836
38843
  boundsLookup;
38837
38844
  /**
38838
- * Finished the lookup for optimized access.
38845
+ * Whether this system's bounds have already been scaled via `finish`. Prevents double-scaling
38846
+ * when the parent `BoundsLookup` is preserved across partial renders and `finish` is invoked
38847
+ * again on a mix of already-scaled (preserved) and newly-registered (natural-coordinate) systems.
38848
+ */
38849
+ isFinished = false;
38850
+ /**
38851
+ * Finished the lookup for optimized access. Idempotent: once finished, further calls are no-ops
38852
+ * so preserved systems survive partial renders without being re-scaled.
38839
38853
  */
38840
38854
  finish(scale = 1) {
38855
+ if (this.isFinished) {
38856
+ return;
38857
+ }
38841
38858
  this.realBounds.scaleWith(scale);
38842
38859
  this.visualBounds.scaleWith(scale);
38843
38860
  for (const t of this.bars) {
38844
38861
  t.finish(scale);
38845
38862
  }
38863
+ this.isFinished = true;
38846
38864
  }
38847
38865
  /**
38848
38866
  * Adds a new master bar to this lookup.
@@ -39015,6 +39033,58 @@
39015
39033
  }
39016
39034
  this.isFinished = true;
39017
39035
  }
39036
+ /**
39037
+ * Re-opens the lookup for registrations without discarding previously registered bounds.
39038
+ * Used by the renderer when it preserves this lookup across a partial render so that new
39039
+ * bounds for the re-layouted range can be added while preserved systems stay intact.
39040
+ * @internal
39041
+ */
39042
+ resetForPartialUpdate() {
39043
+ this.isFinished = false;
39044
+ }
39045
+ /**
39046
+ * Removes all entries belonging to the given master bar index and any bars after it.
39047
+ * Used before a partial render re-registers bounds for the re-layouted range, so the
39048
+ * preserved lookup ends up with only the unchanged entries when registration begins.
39049
+ *
39050
+ * Assumes the layout aligns its re-layouted range to system boundaries - i.e. the first
39051
+ * system to clear starts exactly at `masterBarIndex`. Caller is responsible for passing
39052
+ * the first master-bar-index of the first re-layouted system.
39053
+ * @internal
39054
+ */
39055
+ clearFromMasterBar(masterBarIndex) {
39056
+ // drop staff systems whose bars start at or after the cleared range.
39057
+ let firstRemovedSystem = -1;
39058
+ for (let i = 0; i < this.staffSystems.length; i++) {
39059
+ const systemBars = this.staffSystems[i].bars;
39060
+ if (systemBars.length > 0 && systemBars[0].index >= masterBarIndex) {
39061
+ firstRemovedSystem = i;
39062
+ break;
39063
+ }
39064
+ }
39065
+ if (firstRemovedSystem !== -1) {
39066
+ this.staffSystems.splice(firstRemovedSystem, this.staffSystems.length - firstRemovedSystem);
39067
+ }
39068
+ // drop master bar entries at or beyond the cleared range.
39069
+ for (const key of Array.from(this._masterBarLookup.keys())) {
39070
+ if (key >= masterBarIndex) {
39071
+ this._masterBarLookup.delete(key);
39072
+ }
39073
+ }
39074
+ // drop beat entries whose beats belong to cleared bars.
39075
+ for (const key of Array.from(this._beatLookup.keys())) {
39076
+ const list = this._beatLookup.get(key);
39077
+ const filtered = list.filter(b => b.beat.voice.bar.index < masterBarIndex);
39078
+ if (filtered.length === 0) {
39079
+ this._beatLookup.delete(key);
39080
+ }
39081
+ else if (filtered.length !== list.length) {
39082
+ this._beatLookup.set(key, filtered);
39083
+ }
39084
+ }
39085
+ // drop the in-progress pointer - the next addStaffSystem call will replace it.
39086
+ this._currentStaffSystem = null;
39087
+ }
39018
39088
  /**
39019
39089
  * Adds a new staff sytem to the lookup.
39020
39090
  * @param bounds The staff system bounds to add.
@@ -46846,7 +46916,15 @@
46846
46916
  Logger.warning('Rendering', 'AlphaTab skipped rendering because of width=0 (element invisible)', null);
46847
46917
  return;
46848
46918
  }
46849
- this.boundsLookup = new BoundsLookup();
46919
+ // For partial renders we preserve the existing lookup so bars outside the re-layouted
46920
+ // range keep their already-scaled bounds - the layout will clear the changed range
46921
+ // before the paint pass re-registers fresh entries for it.
46922
+ if (renderHints?.firstChangedMasterBar !== undefined && this.boundsLookup) {
46923
+ this.boundsLookup.resetForPartialUpdate();
46924
+ }
46925
+ else {
46926
+ this.boundsLookup = new BoundsLookup();
46927
+ }
46850
46928
  this._recreateCanvas();
46851
46929
  this.canvas.lineWidth = 1;
46852
46930
  this.canvas.settings = this.settings;
@@ -48720,9 +48798,6 @@
48720
48798
  * @param beat The beat to add.
48721
48799
  */
48722
48800
  highlightBeat(beat, playbackStart) {
48723
- if (beat.isEmpty && !beat.voice.isEmpty) {
48724
- return;
48725
- }
48726
48801
  if (!this._highlightedBeats.has(beat.id)) {
48727
48802
  this._highlightedBeats.set(beat.id, true);
48728
48803
  this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart));
@@ -50190,7 +50265,13 @@
50190
50265
  let beatStart = beat.playbackStart;
50191
50266
  let audioDuration = beat.playbackDuration;
50192
50267
  const masterBarDuration = beat.voice.bar.masterBar.calculateDuration();
50193
- if (beat.voice.bar.isEmpty) {
50268
+ // For a bar whose voice contains a single empty beat (the typical "whole-bar rest"
50269
+ // placeholder inserted during score.finish), extend the beat's audio duration to cover
50270
+ // the full bar so cursor navigation has a beat to follow across the whole bar. Don't
50271
+ // apply this when the voice has multiple beats: those represent explicit rhythmic
50272
+ // subdivisions even when each beat is empty (e.g. a recording grid of placeholder
50273
+ // slots), and overriding would make every beat overlap the whole bar.
50274
+ if (beat.voice.bar.isEmpty && beat.voice.beats.length === 1) {
50194
50275
  audioDuration = masterBarDuration;
50195
50276
  }
50196
50277
  else if (beat.voice.bar.masterBar.tripletFeel !== TripletFeel.NoTripletFeel &&
@@ -54811,7 +54892,8 @@
54811
54892
  this._isInitialBeatCursorUpdate ||
54812
54893
  barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
54813
54894
  startBeatX < previousBeatBounds.onNotesX ||
54814
- barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
54895
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1 ||
54896
+ barBounds.h !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.h;
54815
54897
  if (jumpCursor) {
54816
54898
  cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
54817
54899
  }
@@ -58051,8 +58133,9 @@
58051
58133
  break;
58052
58134
  case 'alphaTab.renderScore':
58053
58135
  this._updateFontSizes(data.fontSizes);
58136
+ const renderHints = data.renderHints;
58054
58137
  const score = data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings);
58055
- this._renderMultiple(score, data.trackIndexes);
58138
+ this._renderMultiple(score, data.trackIndexes, renderHints);
58056
58139
  break;
58057
58140
  case 'alphaTab.updateSettings':
58058
58141
  this._updateSettings(data.settings);
@@ -62925,6 +63008,15 @@
62925
63008
  }
62926
63009
  return false;
62927
63010
  }
63011
+ /**
63012
+ * The fixed-overhead width of this renderer: glyphs that do not stretch when
63013
+ * the bar is scaled (clef, key signature, time signature, barlines, courtesy
63014
+ * accidentals, etc). Treated as a fixed allocation by the system-level layout
63015
+ * before distributing remaining width across bars by {@link Bar.displayScale}.
63016
+ */
63017
+ get fixedOverhead() {
63018
+ return this._preBeatGlyphs.width + this._postBeatGlyphs.width;
63019
+ }
62928
63020
  scaleToWidth(width) {
62929
63021
  // preBeat and postBeat glyphs do not get resized
62930
63022
  const containerWidth = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width;
@@ -65483,6 +65575,22 @@
65483
65575
  postBeatSize = 0;
65484
65576
  minStretchForce = 0;
65485
65577
  totalSpringConstant = 0;
65578
+ /**
65579
+ * The smallest note duration encountered within this bar's springs, used as the reference in
65580
+ * the Gourlay stretch formula. Read by the owning {@link StaffSystem} so that the system can
65581
+ * aggregate a shared minimum across all bars and trigger a reconcile if an added bar introduces
65582
+ * a shorter duration than previously seen.
65583
+ */
65584
+ get localMinDuration() {
65585
+ return this._minDuration;
65586
+ }
65587
+ /**
65588
+ * The minimum-duration reference against which the spring constants currently held by this info
65589
+ * were computed. Set by {@link finish} and {@link recomputeSpringConstants}. The owning
65590
+ * StaffSystem compares this against its system-wide minimum to decide whether spring constants
65591
+ * need re-derivation.
65592
+ */
65593
+ computedWithMinDuration = 0;
65486
65594
  _updateMinStretchForce(force) {
65487
65595
  if (this.minStretchForce < force) {
65488
65596
  this.minStretchForce = force;
@@ -65652,10 +65760,26 @@
65652
65760
  this._incompleteGraceRodsWidth += sp.preBeatWidth + sp.postSpringWidth;
65653
65761
  }
65654
65762
  }
65655
- this._calculateSpringConstants();
65763
+ this._calculateSpringConstants(this._minDuration);
65764
+ this.computedWithMinDuration = this._minDuration;
65656
65765
  this.version++;
65657
65766
  }
65658
- _calculateSpringConstants() {
65767
+ /**
65768
+ * Re-derives the spring constants (and {@link minStretchForce} / {@link totalSpringConstant})
65769
+ * using a caller-supplied minimum-duration reference rather than this bar's local minimum.
65770
+ *
65771
+ * Called by {@link StaffSystem.reconcileMinDurationIfDirty} when a bar added later to the
65772
+ * system introduced a shorter note than previously seen, invalidating this bar's spring
65773
+ * constants. Grace-rod data is not recomputed — it is independent of the minimum-duration
65774
+ * reference. The internal {@link version} is bumped so downstream consumers (e.g.
65775
+ * {@link BarRendererBase.applyLayoutingInfo}) pick up the refreshed positions.
65776
+ */
65777
+ recomputeSpringConstants(minDuration) {
65778
+ this._calculateSpringConstants(minDuration);
65779
+ this.computedWithMinDuration = minDuration;
65780
+ this.version++;
65781
+ }
65782
+ _calculateSpringConstants(minDuration) {
65659
65783
  let totalSpringConstant = 0;
65660
65784
  const sortedSprings = this._timeSortedSprings;
65661
65785
  if (sortedSprings.length === 0) {
@@ -65673,7 +65797,7 @@
65673
65797
  const nextSpring = sortedSprings[i + 1];
65674
65798
  duration = Math.abs(nextSpring.timePosition - currentSpring.timePosition);
65675
65799
  }
65676
- currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration);
65800
+ currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration, minDuration);
65677
65801
  totalSpringConstant += 1 / currentSpring.springConstant;
65678
65802
  }
65679
65803
  this.totalSpringConstant = 1 / totalSpringConstant;
@@ -65733,7 +65857,7 @@
65733
65857
  // springX += this.calculateWidth(force, spring.springConstant);
65734
65858
  // }
65735
65859
  // }
65736
- _calculateSpringConstant(spring, duration) {
65860
+ _calculateSpringConstant(spring, duration, minDuration) {
65737
65861
  if (duration <= 0) {
65738
65862
  duration = MidiUtils.toTicks(Duration.TwoHundredFiftySixth);
65739
65863
  }
@@ -65741,7 +65865,6 @@
65741
65865
  spring.smallestDuration = duration;
65742
65866
  }
65743
65867
  const smallestDuration = spring.smallestDuration;
65744
- const minDuration = this._minDuration;
65745
65868
  const minDurationWidth = BarLayoutingInfo._defaultMinDurationWidth;
65746
65869
  const phi = 1 + 0.85 * Math.log2(duration / minDuration);
65747
65870
  return (smallestDuration / duration) * (1 / (phi * minDurationWidth));
@@ -65804,6 +65927,18 @@
65804
65927
  canWrap = true;
65805
65928
  masterBar;
65806
65929
  additionalMultiBarRestIndexes = null;
65930
+ /**
65931
+ * Max fixed overhead (prefix + postfix glyph width) across all staves of this bar.
65932
+ * Used by the layout-mode horizontal scaling pass to carve out the fixed-overhead bucket
65933
+ * before distributing staff width across bars.
65934
+ */
65935
+ maxFixedOverhead = 0;
65936
+ /**
65937
+ * Max natural content width (computedWidth - fixedOverhead) across all staves of this bar.
65938
+ * Used as the bar weight when the layout ignores {@link MasterBar.displayScale} (e.g.
65939
+ * Page layout with `SystemsLayoutMode.Automatic`).
65940
+ */
65941
+ maxContentWidth = 0;
65807
65942
  get lastMasterBarIndex() {
65808
65943
  if (this.additionalMultiBarRestIndexes) {
65809
65944
  return this.additionalMultiBarRestIndexes[this.additionalMultiBarRestIndexes.length - 1];
@@ -65985,6 +66120,45 @@
65985
66120
  * This value is mainly used in the parchment style layout for correct scaling of the bars.
65986
66121
  */
65987
66122
  totalBarDisplayScale = 0;
66123
+ /**
66124
+ * Sum of per-bar {@link MasterBarsRenderers.maxFixedOverhead} across the system. The layout-mode
66125
+ * horizontal scaling pass subtracts this from the available staff width before distributing the
66126
+ * remainder across bars.
66127
+ */
66128
+ totalFixedOverhead = 0;
66129
+ /**
66130
+ * Sum of per-bar {@link MasterBarsRenderers.maxContentWidth} across the system. Used as the
66131
+ * denominator when distributing staff width in modes that weight bars by natural content width
66132
+ * (Page layout with `SystemsLayoutMode.Automatic`).
66133
+ */
66134
+ totalContentWidth = 0;
66135
+ /**
66136
+ * Shortest note duration (in ticks) across every bar that has been added to this system, used
66137
+ * as the common reference in the Gourlay stretch formula so that rhythmically-equivalent beats
66138
+ * in different bars of the same system align column-wise.
66139
+ *
66140
+ * `-1` means "no bar added yet". The value only moves downward during system assembly; when a
66141
+ * new bar introduces a shorter minimum, {@link isMinDurationDirty} is set so that
66142
+ * {@link reconcileMinDurationIfDirty} can re-derive spring constants on the previously-added
66143
+ * bars before layout distribution runs.
66144
+ */
66145
+ minDuration = -1;
66146
+ /**
66147
+ * Set when a bar added to this system introduced a shorter {@link minDuration} than previously
66148
+ * seen, leaving earlier bars' spring constants stale. Consumed by
66149
+ * {@link reconcileMinDurationIfDirty} which is called from `VerticalLayoutBase._fitSystem`
66150
+ * once the system is fully assembled.
66151
+ */
66152
+ isMinDurationDirty = false;
66153
+ /**
66154
+ * Whether this system coordinates a shared minimum-duration reference across its bars for the
66155
+ * Gourlay stretch formula. Defaults to `true` for page-style and parchment layouts where bars
66156
+ * of a system fight for a common staff width. Set to `false` for horizontal layouts where each
66157
+ * bar is sized independently (by `bar.displayWidth` or its intrinsic width) and there is no
66158
+ * column-alignment concern - each bar keeps its local minimum so pre-existing rendering is
66159
+ * preserved.
66160
+ */
66161
+ shareMinDurationAcrossBars = true;
65988
66162
  isLast = false;
65989
66163
  masterBarsRenderers = [];
65990
66164
  staves = [];
@@ -66046,6 +66220,9 @@
66046
66220
  }
66047
66221
  this.firstVisibleStaff = firstVisibleStaff;
66048
66222
  this._calculateAccoladeSpacing(tracks);
66223
+ // On the resize path the layoutingInfo was finalized in a previous layout pass, so we
66224
+ // only need to check whether its min-duration reference still matches the new system's.
66225
+ this._trackSystemMinDuration(renderers.layoutingInfo);
66049
66226
  this._applyLayoutAndUpdateWidth();
66050
66227
  return renderers;
66051
66228
  }
@@ -66101,10 +66278,89 @@
66101
66278
  this.firstVisibleStaff = firstVisibleStaff;
66102
66279
  this._calculateAccoladeSpacing(tracks);
66103
66280
  barLayoutingInfo.finish();
66281
+ // Reconcile against the system-wide minimum-duration reference now that springs are
66282
+ // finalized. If this bar introduced a shorter note, earlier bars become stale (flagged
66283
+ // for bulk reconcile at fit time). If the system already had a shorter min than this
66284
+ // bar's local one, this bar's spring constants are recomputed immediately so the width
66285
+ // we return below reflects the shared reference.
66286
+ this._trackSystemMinDuration(barLayoutingInfo);
66104
66287
  // ensure same widths of new renderer
66105
66288
  result.width = this._applyLayoutAndUpdateWidth();
66106
66289
  return result;
66107
66290
  }
66291
+ /**
66292
+ * Updates {@link minDuration} and {@link isMinDurationDirty} when a bar is added, and brings
66293
+ * the just-added bar's {@link BarLayoutingInfo} in line with the current system minimum if the
66294
+ * system already saw a shorter reference. The bulk reconcile over previously-added bars is
66295
+ * deferred to {@link reconcileMinDurationIfDirty} (called from `_fitSystem`) to avoid
66296
+ * re-iterating the system every time a bar is appended.
66297
+ */
66298
+ _trackSystemMinDuration(info) {
66299
+ if (!this.shareMinDurationAcrossBars) {
66300
+ return;
66301
+ }
66302
+ const localMin = info.localMinDuration;
66303
+ if (this.minDuration === -1 || localMin < this.minDuration) {
66304
+ // this bar shortens the system minimum; earlier bars (if any) are now stale
66305
+ if (this.masterBarsRenderers.length > 1 && localMin !== this.minDuration) {
66306
+ this.isMinDurationDirty = true;
66307
+ }
66308
+ this.minDuration = localMin;
66309
+ }
66310
+ if (info.computedWithMinDuration > this.minDuration) {
66311
+ // this bar was initialized against a larger (local) min than the system carries; pull
66312
+ // it down to the system reference so its computedWidth reflects the shared spacing.
66313
+ info.recomputeSpringConstants(this.minDuration);
66314
+ }
66315
+ }
66316
+ /**
66317
+ * Re-derives spring constants on bars whose {@link BarLayoutingInfo.computedWithMinDuration}
66318
+ * is out of sync with the current {@link minDuration}, and rebuilds the cached system totals
66319
+ * (widths, {@link totalFixedOverhead}, {@link totalContentWidth}) from the refreshed bar
66320
+ * widths. Called from `VerticalLayoutBase._fitSystem` after the system is fully assembled and
66321
+ * before distribution runs. No-op when {@link isMinDurationDirty} is false.
66322
+ */
66323
+ reconcileMinDurationIfDirty() {
66324
+ if (!this.isMinDurationDirty) {
66325
+ return;
66326
+ }
66327
+ let systemWidth = this.accoladeWidth;
66328
+ let totalFixedOverhead = 0;
66329
+ let totalContentWidth = 0;
66330
+ for (const mb of this.masterBarsRenderers) {
66331
+ if (mb.layoutingInfo.computedWithMinDuration > this.minDuration) {
66332
+ mb.layoutingInfo.recomputeSpringConstants(this.minDuration);
66333
+ }
66334
+ let maxPrefix = 0;
66335
+ let maxContent = 0;
66336
+ let realWidth = 0;
66337
+ for (const r of mb.renderers) {
66338
+ r.applyLayoutingInfo();
66339
+ if (r.computedWidth > realWidth) {
66340
+ realWidth = r.computedWidth;
66341
+ }
66342
+ const overhead = r.fixedOverhead;
66343
+ if (overhead > maxPrefix) {
66344
+ maxPrefix = overhead;
66345
+ }
66346
+ const content = Math.max(0, r.computedWidth - overhead);
66347
+ if (content > maxContent) {
66348
+ maxContent = content;
66349
+ }
66350
+ }
66351
+ mb.maxFixedOverhead = maxPrefix;
66352
+ mb.maxContentWidth = maxContent;
66353
+ mb.width = realWidth;
66354
+ systemWidth += realWidth;
66355
+ totalFixedOverhead += maxPrefix;
66356
+ totalContentWidth += maxContent;
66357
+ }
66358
+ this.width = systemWidth;
66359
+ this.computedWidth = systemWidth;
66360
+ this.totalFixedOverhead = totalFixedOverhead;
66361
+ this.totalContentWidth = totalContentWidth;
66362
+ this.isMinDurationDirty = false;
66363
+ }
66108
66364
  getBarDisplayScale(renderer) {
66109
66365
  return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale;
66110
66366
  }
@@ -66143,12 +66399,16 @@
66143
66399
  this.width -= width;
66144
66400
  this.computedWidth -= width;
66145
66401
  this.totalBarDisplayScale -= barDisplayScale;
66402
+ this.totalFixedOverhead -= toRemove.maxFixedOverhead;
66403
+ this.totalContentWidth -= toRemove.maxContentWidth;
66146
66404
  return toRemove;
66147
66405
  }
66148
66406
  return null;
66149
66407
  }
66150
66408
  _applyLayoutAndUpdateWidth() {
66151
66409
  let realWidth = 0;
66410
+ let maxFixedOverhead = 0;
66411
+ let maxContentWidth = 0;
66152
66412
  let barDisplayScale = 0;
66153
66413
  for (const s of this.allStaves) {
66154
66414
  const last = s.barRenderers[s.barRenderers.length - 1];
@@ -66157,8 +66417,21 @@
66157
66417
  if (last.computedWidth > realWidth) {
66158
66418
  realWidth = last.computedWidth;
66159
66419
  }
66420
+ const overhead = last.fixedOverhead;
66421
+ if (overhead > maxFixedOverhead) {
66422
+ maxFixedOverhead = overhead;
66423
+ }
66424
+ const content = Math.max(0, last.computedWidth - overhead);
66425
+ if (content > maxContentWidth) {
66426
+ maxContentWidth = content;
66427
+ }
66160
66428
  }
66429
+ const renderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1];
66430
+ renderers.maxFixedOverhead = maxFixedOverhead;
66431
+ renderers.maxContentWidth = maxContentWidth;
66161
66432
  this.totalBarDisplayScale += barDisplayScale;
66433
+ this.totalFixedOverhead += maxFixedOverhead;
66434
+ this.totalContentWidth += maxContentWidth;
66162
66435
  this.width += realWidth;
66163
66436
  this.computedWidth += realWidth;
66164
66437
  return realWidth;
@@ -66656,17 +66929,6 @@
66656
66929
  }
66657
66930
  }
66658
66931
 
66659
- /**
66660
- * @internal
66661
- */
66662
- class LazyPartial {
66663
- args;
66664
- renderCallback;
66665
- constructor(args, renderCallback) {
66666
- this.args = args;
66667
- this.renderCallback = renderCallback;
66668
- }
66669
- }
66670
66932
  /**
66671
66933
  * This is the base class for creating new layouting engines for the score renderer.
66672
66934
  * @internal
@@ -66697,15 +66959,21 @@
66697
66959
  this.doResize();
66698
66960
  }
66699
66961
  layoutAndRender(renderHints) {
66700
- this._lazyPartials.clear();
66701
66962
  this.slurRegistry.clear();
66702
- this.beamingRuleLookups.clear();
66703
- this._barRendererLookup.clear();
66704
- this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66705
66963
  const score = this.renderer.score;
66706
66964
  this.firstBarIndex = ModelUtils.computeFirstDisplayedBarIndex(score, this.renderer.settings);
66707
66965
  this.lastBarIndex = ModelUtils.computeLastDisplayedBarIndex(score, this.renderer.settings, this.firstBarIndex);
66708
66966
  this.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo(this.renderer.tracks, this.firstBarIndex, this.lastBarIndex);
66967
+ const firstChangedMasterBar = renderHints?.firstChangedMasterBar;
66968
+ if (firstChangedMasterBar !== undefined) {
66969
+ if (this.doUpdateForBars(renderHints)) {
66970
+ return;
66971
+ }
66972
+ }
66973
+ this._lazyPartials.clear();
66974
+ this.beamingRuleLookups.clear();
66975
+ this._barRendererLookup.clear();
66976
+ this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66709
66977
  this.pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale);
66710
66978
  if (!this.pagePadding) {
66711
66979
  this.pagePadding = [0, 0, 0, 0];
@@ -66720,6 +66988,9 @@
66720
66988
  this.doLayoutAndRender(renderHints);
66721
66989
  }
66722
66990
  _lazyPartials = new Map();
66991
+ getExistingPartialArgs(id) {
66992
+ return this._lazyPartials.has(id) ? this._lazyPartials.get(id).args : undefined;
66993
+ }
66723
66994
  registerPartial(args, callback) {
66724
66995
  if (args.height === 0) {
66725
66996
  return;
@@ -66738,7 +67009,11 @@
66738
67009
  }
66739
67010
  else {
66740
67011
  // in case of lazy loading -> first register lazy, then notify
66741
- this._lazyPartials.set(args.id, new LazyPartial(args, callback));
67012
+ const partial = {
67013
+ args,
67014
+ renderCallback: callback
67015
+ };
67016
+ this._lazyPartials.set(args.id, partial);
66742
67017
  this.renderer.partialLayoutFinished.trigger(args);
66743
67018
  }
66744
67019
  }
@@ -67033,7 +67308,7 @@
67033
67308
  glyph.textAlign = TextAlign.Left;
67034
67309
  }
67035
67310
  }
67036
- layoutAndRenderAnnotation(y) {
67311
+ _layoutAndRenderAnnotation(y) {
67037
67312
  // attention, you are not allowed to remove change this notice within any version of this library without permission!
67038
67313
  const msg = 'rendered by alphaTab';
67039
67314
  const resources = this.renderer.settings.display.resources;
@@ -67097,6 +67372,12 @@
67097
67372
  }
67098
67373
  doResize() {
67099
67374
  }
67375
+ doUpdateForBars(_renderHints) {
67376
+ // not supported yet, modifications likely cause anyhow full updates
67377
+ // as we do not optimize effect bands yet. with effect bands being more
67378
+ // isolated in bars we could try updating dynamically
67379
+ return false;
67380
+ }
67100
67381
  doLayoutAndRender(renderHints) {
67101
67382
  const score = this.renderer.score;
67102
67383
  let startIndex = this.renderer.settings.display.startBar;
@@ -67110,6 +67391,11 @@
67110
67391
  endBarIndex = startIndex + endBarIndex - 1; // map count to array index
67111
67392
  endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex));
67112
67393
  this._system = this.createEmptyStaffSystem(0);
67394
+ // Each bar in horizontal layout is sized independently (by bar.displayWidth or the bar's
67395
+ // intrinsic width), so there is no shared staff width to distribute across bars. Keep each
67396
+ // bar's spring constants referenced against its own local minimum-duration so rendering
67397
+ // matches the historical per-bar behaviour.
67398
+ this._system.shareMinDurationAcrossBars = false;
67113
67399
  this._system.isLast = true;
67114
67400
  this._system.x = this.pagePadding[0];
67115
67401
  this._system.y = this.pagePadding[1];
@@ -67171,7 +67457,7 @@
67171
67457
  currentBarIndex += partial.masterBars.length;
67172
67458
  }
67173
67459
  this.height = this.layoutAndRenderBottomScoreInfo(this.height);
67174
- this.height = this.layoutAndRenderAnnotation(this.height);
67460
+ this.height = this._layoutAndRenderAnnotation(this.height);
67175
67461
  this.height += this.pagePadding[3];
67176
67462
  this.height *= this.renderer.settings.display.scale;
67177
67463
  }
@@ -67238,11 +67524,16 @@
67238
67524
  _allMasterBarRenderers = [];
67239
67525
  _barsFromPreviousSystem = [];
67240
67526
  _reuseViewPort = false;
67527
+ _preSystemPartialIds = [];
67528
+ _systemPartialIds = [];
67241
67529
  doLayoutAndRender(renderHints) {
67242
67530
  let y = this.pagePadding[1];
67243
67531
  this.width = this.renderer.width;
67244
67532
  this._allMasterBarRenderers = [];
67533
+ this._preSystemPartialIds = [];
67534
+ this._systemPartialIds = [];
67245
67535
  this._reuseViewPort = renderHints?.reuseViewport ?? false;
67536
+ this._systems = [];
67246
67537
  //
67247
67538
  // 1. Score Info
67248
67539
  y = this._layoutAndRenderScoreInfo(y, -1);
@@ -67254,15 +67545,23 @@
67254
67545
  y = this._layoutAndRenderChordDiagrams(y, -1);
67255
67546
  //
67256
67547
  // 4. One result per StaffSystem
67257
- y = this._layoutAndRenderScore(y);
67548
+ y = this._layoutAndRenderScore(y, this.firstBarIndex);
67258
67549
  y = this.layoutAndRenderBottomScoreInfo(y);
67259
- y = this.layoutAndRenderAnnotation(y);
67550
+ y = this._layoutAndRenderAnnotation(y);
67260
67551
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67261
67552
  }
67262
67553
  registerPartial(args, callback) {
67263
67554
  args.reuseViewport = this._reuseViewPort;
67264
67555
  super.registerPartial(args, callback);
67265
67556
  }
67557
+ reregisterPartial(id) {
67558
+ const args = this.getExistingPartialArgs(id);
67559
+ if (!args) {
67560
+ return;
67561
+ }
67562
+ args.reuseViewport = this._reuseViewPort;
67563
+ this.renderer.partialLayoutFinished.trigger(args);
67564
+ }
67266
67565
  get supportsResize() {
67267
67566
  return true;
67268
67567
  }
@@ -67273,6 +67572,47 @@
67273
67572
  }
67274
67573
  return x;
67275
67574
  }
67575
+ doUpdateForBars(renderHints) {
67576
+ this._reuseViewPort = renderHints.reuseViewport ?? false;
67577
+ const firstModifiedMasterBar = renderHints.firstChangedMasterBar;
67578
+ // first update existing systems as needed
67579
+ const systemIndex = this._systems.findIndex(s => {
67580
+ const first = s.masterBarsRenderers[0].masterBar.index;
67581
+ const last = s.masterBarsRenderers[s.masterBarsRenderers.length - 1].masterBar.index;
67582
+ return first <= firstModifiedMasterBar && firstModifiedMasterBar <= last;
67583
+ });
67584
+ if (systemIndex === -1 || !this.renderer.settings.core.enableLazyLoading) {
67585
+ return false;
67586
+ }
67587
+ // Bars from the start of the re-layouted system onward will be re-registered during the
67588
+ // paint pass. Clear their old entries from the preserved BoundsLookup so registration
67589
+ // produces a clean, complete lookup after this render finishes.
67590
+ const firstRebuiltBarIndex = this._systems[systemIndex].masterBarsRenderers[0].masterBar.index;
67591
+ this.renderer.boundsLookup.clearFromMasterBar(firstRebuiltBarIndex);
67592
+ // for now we do a full relayout from the first modified masterbar
67593
+ // there is a lot of room for even more performant updates, but they come
67594
+ // at a risk that features break.
67595
+ // e.g. we could only shift systems where the content didn't change,
67596
+ // but we might still have ties/slurs which have to be updated.
67597
+ const removeSystems = this._systems.splice(systemIndex, this._systems.length - systemIndex);
67598
+ this._systemPartialIds.splice(systemIndex, this._systemPartialIds.length - systemIndex);
67599
+ const system = removeSystems[0];
67600
+ let y = system.y;
67601
+ const firstBarIndex = system.masterBarsRenderers[0].masterBar.index;
67602
+ // signal all partials which didn't change
67603
+ for (const preSystemPartial of this._preSystemPartialIds) {
67604
+ this.reregisterPartial(preSystemPartial);
67605
+ }
67606
+ for (let i = 0; i < systemIndex; i++) {
67607
+ this.reregisterPartial(this._systemPartialIds[i]);
67608
+ }
67609
+ // new partials for all other prats
67610
+ y = this._layoutAndRenderScore(y, firstBarIndex);
67611
+ y = this.layoutAndRenderBottomScoreInfo(y);
67612
+ y = this._layoutAndRenderAnnotation(y);
67613
+ this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67614
+ return true;
67615
+ }
67276
67616
  doResize() {
67277
67617
  let y = this.pagePadding[1];
67278
67618
  this.width = this.renderer.width;
@@ -67291,7 +67631,7 @@
67291
67631
  // 4. One result per StaffSystem
67292
67632
  y = this._resizeAndRenderScore(y, oldHeight);
67293
67633
  y = this.layoutAndRenderBottomScoreInfo(y);
67294
- y = this.layoutAndRenderAnnotation(y);
67634
+ y = this._layoutAndRenderAnnotation(y);
67295
67635
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67296
67636
  }
67297
67637
  _layoutAndRenderTunings(y, totalHeight = -1) {
@@ -67315,6 +67655,7 @@
67315
67655
  canvas.textAlign = TextAlign.Center;
67316
67656
  this.tuningGlyph.paint(0, 0, canvas);
67317
67657
  });
67658
+ this._preSystemPartialIds.push(e.id);
67318
67659
  return y + tuningHeight;
67319
67660
  }
67320
67661
  _layoutAndRenderChordDiagrams(y, totalHeight = -1) {
@@ -67338,6 +67679,7 @@
67338
67679
  canvas.textAlign = TextAlign.Center;
67339
67680
  this.chordDiagrams.paint(0, 0, canvas);
67340
67681
  });
67682
+ this._preSystemPartialIds.push(e.id);
67341
67683
  return y + diagramHeight;
67342
67684
  }
67343
67685
  _layoutAndRenderScoreInfo(y, totalHeight = -1) {
@@ -67380,12 +67722,14 @@
67380
67722
  g.paint(0, 0, canvas);
67381
67723
  }
67382
67724
  });
67725
+ this._preSystemPartialIds.push(e.id);
67383
67726
  }
67384
67727
  return y + infoHeight;
67385
67728
  }
67386
67729
  _resizeAndRenderScore(y, oldHeight) {
67387
67730
  // if we have a fixed number of bars per row, we only need to refit them.
67388
67731
  const barsPerRowActive = this.getBarsPerSystem(0) > 0;
67732
+ this._systemPartialIds = [];
67389
67733
  if (barsPerRowActive) {
67390
67734
  for (let i = 0; i < this._systems.length; i++) {
67391
67735
  const system = this._systems[i];
@@ -67448,11 +67792,9 @@
67448
67792
  }
67449
67793
  return y;
67450
67794
  }
67451
- _layoutAndRenderScore(y) {
67452
- const startIndex = this.firstBarIndex;
67795
+ _layoutAndRenderScore(y, startIndex) {
67453
67796
  let currentBarIndex = startIndex;
67454
67797
  const endBarIndex = this.lastBarIndex;
67455
- this._systems = [];
67456
67798
  while (currentBarIndex <= endBarIndex) {
67457
67799
  // create system and align set proper coordinates
67458
67800
  const system = this._createStaffSystem(currentBarIndex, endBarIndex);
@@ -67487,6 +67829,7 @@
67487
67829
  // since we use partial drawing
67488
67830
  system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas);
67489
67831
  });
67832
+ this._systemPartialIds.push(args.id);
67490
67833
  // calculate coordinates for next system
67491
67834
  return height;
67492
67835
  }
@@ -67494,6 +67837,10 @@
67494
67837
  * Realignes the bars in this line according to the available space
67495
67838
  */
67496
67839
  _fitSystem(system) {
67840
+ // If a bar added late in the assembly introduced a shorter note than earlier bars, the
67841
+ // earlier bars' spring constants (and the cached system widths / totals) are stale.
67842
+ // Reconcile now - it's a no-op when nothing changed.
67843
+ system.reconcileMinDurationIfDirty();
67497
67844
  if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) {
67498
67845
  this._scaleToWidth(system, this._maxWidth);
67499
67846
  }
@@ -67505,29 +67852,35 @@
67505
67852
  _scaleToWidth(system, width) {
67506
67853
  const staffWidth = width - system.accoladeWidth;
67507
67854
  const shouldApplyBarScale = this.shouldApplyBarScale;
67508
- const totalScale = system.totalBarDisplayScale;
67509
- // NOTE: it currently delivers best results if we evenly distribute the available space across bars
67510
- // scaling bars relatively to their computed width, rather causes distortions whenever bars have
67511
- // pre-beat glyphs.
67512
- // most precise scaling would come if we use the contents (voiceContainerGlyph) width as a calculation
67513
- // factor. but this would make the calculation additionally complex with not much gain.
67514
- const difference = width - system.computedWidth;
67515
- const spacePerBar = difference / system.masterBarsRenderers.length;
67855
+ // Industry fixed-overhead model (Behind Bars, Dorico, Finale, Sibelius, MuseScore, Guitar Pro):
67856
+ // prefix/postfix glyphs (clef, key sig, time sig, barlines) are treated as fixed overhead and the
67857
+ // remaining staff width is distributed across bars by a per-bar weight.
67858
+ //
67859
+ // distributable = staffWidth - totalFixedOverhead
67860
+ // contentShare = distributable / sum(weight)
67861
+ // bar.width = bar.maxFixedOverhead + weight * contentShare
67862
+ //
67863
+ // The weight depends on the layout mode:
67864
+ // - shouldApplyBarScale=true -> weight = bar.displayScale (model-driven, matches Guitar Pro)
67865
+ // displayScale defaults to 1, so an unset value behaves identically
67866
+ // to an explicit 1 (GP omits the property when not customized).
67867
+ // - shouldApplyBarScale=false -> weight = natural content width (automatic, ignores displayScale)
67868
+ //
67869
+ // Per-bar maxFixedOverhead / maxContentWidth and the system-wide totals are maintained incrementally
67870
+ // in StaffSystem._applyLayoutAndUpdateWidth / revertLastBar so this pass can apply directly.
67871
+ const weightTotal = shouldApplyBarScale ? system.totalBarDisplayScale : system.totalContentWidth;
67872
+ const distributable = Math.max(0, staffWidth - system.totalFixedOverhead);
67873
+ const contentShare = weightTotal > 0 ? distributable / weightTotal : 0;
67516
67874
  for (const s of system.allStaves) {
67517
67875
  s.resetSharedLayoutData();
67518
- // scale the bars by keeping their respective ratio size
67519
67876
  let w = 0;
67520
- for (const renderer of s.barRenderers) {
67877
+ for (let i = 0; i < s.barRenderers.length; i++) {
67878
+ const renderer = s.barRenderers[i];
67879
+ const mb = system.masterBarsRenderers[i];
67521
67880
  renderer.x = w;
67522
67881
  renderer.y = s.topPadding + s.topOverflow;
67523
- let actualBarWidth;
67524
- if (shouldApplyBarScale) {
67525
- const barDisplayScale = system.getBarDisplayScale(renderer);
67526
- actualBarWidth = (barDisplayScale * staffWidth) / totalScale;
67527
- }
67528
- else {
67529
- actualBarWidth = renderer.computedWidth + spacePerBar;
67530
- }
67882
+ const weight = shouldApplyBarScale ? system.getBarDisplayScale(renderer) : mb.maxContentWidth;
67883
+ const actualBarWidth = mb.maxFixedOverhead + weight * contentShare;
67531
67884
  renderer.scaleToWidth(actualBarWidth);
67532
67885
  w += renderer.width;
67533
67886
  }
@@ -69351,12 +69704,24 @@
69351
69704
  const firstNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(firstNonRestBeat);
69352
69705
  const lastNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(lastNonRestBeat);
69353
69706
  const direction = this.getTupletBeamDirection(firstNonRestBeamingHelper);
69354
- let startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69355
- let endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
69707
+ let startY;
69708
+ let endY;
69356
69709
  if (isRestOnly) {
69357
- startY = Math.max(startY, endY);
69710
+ // rests have no stems, so anchor to the actual rest glyph bounds
69711
+ // instead of a stem-adjusted flag position (which would place the bracket
69712
+ // a full quarter-stem length away from the rests).
69713
+ if (direction === BeamDirection.Up) {
69714
+ startY = Math.min(this.getRestY(firstNonRestBeat, NoteYPosition.Top), this.getRestY(lastNonRestBeat, NoteYPosition.Top));
69715
+ }
69716
+ else {
69717
+ startY = Math.max(this.getRestY(firstNonRestBeat, NoteYPosition.Bottom), this.getRestY(lastNonRestBeat, NoteYPosition.Bottom));
69718
+ }
69358
69719
  endY = startY;
69359
69720
  }
69721
+ else {
69722
+ startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69723
+ endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
69724
+ }
69360
69725
  // align line centered in available space
69361
69726
  if (direction === BeamDirection.Down) {
69362
69727
  startY += shift;
@@ -69726,7 +70091,30 @@
69726
70091
  let minNoteY = 0;
69727
70092
  for (const v of this.helpers.beamHelpers) {
69728
70093
  for (const h of v) {
69729
- if (!this.shouldPaintBeamingHelper(h)) ;
70094
+ if (!this.shouldPaintBeamingHelper(h)) {
70095
+ // beam is not drawn, but a rest-only tuplet still draws a bracket
70096
+ // anchored to the rest glyph bounds and needs overflow reserved.
70097
+ if (h.hasTuplet && h.isRestBeamHelper) {
70098
+ const tupletGroup = h.beats[0].tupletGroup;
70099
+ const tupletFirst = tupletGroup.beats[0];
70100
+ const tupletLast = tupletGroup.beats[tupletGroup.beats.length - 1];
70101
+ const tupletDirection = this.getTupletBeamDirection(h);
70102
+ if (tupletDirection === BeamDirection.Up) {
70103
+ const restTop = Math.min(this.getRestY(tupletFirst, NoteYPosition.Top), this.getRestY(tupletLast, NoteYPosition.Top));
70104
+ const topY = restTop - this.tupletSize - this.tupletOffset;
70105
+ if (topY < maxNoteY) {
70106
+ maxNoteY = topY;
70107
+ }
70108
+ }
70109
+ else {
70110
+ const restBottom = Math.max(this.getRestY(tupletFirst, NoteYPosition.Bottom), this.getRestY(tupletLast, NoteYPosition.Bottom));
70111
+ const bottomY = restBottom + this.tupletSize + this.tupletOffset;
70112
+ if (bottomY > minNoteY) {
70113
+ minNoteY = bottomY;
70114
+ }
70115
+ }
70116
+ }
70117
+ }
69730
70118
  else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) {
69731
70119
  const tupletDirection = this.getTupletBeamDirection(h);
69732
70120
  const direction = this.getBeamDirection(h);