@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.
@@ -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
  *
@@ -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.1785';
207
+ static date = '2026-04-27T03:55:02.662Z';
208
+ static commit = '760ed909a3d8dc36b159d23b4ff6780e95a3daf1';
209
209
  static print(print) {
210
210
  print(`alphaTab ${VersionInfo.version}`);
211
211
  print(`commit: ${VersionInfo.commit}`);
@@ -32628,9 +32628,16 @@ class DisplaySettings {
32628
32628
  *
32629
32629
  * The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display.
32630
32630
  *
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.
32631
+ * In both modes, prefix and postfix glyphs (clef, key signature, time signature, barlines) are treated as fixed overhead: they keep their
32632
+ * natural size and the remaining staff width is distributed across bars by a per-bar weight. This matches the convention used by
32633
+ * Guitar Pro, Dorico, Finale, Sibelius and MuseScore. Bars that carry a system-start prefix or a mid-line clef/key/time-signature change
32634
+ * are therefore visibly wider than plain bars with the same weight. The weight source depends on the mode:
32635
+ *
32636
+ * * `Automatic` (default for `page` layout): weights come from the built-in spacing engine (the natural content width of each bar).
32637
+ * `displayScale` on the model is ignored.
32638
+ * * `UseModelLayout` (and the `parchment` layout): weights come from `bar.displayScale` / `masterBar.displayScale`. An unset
32639
+ * `displayScale` defaults to `1` and behaves identically to an explicit `1`, matching Guitar Pro (which omits the value when the
32640
+ * author hasn't customized it).
32634
32641
  *
32635
32642
  * ### Horizontal Layout
32636
32643
  *
@@ -38829,14 +38836,25 @@ class StaffSystemBounds {
38829
38836
  */
38830
38837
  boundsLookup;
38831
38838
  /**
38832
- * Finished the lookup for optimized access.
38839
+ * Whether this system's bounds have already been scaled via `finish`. Prevents double-scaling
38840
+ * when the parent `BoundsLookup` is preserved across partial renders and `finish` is invoked
38841
+ * again on a mix of already-scaled (preserved) and newly-registered (natural-coordinate) systems.
38842
+ */
38843
+ isFinished = false;
38844
+ /**
38845
+ * Finished the lookup for optimized access. Idempotent: once finished, further calls are no-ops
38846
+ * so preserved systems survive partial renders without being re-scaled.
38833
38847
  */
38834
38848
  finish(scale = 1) {
38849
+ if (this.isFinished) {
38850
+ return;
38851
+ }
38835
38852
  this.realBounds.scaleWith(scale);
38836
38853
  this.visualBounds.scaleWith(scale);
38837
38854
  for (const t of this.bars) {
38838
38855
  t.finish(scale);
38839
38856
  }
38857
+ this.isFinished = true;
38840
38858
  }
38841
38859
  /**
38842
38860
  * Adds a new master bar to this lookup.
@@ -39009,6 +39027,58 @@ class BoundsLookup {
39009
39027
  }
39010
39028
  this.isFinished = true;
39011
39029
  }
39030
+ /**
39031
+ * Re-opens the lookup for registrations without discarding previously registered bounds.
39032
+ * Used by the renderer when it preserves this lookup across a partial render so that new
39033
+ * bounds for the re-layouted range can be added while preserved systems stay intact.
39034
+ * @internal
39035
+ */
39036
+ resetForPartialUpdate() {
39037
+ this.isFinished = false;
39038
+ }
39039
+ /**
39040
+ * Removes all entries belonging to the given master bar index and any bars after it.
39041
+ * Used before a partial render re-registers bounds for the re-layouted range, so the
39042
+ * preserved lookup ends up with only the unchanged entries when registration begins.
39043
+ *
39044
+ * Assumes the layout aligns its re-layouted range to system boundaries - i.e. the first
39045
+ * system to clear starts exactly at `masterBarIndex`. Caller is responsible for passing
39046
+ * the first master-bar-index of the first re-layouted system.
39047
+ * @internal
39048
+ */
39049
+ clearFromMasterBar(masterBarIndex) {
39050
+ // drop staff systems whose bars start at or after the cleared range.
39051
+ let firstRemovedSystem = -1;
39052
+ for (let i = 0; i < this.staffSystems.length; i++) {
39053
+ const systemBars = this.staffSystems[i].bars;
39054
+ if (systemBars.length > 0 && systemBars[0].index >= masterBarIndex) {
39055
+ firstRemovedSystem = i;
39056
+ break;
39057
+ }
39058
+ }
39059
+ if (firstRemovedSystem !== -1) {
39060
+ this.staffSystems.splice(firstRemovedSystem, this.staffSystems.length - firstRemovedSystem);
39061
+ }
39062
+ // drop master bar entries at or beyond the cleared range.
39063
+ for (const key of Array.from(this._masterBarLookup.keys())) {
39064
+ if (key >= masterBarIndex) {
39065
+ this._masterBarLookup.delete(key);
39066
+ }
39067
+ }
39068
+ // drop beat entries whose beats belong to cleared bars.
39069
+ for (const key of Array.from(this._beatLookup.keys())) {
39070
+ const list = this._beatLookup.get(key);
39071
+ const filtered = list.filter(b => b.beat.voice.bar.index < masterBarIndex);
39072
+ if (filtered.length === 0) {
39073
+ this._beatLookup.delete(key);
39074
+ }
39075
+ else if (filtered.length !== list.length) {
39076
+ this._beatLookup.set(key, filtered);
39077
+ }
39078
+ }
39079
+ // drop the in-progress pointer - the next addStaffSystem call will replace it.
39080
+ this._currentStaffSystem = null;
39081
+ }
39012
39082
  /**
39013
39083
  * Adds a new staff sytem to the lookup.
39014
39084
  * @param bounds The staff system bounds to add.
@@ -46840,7 +46910,15 @@ class ScoreRenderer {
46840
46910
  Logger.warning('Rendering', 'AlphaTab skipped rendering because of width=0 (element invisible)', null);
46841
46911
  return;
46842
46912
  }
46843
- this.boundsLookup = new BoundsLookup();
46913
+ // For partial renders we preserve the existing lookup so bars outside the re-layouted
46914
+ // range keep their already-scaled bounds - the layout will clear the changed range
46915
+ // before the paint pass re-registers fresh entries for it.
46916
+ if (renderHints?.firstChangedMasterBar !== undefined && this.boundsLookup) {
46917
+ this.boundsLookup.resetForPartialUpdate();
46918
+ }
46919
+ else {
46920
+ this.boundsLookup = new BoundsLookup();
46921
+ }
46844
46922
  this._recreateCanvas();
46845
46923
  this.canvas.lineWidth = 1;
46846
46924
  this.canvas.settings = this.settings;
@@ -48714,9 +48792,6 @@ class BeatTickLookup {
48714
48792
  * @param beat The beat to add.
48715
48793
  */
48716
48794
  highlightBeat(beat, playbackStart) {
48717
- if (beat.isEmpty && !beat.voice.isEmpty) {
48718
- return;
48719
- }
48720
48795
  if (!this._highlightedBeats.has(beat.id)) {
48721
48796
  this._highlightedBeats.set(beat.id, true);
48722
48797
  this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart));
@@ -50184,7 +50259,13 @@ class MidiFileGenerator {
50184
50259
  let beatStart = beat.playbackStart;
50185
50260
  let audioDuration = beat.playbackDuration;
50186
50261
  const masterBarDuration = beat.voice.bar.masterBar.calculateDuration();
50187
- if (beat.voice.bar.isEmpty) {
50262
+ // For a bar whose voice contains a single empty beat (the typical "whole-bar rest"
50263
+ // placeholder inserted during score.finish), extend the beat's audio duration to cover
50264
+ // the full bar so cursor navigation has a beat to follow across the whole bar. Don't
50265
+ // apply this when the voice has multiple beats: those represent explicit rhythmic
50266
+ // subdivisions even when each beat is empty (e.g. a recording grid of placeholder
50267
+ // slots), and overriding would make every beat overlap the whole bar.
50268
+ if (beat.voice.bar.isEmpty && beat.voice.beats.length === 1) {
50188
50269
  audioDuration = masterBarDuration;
50189
50270
  }
50190
50271
  else if (beat.voice.bar.masterBar.tripletFeel !== TripletFeel.NoTripletFeel &&
@@ -54805,7 +54886,8 @@ class AlphaTabApiBase {
54805
54886
  this._isInitialBeatCursorUpdate ||
54806
54887
  barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
54807
54888
  startBeatX < previousBeatBounds.onNotesX ||
54808
- barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
54889
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1 ||
54890
+ barBounds.h !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.h;
54809
54891
  if (jumpCursor) {
54810
54892
  cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
54811
54893
  }
@@ -58045,8 +58127,9 @@ class AlphaTabWebWorker {
58045
58127
  break;
58046
58128
  case 'alphaTab.renderScore':
58047
58129
  this._updateFontSizes(data.fontSizes);
58130
+ const renderHints = data.renderHints;
58048
58131
  const score = data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings);
58049
- this._renderMultiple(score, data.trackIndexes);
58132
+ this._renderMultiple(score, data.trackIndexes, renderHints);
58050
58133
  break;
58051
58134
  case 'alphaTab.updateSettings':
58052
58135
  this._updateSettings(data.settings);
@@ -62919,6 +63002,15 @@ class BarRendererBase {
62919
63002
  }
62920
63003
  return false;
62921
63004
  }
63005
+ /**
63006
+ * The fixed-overhead width of this renderer: glyphs that do not stretch when
63007
+ * the bar is scaled (clef, key signature, time signature, barlines, courtesy
63008
+ * accidentals, etc). Treated as a fixed allocation by the system-level layout
63009
+ * before distributing remaining width across bars by {@link Bar.displayScale}.
63010
+ */
63011
+ get fixedOverhead() {
63012
+ return this._preBeatGlyphs.width + this._postBeatGlyphs.width;
63013
+ }
62922
63014
  scaleToWidth(width) {
62923
63015
  // preBeat and postBeat glyphs do not get resized
62924
63016
  const containerWidth = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width;
@@ -65477,6 +65569,22 @@ class BarLayoutingInfo {
65477
65569
  postBeatSize = 0;
65478
65570
  minStretchForce = 0;
65479
65571
  totalSpringConstant = 0;
65572
+ /**
65573
+ * The smallest note duration encountered within this bar's springs, used as the reference in
65574
+ * the Gourlay stretch formula. Read by the owning {@link StaffSystem} so that the system can
65575
+ * aggregate a shared minimum across all bars and trigger a reconcile if an added bar introduces
65576
+ * a shorter duration than previously seen.
65577
+ */
65578
+ get localMinDuration() {
65579
+ return this._minDuration;
65580
+ }
65581
+ /**
65582
+ * The minimum-duration reference against which the spring constants currently held by this info
65583
+ * were computed. Set by {@link finish} and {@link recomputeSpringConstants}. The owning
65584
+ * StaffSystem compares this against its system-wide minimum to decide whether spring constants
65585
+ * need re-derivation.
65586
+ */
65587
+ computedWithMinDuration = 0;
65480
65588
  _updateMinStretchForce(force) {
65481
65589
  if (this.minStretchForce < force) {
65482
65590
  this.minStretchForce = force;
@@ -65646,10 +65754,26 @@ class BarLayoutingInfo {
65646
65754
  this._incompleteGraceRodsWidth += sp.preBeatWidth + sp.postSpringWidth;
65647
65755
  }
65648
65756
  }
65649
- this._calculateSpringConstants();
65757
+ this._calculateSpringConstants(this._minDuration);
65758
+ this.computedWithMinDuration = this._minDuration;
65650
65759
  this.version++;
65651
65760
  }
65652
- _calculateSpringConstants() {
65761
+ /**
65762
+ * Re-derives the spring constants (and {@link minStretchForce} / {@link totalSpringConstant})
65763
+ * using a caller-supplied minimum-duration reference rather than this bar's local minimum.
65764
+ *
65765
+ * Called by {@link StaffSystem.reconcileMinDurationIfDirty} when a bar added later to the
65766
+ * system introduced a shorter note than previously seen, invalidating this bar's spring
65767
+ * constants. Grace-rod data is not recomputed — it is independent of the minimum-duration
65768
+ * reference. The internal {@link version} is bumped so downstream consumers (e.g.
65769
+ * {@link BarRendererBase.applyLayoutingInfo}) pick up the refreshed positions.
65770
+ */
65771
+ recomputeSpringConstants(minDuration) {
65772
+ this._calculateSpringConstants(minDuration);
65773
+ this.computedWithMinDuration = minDuration;
65774
+ this.version++;
65775
+ }
65776
+ _calculateSpringConstants(minDuration) {
65653
65777
  let totalSpringConstant = 0;
65654
65778
  const sortedSprings = this._timeSortedSprings;
65655
65779
  if (sortedSprings.length === 0) {
@@ -65667,7 +65791,7 @@ class BarLayoutingInfo {
65667
65791
  const nextSpring = sortedSprings[i + 1];
65668
65792
  duration = Math.abs(nextSpring.timePosition - currentSpring.timePosition);
65669
65793
  }
65670
- currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration);
65794
+ currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration, minDuration);
65671
65795
  totalSpringConstant += 1 / currentSpring.springConstant;
65672
65796
  }
65673
65797
  this.totalSpringConstant = 1 / totalSpringConstant;
@@ -65727,7 +65851,7 @@ class BarLayoutingInfo {
65727
65851
  // springX += this.calculateWidth(force, spring.springConstant);
65728
65852
  // }
65729
65853
  // }
65730
- _calculateSpringConstant(spring, duration) {
65854
+ _calculateSpringConstant(spring, duration, minDuration) {
65731
65855
  if (duration <= 0) {
65732
65856
  duration = MidiUtils.toTicks(Duration.TwoHundredFiftySixth);
65733
65857
  }
@@ -65735,7 +65859,6 @@ class BarLayoutingInfo {
65735
65859
  spring.smallestDuration = duration;
65736
65860
  }
65737
65861
  const smallestDuration = spring.smallestDuration;
65738
- const minDuration = this._minDuration;
65739
65862
  const minDurationWidth = BarLayoutingInfo._defaultMinDurationWidth;
65740
65863
  const phi = 1 + 0.85 * Math.log2(duration / minDuration);
65741
65864
  return (smallestDuration / duration) * (1 / (phi * minDurationWidth));
@@ -65798,6 +65921,18 @@ class MasterBarsRenderers {
65798
65921
  canWrap = true;
65799
65922
  masterBar;
65800
65923
  additionalMultiBarRestIndexes = null;
65924
+ /**
65925
+ * Max fixed overhead (prefix + postfix glyph width) across all staves of this bar.
65926
+ * Used by the layout-mode horizontal scaling pass to carve out the fixed-overhead bucket
65927
+ * before distributing staff width across bars.
65928
+ */
65929
+ maxFixedOverhead = 0;
65930
+ /**
65931
+ * Max natural content width (computedWidth - fixedOverhead) across all staves of this bar.
65932
+ * Used as the bar weight when the layout ignores {@link MasterBar.displayScale} (e.g.
65933
+ * Page layout with `SystemsLayoutMode.Automatic`).
65934
+ */
65935
+ maxContentWidth = 0;
65801
65936
  get lastMasterBarIndex() {
65802
65937
  if (this.additionalMultiBarRestIndexes) {
65803
65938
  return this.additionalMultiBarRestIndexes[this.additionalMultiBarRestIndexes.length - 1];
@@ -65979,6 +66114,45 @@ class StaffSystem {
65979
66114
  * This value is mainly used in the parchment style layout for correct scaling of the bars.
65980
66115
  */
65981
66116
  totalBarDisplayScale = 0;
66117
+ /**
66118
+ * Sum of per-bar {@link MasterBarsRenderers.maxFixedOverhead} across the system. The layout-mode
66119
+ * horizontal scaling pass subtracts this from the available staff width before distributing the
66120
+ * remainder across bars.
66121
+ */
66122
+ totalFixedOverhead = 0;
66123
+ /**
66124
+ * Sum of per-bar {@link MasterBarsRenderers.maxContentWidth} across the system. Used as the
66125
+ * denominator when distributing staff width in modes that weight bars by natural content width
66126
+ * (Page layout with `SystemsLayoutMode.Automatic`).
66127
+ */
66128
+ totalContentWidth = 0;
66129
+ /**
66130
+ * Shortest note duration (in ticks) across every bar that has been added to this system, used
66131
+ * as the common reference in the Gourlay stretch formula so that rhythmically-equivalent beats
66132
+ * in different bars of the same system align column-wise.
66133
+ *
66134
+ * `-1` means "no bar added yet". The value only moves downward during system assembly; when a
66135
+ * new bar introduces a shorter minimum, {@link isMinDurationDirty} is set so that
66136
+ * {@link reconcileMinDurationIfDirty} can re-derive spring constants on the previously-added
66137
+ * bars before layout distribution runs.
66138
+ */
66139
+ minDuration = -1;
66140
+ /**
66141
+ * Set when a bar added to this system introduced a shorter {@link minDuration} than previously
66142
+ * seen, leaving earlier bars' spring constants stale. Consumed by
66143
+ * {@link reconcileMinDurationIfDirty} which is called from `VerticalLayoutBase._fitSystem`
66144
+ * once the system is fully assembled.
66145
+ */
66146
+ isMinDurationDirty = false;
66147
+ /**
66148
+ * Whether this system coordinates a shared minimum-duration reference across its bars for the
66149
+ * Gourlay stretch formula. Defaults to `true` for page-style and parchment layouts where bars
66150
+ * of a system fight for a common staff width. Set to `false` for horizontal layouts where each
66151
+ * bar is sized independently (by `bar.displayWidth` or its intrinsic width) and there is no
66152
+ * column-alignment concern - each bar keeps its local minimum so pre-existing rendering is
66153
+ * preserved.
66154
+ */
66155
+ shareMinDurationAcrossBars = true;
65982
66156
  isLast = false;
65983
66157
  masterBarsRenderers = [];
65984
66158
  staves = [];
@@ -66040,6 +66214,9 @@ class StaffSystem {
66040
66214
  }
66041
66215
  this.firstVisibleStaff = firstVisibleStaff;
66042
66216
  this._calculateAccoladeSpacing(tracks);
66217
+ // On the resize path the layoutingInfo was finalized in a previous layout pass, so we
66218
+ // only need to check whether its min-duration reference still matches the new system's.
66219
+ this._trackSystemMinDuration(renderers.layoutingInfo);
66043
66220
  this._applyLayoutAndUpdateWidth();
66044
66221
  return renderers;
66045
66222
  }
@@ -66095,10 +66272,89 @@ class StaffSystem {
66095
66272
  this.firstVisibleStaff = firstVisibleStaff;
66096
66273
  this._calculateAccoladeSpacing(tracks);
66097
66274
  barLayoutingInfo.finish();
66275
+ // Reconcile against the system-wide minimum-duration reference now that springs are
66276
+ // finalized. If this bar introduced a shorter note, earlier bars become stale (flagged
66277
+ // for bulk reconcile at fit time). If the system already had a shorter min than this
66278
+ // bar's local one, this bar's spring constants are recomputed immediately so the width
66279
+ // we return below reflects the shared reference.
66280
+ this._trackSystemMinDuration(barLayoutingInfo);
66098
66281
  // ensure same widths of new renderer
66099
66282
  result.width = this._applyLayoutAndUpdateWidth();
66100
66283
  return result;
66101
66284
  }
66285
+ /**
66286
+ * Updates {@link minDuration} and {@link isMinDurationDirty} when a bar is added, and brings
66287
+ * the just-added bar's {@link BarLayoutingInfo} in line with the current system minimum if the
66288
+ * system already saw a shorter reference. The bulk reconcile over previously-added bars is
66289
+ * deferred to {@link reconcileMinDurationIfDirty} (called from `_fitSystem`) to avoid
66290
+ * re-iterating the system every time a bar is appended.
66291
+ */
66292
+ _trackSystemMinDuration(info) {
66293
+ if (!this.shareMinDurationAcrossBars) {
66294
+ return;
66295
+ }
66296
+ const localMin = info.localMinDuration;
66297
+ if (this.minDuration === -1 || localMin < this.minDuration) {
66298
+ // this bar shortens the system minimum; earlier bars (if any) are now stale
66299
+ if (this.masterBarsRenderers.length > 1 && localMin !== this.minDuration) {
66300
+ this.isMinDurationDirty = true;
66301
+ }
66302
+ this.minDuration = localMin;
66303
+ }
66304
+ if (info.computedWithMinDuration > this.minDuration) {
66305
+ // this bar was initialized against a larger (local) min than the system carries; pull
66306
+ // it down to the system reference so its computedWidth reflects the shared spacing.
66307
+ info.recomputeSpringConstants(this.minDuration);
66308
+ }
66309
+ }
66310
+ /**
66311
+ * Re-derives spring constants on bars whose {@link BarLayoutingInfo.computedWithMinDuration}
66312
+ * is out of sync with the current {@link minDuration}, and rebuilds the cached system totals
66313
+ * (widths, {@link totalFixedOverhead}, {@link totalContentWidth}) from the refreshed bar
66314
+ * widths. Called from `VerticalLayoutBase._fitSystem` after the system is fully assembled and
66315
+ * before distribution runs. No-op when {@link isMinDurationDirty} is false.
66316
+ */
66317
+ reconcileMinDurationIfDirty() {
66318
+ if (!this.isMinDurationDirty) {
66319
+ return;
66320
+ }
66321
+ let systemWidth = this.accoladeWidth;
66322
+ let totalFixedOverhead = 0;
66323
+ let totalContentWidth = 0;
66324
+ for (const mb of this.masterBarsRenderers) {
66325
+ if (mb.layoutingInfo.computedWithMinDuration > this.minDuration) {
66326
+ mb.layoutingInfo.recomputeSpringConstants(this.minDuration);
66327
+ }
66328
+ let maxPrefix = 0;
66329
+ let maxContent = 0;
66330
+ let realWidth = 0;
66331
+ for (const r of mb.renderers) {
66332
+ r.applyLayoutingInfo();
66333
+ if (r.computedWidth > realWidth) {
66334
+ realWidth = r.computedWidth;
66335
+ }
66336
+ const overhead = r.fixedOverhead;
66337
+ if (overhead > maxPrefix) {
66338
+ maxPrefix = overhead;
66339
+ }
66340
+ const content = Math.max(0, r.computedWidth - overhead);
66341
+ if (content > maxContent) {
66342
+ maxContent = content;
66343
+ }
66344
+ }
66345
+ mb.maxFixedOverhead = maxPrefix;
66346
+ mb.maxContentWidth = maxContent;
66347
+ mb.width = realWidth;
66348
+ systemWidth += realWidth;
66349
+ totalFixedOverhead += maxPrefix;
66350
+ totalContentWidth += maxContent;
66351
+ }
66352
+ this.width = systemWidth;
66353
+ this.computedWidth = systemWidth;
66354
+ this.totalFixedOverhead = totalFixedOverhead;
66355
+ this.totalContentWidth = totalContentWidth;
66356
+ this.isMinDurationDirty = false;
66357
+ }
66102
66358
  getBarDisplayScale(renderer) {
66103
66359
  return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale;
66104
66360
  }
@@ -66137,12 +66393,16 @@ class StaffSystem {
66137
66393
  this.width -= width;
66138
66394
  this.computedWidth -= width;
66139
66395
  this.totalBarDisplayScale -= barDisplayScale;
66396
+ this.totalFixedOverhead -= toRemove.maxFixedOverhead;
66397
+ this.totalContentWidth -= toRemove.maxContentWidth;
66140
66398
  return toRemove;
66141
66399
  }
66142
66400
  return null;
66143
66401
  }
66144
66402
  _applyLayoutAndUpdateWidth() {
66145
66403
  let realWidth = 0;
66404
+ let maxFixedOverhead = 0;
66405
+ let maxContentWidth = 0;
66146
66406
  let barDisplayScale = 0;
66147
66407
  for (const s of this.allStaves) {
66148
66408
  const last = s.barRenderers[s.barRenderers.length - 1];
@@ -66151,8 +66411,21 @@ class StaffSystem {
66151
66411
  if (last.computedWidth > realWidth) {
66152
66412
  realWidth = last.computedWidth;
66153
66413
  }
66414
+ const overhead = last.fixedOverhead;
66415
+ if (overhead > maxFixedOverhead) {
66416
+ maxFixedOverhead = overhead;
66417
+ }
66418
+ const content = Math.max(0, last.computedWidth - overhead);
66419
+ if (content > maxContentWidth) {
66420
+ maxContentWidth = content;
66421
+ }
66154
66422
  }
66423
+ const renderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1];
66424
+ renderers.maxFixedOverhead = maxFixedOverhead;
66425
+ renderers.maxContentWidth = maxContentWidth;
66155
66426
  this.totalBarDisplayScale += barDisplayScale;
66427
+ this.totalFixedOverhead += maxFixedOverhead;
66428
+ this.totalContentWidth += maxContentWidth;
66156
66429
  this.width += realWidth;
66157
66430
  this.computedWidth += realWidth;
66158
66431
  return realWidth;
@@ -66650,17 +66923,6 @@ class StaffSystem {
66650
66923
  }
66651
66924
  }
66652
66925
 
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
66926
  /**
66665
66927
  * This is the base class for creating new layouting engines for the score renderer.
66666
66928
  * @internal
@@ -66691,15 +66953,21 @@ class ScoreLayout {
66691
66953
  this.doResize();
66692
66954
  }
66693
66955
  layoutAndRender(renderHints) {
66694
- this._lazyPartials.clear();
66695
66956
  this.slurRegistry.clear();
66696
- this.beamingRuleLookups.clear();
66697
- this._barRendererLookup.clear();
66698
- this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66699
66957
  const score = this.renderer.score;
66700
66958
  this.firstBarIndex = ModelUtils.computeFirstDisplayedBarIndex(score, this.renderer.settings);
66701
66959
  this.lastBarIndex = ModelUtils.computeLastDisplayedBarIndex(score, this.renderer.settings, this.firstBarIndex);
66702
66960
  this.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo(this.renderer.tracks, this.firstBarIndex, this.lastBarIndex);
66961
+ const firstChangedMasterBar = renderHints?.firstChangedMasterBar;
66962
+ if (firstChangedMasterBar !== undefined) {
66963
+ if (this.doUpdateForBars(renderHints)) {
66964
+ return;
66965
+ }
66966
+ }
66967
+ this._lazyPartials.clear();
66968
+ this.beamingRuleLookups.clear();
66969
+ this._barRendererLookup.clear();
66970
+ this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66703
66971
  this.pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale);
66704
66972
  if (!this.pagePadding) {
66705
66973
  this.pagePadding = [0, 0, 0, 0];
@@ -66714,6 +66982,9 @@ class ScoreLayout {
66714
66982
  this.doLayoutAndRender(renderHints);
66715
66983
  }
66716
66984
  _lazyPartials = new Map();
66985
+ getExistingPartialArgs(id) {
66986
+ return this._lazyPartials.has(id) ? this._lazyPartials.get(id).args : undefined;
66987
+ }
66717
66988
  registerPartial(args, callback) {
66718
66989
  if (args.height === 0) {
66719
66990
  return;
@@ -66732,7 +67003,11 @@ class ScoreLayout {
66732
67003
  }
66733
67004
  else {
66734
67005
  // in case of lazy loading -> first register lazy, then notify
66735
- this._lazyPartials.set(args.id, new LazyPartial(args, callback));
67006
+ const partial = {
67007
+ args,
67008
+ renderCallback: callback
67009
+ };
67010
+ this._lazyPartials.set(args.id, partial);
66736
67011
  this.renderer.partialLayoutFinished.trigger(args);
66737
67012
  }
66738
67013
  }
@@ -67027,7 +67302,7 @@ class ScoreLayout {
67027
67302
  glyph.textAlign = TextAlign.Left;
67028
67303
  }
67029
67304
  }
67030
- layoutAndRenderAnnotation(y) {
67305
+ _layoutAndRenderAnnotation(y) {
67031
67306
  // attention, you are not allowed to remove change this notice within any version of this library without permission!
67032
67307
  const msg = 'rendered by alphaTab';
67033
67308
  const resources = this.renderer.settings.display.resources;
@@ -67091,6 +67366,12 @@ class HorizontalScreenLayout extends ScoreLayout {
67091
67366
  }
67092
67367
  doResize() {
67093
67368
  }
67369
+ doUpdateForBars(_renderHints) {
67370
+ // not supported yet, modifications likely cause anyhow full updates
67371
+ // as we do not optimize effect bands yet. with effect bands being more
67372
+ // isolated in bars we could try updating dynamically
67373
+ return false;
67374
+ }
67094
67375
  doLayoutAndRender(renderHints) {
67095
67376
  const score = this.renderer.score;
67096
67377
  let startIndex = this.renderer.settings.display.startBar;
@@ -67104,6 +67385,11 @@ class HorizontalScreenLayout extends ScoreLayout {
67104
67385
  endBarIndex = startIndex + endBarIndex - 1; // map count to array index
67105
67386
  endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex));
67106
67387
  this._system = this.createEmptyStaffSystem(0);
67388
+ // Each bar in horizontal layout is sized independently (by bar.displayWidth or the bar's
67389
+ // intrinsic width), so there is no shared staff width to distribute across bars. Keep each
67390
+ // bar's spring constants referenced against its own local minimum-duration so rendering
67391
+ // matches the historical per-bar behaviour.
67392
+ this._system.shareMinDurationAcrossBars = false;
67107
67393
  this._system.isLast = true;
67108
67394
  this._system.x = this.pagePadding[0];
67109
67395
  this._system.y = this.pagePadding[1];
@@ -67165,7 +67451,7 @@ class HorizontalScreenLayout extends ScoreLayout {
67165
67451
  currentBarIndex += partial.masterBars.length;
67166
67452
  }
67167
67453
  this.height = this.layoutAndRenderBottomScoreInfo(this.height);
67168
- this.height = this.layoutAndRenderAnnotation(this.height);
67454
+ this.height = this._layoutAndRenderAnnotation(this.height);
67169
67455
  this.height += this.pagePadding[3];
67170
67456
  this.height *= this.renderer.settings.display.scale;
67171
67457
  }
@@ -67232,11 +67518,16 @@ class VerticalLayoutBase extends ScoreLayout {
67232
67518
  _allMasterBarRenderers = [];
67233
67519
  _barsFromPreviousSystem = [];
67234
67520
  _reuseViewPort = false;
67521
+ _preSystemPartialIds = [];
67522
+ _systemPartialIds = [];
67235
67523
  doLayoutAndRender(renderHints) {
67236
67524
  let y = this.pagePadding[1];
67237
67525
  this.width = this.renderer.width;
67238
67526
  this._allMasterBarRenderers = [];
67527
+ this._preSystemPartialIds = [];
67528
+ this._systemPartialIds = [];
67239
67529
  this._reuseViewPort = renderHints?.reuseViewport ?? false;
67530
+ this._systems = [];
67240
67531
  //
67241
67532
  // 1. Score Info
67242
67533
  y = this._layoutAndRenderScoreInfo(y, -1);
@@ -67248,15 +67539,23 @@ class VerticalLayoutBase extends ScoreLayout {
67248
67539
  y = this._layoutAndRenderChordDiagrams(y, -1);
67249
67540
  //
67250
67541
  // 4. One result per StaffSystem
67251
- y = this._layoutAndRenderScore(y);
67542
+ y = this._layoutAndRenderScore(y, this.firstBarIndex);
67252
67543
  y = this.layoutAndRenderBottomScoreInfo(y);
67253
- y = this.layoutAndRenderAnnotation(y);
67544
+ y = this._layoutAndRenderAnnotation(y);
67254
67545
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67255
67546
  }
67256
67547
  registerPartial(args, callback) {
67257
67548
  args.reuseViewport = this._reuseViewPort;
67258
67549
  super.registerPartial(args, callback);
67259
67550
  }
67551
+ reregisterPartial(id) {
67552
+ const args = this.getExistingPartialArgs(id);
67553
+ if (!args) {
67554
+ return;
67555
+ }
67556
+ args.reuseViewport = this._reuseViewPort;
67557
+ this.renderer.partialLayoutFinished.trigger(args);
67558
+ }
67260
67559
  get supportsResize() {
67261
67560
  return true;
67262
67561
  }
@@ -67267,6 +67566,47 @@ class VerticalLayoutBase extends ScoreLayout {
67267
67566
  }
67268
67567
  return x;
67269
67568
  }
67569
+ doUpdateForBars(renderHints) {
67570
+ this._reuseViewPort = renderHints.reuseViewport ?? false;
67571
+ const firstModifiedMasterBar = renderHints.firstChangedMasterBar;
67572
+ // first update existing systems as needed
67573
+ const systemIndex = this._systems.findIndex(s => {
67574
+ const first = s.masterBarsRenderers[0].masterBar.index;
67575
+ const last = s.masterBarsRenderers[s.masterBarsRenderers.length - 1].masterBar.index;
67576
+ return first <= firstModifiedMasterBar && firstModifiedMasterBar <= last;
67577
+ });
67578
+ if (systemIndex === -1 || !this.renderer.settings.core.enableLazyLoading) {
67579
+ return false;
67580
+ }
67581
+ // Bars from the start of the re-layouted system onward will be re-registered during the
67582
+ // paint pass. Clear their old entries from the preserved BoundsLookup so registration
67583
+ // produces a clean, complete lookup after this render finishes.
67584
+ const firstRebuiltBarIndex = this._systems[systemIndex].masterBarsRenderers[0].masterBar.index;
67585
+ this.renderer.boundsLookup.clearFromMasterBar(firstRebuiltBarIndex);
67586
+ // for now we do a full relayout from the first modified masterbar
67587
+ // there is a lot of room for even more performant updates, but they come
67588
+ // at a risk that features break.
67589
+ // e.g. we could only shift systems where the content didn't change,
67590
+ // but we might still have ties/slurs which have to be updated.
67591
+ const removeSystems = this._systems.splice(systemIndex, this._systems.length - systemIndex);
67592
+ this._systemPartialIds.splice(systemIndex, this._systemPartialIds.length - systemIndex);
67593
+ const system = removeSystems[0];
67594
+ let y = system.y;
67595
+ const firstBarIndex = system.masterBarsRenderers[0].masterBar.index;
67596
+ // signal all partials which didn't change
67597
+ for (const preSystemPartial of this._preSystemPartialIds) {
67598
+ this.reregisterPartial(preSystemPartial);
67599
+ }
67600
+ for (let i = 0; i < systemIndex; i++) {
67601
+ this.reregisterPartial(this._systemPartialIds[i]);
67602
+ }
67603
+ // new partials for all other prats
67604
+ y = this._layoutAndRenderScore(y, firstBarIndex);
67605
+ y = this.layoutAndRenderBottomScoreInfo(y);
67606
+ y = this._layoutAndRenderAnnotation(y);
67607
+ this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67608
+ return true;
67609
+ }
67270
67610
  doResize() {
67271
67611
  let y = this.pagePadding[1];
67272
67612
  this.width = this.renderer.width;
@@ -67285,7 +67625,7 @@ class VerticalLayoutBase extends ScoreLayout {
67285
67625
  // 4. One result per StaffSystem
67286
67626
  y = this._resizeAndRenderScore(y, oldHeight);
67287
67627
  y = this.layoutAndRenderBottomScoreInfo(y);
67288
- y = this.layoutAndRenderAnnotation(y);
67628
+ y = this._layoutAndRenderAnnotation(y);
67289
67629
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67290
67630
  }
67291
67631
  _layoutAndRenderTunings(y, totalHeight = -1) {
@@ -67309,6 +67649,7 @@ class VerticalLayoutBase extends ScoreLayout {
67309
67649
  canvas.textAlign = TextAlign.Center;
67310
67650
  this.tuningGlyph.paint(0, 0, canvas);
67311
67651
  });
67652
+ this._preSystemPartialIds.push(e.id);
67312
67653
  return y + tuningHeight;
67313
67654
  }
67314
67655
  _layoutAndRenderChordDiagrams(y, totalHeight = -1) {
@@ -67332,6 +67673,7 @@ class VerticalLayoutBase extends ScoreLayout {
67332
67673
  canvas.textAlign = TextAlign.Center;
67333
67674
  this.chordDiagrams.paint(0, 0, canvas);
67334
67675
  });
67676
+ this._preSystemPartialIds.push(e.id);
67335
67677
  return y + diagramHeight;
67336
67678
  }
67337
67679
  _layoutAndRenderScoreInfo(y, totalHeight = -1) {
@@ -67374,12 +67716,14 @@ class VerticalLayoutBase extends ScoreLayout {
67374
67716
  g.paint(0, 0, canvas);
67375
67717
  }
67376
67718
  });
67719
+ this._preSystemPartialIds.push(e.id);
67377
67720
  }
67378
67721
  return y + infoHeight;
67379
67722
  }
67380
67723
  _resizeAndRenderScore(y, oldHeight) {
67381
67724
  // if we have a fixed number of bars per row, we only need to refit them.
67382
67725
  const barsPerRowActive = this.getBarsPerSystem(0) > 0;
67726
+ this._systemPartialIds = [];
67383
67727
  if (barsPerRowActive) {
67384
67728
  for (let i = 0; i < this._systems.length; i++) {
67385
67729
  const system = this._systems[i];
@@ -67442,11 +67786,9 @@ class VerticalLayoutBase extends ScoreLayout {
67442
67786
  }
67443
67787
  return y;
67444
67788
  }
67445
- _layoutAndRenderScore(y) {
67446
- const startIndex = this.firstBarIndex;
67789
+ _layoutAndRenderScore(y, startIndex) {
67447
67790
  let currentBarIndex = startIndex;
67448
67791
  const endBarIndex = this.lastBarIndex;
67449
- this._systems = [];
67450
67792
  while (currentBarIndex <= endBarIndex) {
67451
67793
  // create system and align set proper coordinates
67452
67794
  const system = this._createStaffSystem(currentBarIndex, endBarIndex);
@@ -67481,6 +67823,7 @@ class VerticalLayoutBase extends ScoreLayout {
67481
67823
  // since we use partial drawing
67482
67824
  system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas);
67483
67825
  });
67826
+ this._systemPartialIds.push(args.id);
67484
67827
  // calculate coordinates for next system
67485
67828
  return height;
67486
67829
  }
@@ -67488,6 +67831,10 @@ class VerticalLayoutBase extends ScoreLayout {
67488
67831
  * Realignes the bars in this line according to the available space
67489
67832
  */
67490
67833
  _fitSystem(system) {
67834
+ // If a bar added late in the assembly introduced a shorter note than earlier bars, the
67835
+ // earlier bars' spring constants (and the cached system widths / totals) are stale.
67836
+ // Reconcile now - it's a no-op when nothing changed.
67837
+ system.reconcileMinDurationIfDirty();
67491
67838
  if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) {
67492
67839
  this._scaleToWidth(system, this._maxWidth);
67493
67840
  }
@@ -67499,29 +67846,35 @@ class VerticalLayoutBase extends ScoreLayout {
67499
67846
  _scaleToWidth(system, width) {
67500
67847
  const staffWidth = width - system.accoladeWidth;
67501
67848
  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;
67849
+ // Industry fixed-overhead model (Behind Bars, Dorico, Finale, Sibelius, MuseScore, Guitar Pro):
67850
+ // prefix/postfix glyphs (clef, key sig, time sig, barlines) are treated as fixed overhead and the
67851
+ // remaining staff width is distributed across bars by a per-bar weight.
67852
+ //
67853
+ // distributable = staffWidth - totalFixedOverhead
67854
+ // contentShare = distributable / sum(weight)
67855
+ // bar.width = bar.maxFixedOverhead + weight * contentShare
67856
+ //
67857
+ // The weight depends on the layout mode:
67858
+ // - shouldApplyBarScale=true -> weight = bar.displayScale (model-driven, matches Guitar Pro)
67859
+ // displayScale defaults to 1, so an unset value behaves identically
67860
+ // to an explicit 1 (GP omits the property when not customized).
67861
+ // - shouldApplyBarScale=false -> weight = natural content width (automatic, ignores displayScale)
67862
+ //
67863
+ // Per-bar maxFixedOverhead / maxContentWidth and the system-wide totals are maintained incrementally
67864
+ // in StaffSystem._applyLayoutAndUpdateWidth / revertLastBar so this pass can apply directly.
67865
+ const weightTotal = shouldApplyBarScale ? system.totalBarDisplayScale : system.totalContentWidth;
67866
+ const distributable = Math.max(0, staffWidth - system.totalFixedOverhead);
67867
+ const contentShare = weightTotal > 0 ? distributable / weightTotal : 0;
67510
67868
  for (const s of system.allStaves) {
67511
67869
  s.resetSharedLayoutData();
67512
- // scale the bars by keeping their respective ratio size
67513
67870
  let w = 0;
67514
- for (const renderer of s.barRenderers) {
67871
+ for (let i = 0; i < s.barRenderers.length; i++) {
67872
+ const renderer = s.barRenderers[i];
67873
+ const mb = system.masterBarsRenderers[i];
67515
67874
  renderer.x = w;
67516
67875
  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
- }
67876
+ const weight = shouldApplyBarScale ? system.getBarDisplayScale(renderer) : mb.maxContentWidth;
67877
+ const actualBarWidth = mb.maxFixedOverhead + weight * contentShare;
67525
67878
  renderer.scaleToWidth(actualBarWidth);
67526
67879
  w += renderer.width;
67527
67880
  }
@@ -69345,12 +69698,24 @@ class LineBarRenderer extends BarRendererBase {
69345
69698
  const firstNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(firstNonRestBeat);
69346
69699
  const lastNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(lastNonRestBeat);
69347
69700
  const direction = this.getTupletBeamDirection(firstNonRestBeamingHelper);
69348
- let startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69349
- let endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
69701
+ let startY;
69702
+ let endY;
69350
69703
  if (isRestOnly) {
69351
- startY = Math.max(startY, endY);
69704
+ // rests have no stems, so anchor to the actual rest glyph bounds
69705
+ // instead of a stem-adjusted flag position (which would place the bracket
69706
+ // a full quarter-stem length away from the rests).
69707
+ if (direction === BeamDirection.Up) {
69708
+ startY = Math.min(this.getRestY(firstNonRestBeat, NoteYPosition.Top), this.getRestY(lastNonRestBeat, NoteYPosition.Top));
69709
+ }
69710
+ else {
69711
+ startY = Math.max(this.getRestY(firstNonRestBeat, NoteYPosition.Bottom), this.getRestY(lastNonRestBeat, NoteYPosition.Bottom));
69712
+ }
69352
69713
  endY = startY;
69353
69714
  }
69715
+ else {
69716
+ startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69717
+ endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
69718
+ }
69354
69719
  // align line centered in available space
69355
69720
  if (direction === BeamDirection.Down) {
69356
69721
  startY += shift;
@@ -69720,7 +70085,30 @@ class LineBarRenderer extends BarRendererBase {
69720
70085
  let minNoteY = 0;
69721
70086
  for (const v of this.helpers.beamHelpers) {
69722
70087
  for (const h of v) {
69723
- if (!this.shouldPaintBeamingHelper(h)) ;
70088
+ if (!this.shouldPaintBeamingHelper(h)) {
70089
+ // beam is not drawn, but a rest-only tuplet still draws a bracket
70090
+ // anchored to the rest glyph bounds and needs overflow reserved.
70091
+ if (h.hasTuplet && h.isRestBeamHelper) {
70092
+ const tupletGroup = h.beats[0].tupletGroup;
70093
+ const tupletFirst = tupletGroup.beats[0];
70094
+ const tupletLast = tupletGroup.beats[tupletGroup.beats.length - 1];
70095
+ const tupletDirection = this.getTupletBeamDirection(h);
70096
+ if (tupletDirection === BeamDirection.Up) {
70097
+ const restTop = Math.min(this.getRestY(tupletFirst, NoteYPosition.Top), this.getRestY(tupletLast, NoteYPosition.Top));
70098
+ const topY = restTop - this.tupletSize - this.tupletOffset;
70099
+ if (topY < maxNoteY) {
70100
+ maxNoteY = topY;
70101
+ }
70102
+ }
70103
+ else {
70104
+ const restBottom = Math.max(this.getRestY(tupletFirst, NoteYPosition.Bottom), this.getRestY(tupletLast, NoteYPosition.Bottom));
70105
+ const bottomY = restBottom + this.tupletSize + this.tupletOffset;
70106
+ if (bottomY > minNoteY) {
70107
+ minNoteY = bottomY;
70108
+ }
70109
+ }
70110
+ }
70111
+ }
69724
70112
  else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) {
69725
70113
  const tupletDirection = this.getTupletBeamDirection(h);
69726
70114
  const direction = this.getBeamDirection(h);