@coderline/alphatab 1.9.0-alpha.1785 → 1.9.0-alpha.1804
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.core.min.mjs +2 -2
- package/dist/alphaTab.core.mjs +399 -52
- package/dist/alphaTab.d.ts +15 -1
- package/dist/alphaTab.js +399 -52
- package/dist/alphaTab.min.js +2 -2
- package/dist/alphaTab.min.mjs +1 -1
- package/dist/alphaTab.mjs +1 -1
- package/dist/alphaTab.worker.min.mjs +1 -1
- package/dist/alphaTab.worker.mjs +1 -1
- package/dist/alphaTab.worklet.min.mjs +1 -1
- package/dist/alphaTab.worklet.mjs +1 -1
- package/package.json +3 -3
package/dist/alphaTab.core.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* alphaTab v1.9.0-alpha.
|
|
2
|
+
* alphaTab v1.9.0-alpha.1804 (develop, build 1804)
|
|
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.
|
|
207
|
-
static date = '2026-
|
|
208
|
-
static commit = '
|
|
206
|
+
static version = '1.9.0-alpha.1804';
|
|
207
|
+
static date = '2026-05-16T03:54:50.685Z';
|
|
208
|
+
static commit = '6e757c0cbec66598a7fa5327abc9d05616984486';
|
|
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
|
|
6425
|
-
|
|
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
|
-
|
|
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
|
*/
|
|
@@ -10221,7 +10315,7 @@ class IOHelper {
|
|
|
10221
10315
|
encoding = 'utf-8';
|
|
10222
10316
|
}
|
|
10223
10317
|
const decoder = new TextDecoder(encoding);
|
|
10224
|
-
return decoder.decode(data
|
|
10318
|
+
return decoder.decode(data);
|
|
10225
10319
|
}
|
|
10226
10320
|
static _detectEncoding(data) {
|
|
10227
10321
|
if (data.length > 2 && data[0] === 0xfe && data[1] === 0xff) {
|
|
@@ -19651,7 +19745,7 @@ class Gp3To5Importer extends ScoreImporter {
|
|
|
19651
19745
|
static _versionString = 'FICHIER GUITAR PRO ';
|
|
19652
19746
|
// NOTE: General Midi only defines percussion instruments from 35-81
|
|
19653
19747
|
// Guitar Pro 5 allowed GS extensions (27-34 and 82-87)
|
|
19654
|
-
// GP7-8 do not have all these definitions anymore, this lookup ensures some fallback
|
|
19748
|
+
// GP7-8 do not have all these definitions anymore, this lookup ensures some fallback
|
|
19655
19749
|
// (even if they are not correct)
|
|
19656
19750
|
// we can support this properly in future when we allow custom alphaTex articulation definitions
|
|
19657
19751
|
// then we don't need to rely on GP specifics anymore but handle things on export/import
|
|
@@ -21042,12 +21136,13 @@ class GpBinaryHelpers {
|
|
|
21042
21136
|
* @returns
|
|
21043
21137
|
*/
|
|
21044
21138
|
static gpReadStringByteLength(data, length, encoding) {
|
|
21139
|
+
// Fixed-width string field: 1 length byte + `length` data bytes, decoded
|
|
21140
|
+
// up to min(stringLength, length). Always consumes 1 + length bytes.
|
|
21045
21141
|
const stringLength = data.readByte();
|
|
21046
|
-
const
|
|
21047
|
-
|
|
21048
|
-
|
|
21049
|
-
|
|
21050
|
-
return s;
|
|
21142
|
+
const fieldBytes = new Uint8Array(length);
|
|
21143
|
+
data.read(fieldBytes, 0, length);
|
|
21144
|
+
const effectiveLength = Math.min(stringLength, length);
|
|
21145
|
+
return IOHelper.toString(fieldBytes.subarray(0, effectiveLength), encoding);
|
|
21051
21146
|
}
|
|
21052
21147
|
}
|
|
21053
21148
|
/**
|
|
@@ -24267,6 +24362,10 @@ class GpifParser {
|
|
|
24267
24362
|
}
|
|
24268
24363
|
// build masterbar automations
|
|
24269
24364
|
for (const [barNumber, automations] of this._masterTrackAutomations) {
|
|
24365
|
+
if (barNumber < 0 || barNumber >= this.score.masterBars.length) {
|
|
24366
|
+
// automation references a bar that is not in the score's masterBars list
|
|
24367
|
+
continue;
|
|
24368
|
+
}
|
|
24270
24369
|
const masterBar = this.score.masterBars[barNumber];
|
|
24271
24370
|
for (let i = 0, j = automations.length; i < j; i++) {
|
|
24272
24371
|
const automation = automations[i];
|
|
@@ -31942,7 +32041,9 @@ class RenderingResources {
|
|
|
31942
32041
|
[NotationElement.RepeatCount, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
|
|
31943
32042
|
[NotationElement.BarNumber, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
|
|
31944
32043
|
[NotationElement.ScoreBendSlur, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
|
|
31945
|
-
[NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)]
|
|
32044
|
+
[NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)],
|
|
32045
|
+
[NotationElement.EffectHammerOnPullOffText, RenderingResources._effectFont],
|
|
32046
|
+
[NotationElement.EffectSlideText, RenderingResources._effectFont]
|
|
31946
32047
|
]);
|
|
31947
32048
|
/**
|
|
31948
32049
|
* The name of the SMuFL Font to use for rendering music symbols.
|
|
@@ -32233,9 +32334,16 @@ class RenderingResources {
|
|
|
32233
32334
|
notationElement = NotationElement.ScoreWords;
|
|
32234
32335
|
break;
|
|
32235
32336
|
}
|
|
32337
|
+
return this.getFontForNotationElement(notationElement);
|
|
32338
|
+
}
|
|
32339
|
+
/**
|
|
32340
|
+
* @internal
|
|
32341
|
+
* @param element
|
|
32342
|
+
*/
|
|
32343
|
+
getFontForNotationElement(notationElement) {
|
|
32236
32344
|
return this.elementFonts.has(notationElement)
|
|
32237
32345
|
? this.elementFonts.get(notationElement)
|
|
32238
|
-
: RenderingResources.defaultFonts.get(
|
|
32346
|
+
: RenderingResources.defaultFonts.get(notationElement);
|
|
32239
32347
|
}
|
|
32240
32348
|
}
|
|
32241
32349
|
|
|
@@ -49161,6 +49269,21 @@ class MasterBarTickLookup {
|
|
|
49161
49269
|
}
|
|
49162
49270
|
}
|
|
49163
49271
|
|
|
49272
|
+
/**
|
|
49273
|
+
* Represents a range of the song that should be played.
|
|
49274
|
+
* @public
|
|
49275
|
+
*/
|
|
49276
|
+
class PlaybackRange {
|
|
49277
|
+
/**
|
|
49278
|
+
* The position in midi ticks from where the song should start.
|
|
49279
|
+
*/
|
|
49280
|
+
startTick = 0;
|
|
49281
|
+
/**
|
|
49282
|
+
* The position in midi ticks to where the song should be played.
|
|
49283
|
+
*/
|
|
49284
|
+
endTick = 0;
|
|
49285
|
+
}
|
|
49286
|
+
|
|
49164
49287
|
/**
|
|
49165
49288
|
* Describes how a cursor should be moving.
|
|
49166
49289
|
* @public
|
|
@@ -49321,6 +49444,13 @@ class MidiTickLookup {
|
|
|
49321
49444
|
* @internal
|
|
49322
49445
|
*/
|
|
49323
49446
|
masterBarLookup = new Map();
|
|
49447
|
+
/**
|
|
49448
|
+
* A dictionary of all beat played. The index is the id to {@link Beat.id}.
|
|
49449
|
+
* The value is the bar relative tick time at which the beat was registered during midi generation.
|
|
49450
|
+
* This lookup only contains the first time a Beat is played.
|
|
49451
|
+
* @internal
|
|
49452
|
+
*/
|
|
49453
|
+
beatLookup = new Map();
|
|
49324
49454
|
/**
|
|
49325
49455
|
* A list of all {@link MasterBarTickLookup} sorted by time.
|
|
49326
49456
|
*/
|
|
@@ -49687,10 +49817,22 @@ class MidiTickLookup {
|
|
|
49687
49817
|
* @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
|
|
49688
49818
|
*/
|
|
49689
49819
|
getBeatStart(beat) {
|
|
49690
|
-
if (!this.masterBarLookup.has(beat.voice.bar.index)) {
|
|
49820
|
+
if (!this.masterBarLookup.has(beat.voice.bar.index) || !this.beatLookup.has(beat.id)) {
|
|
49691
49821
|
return 0;
|
|
49692
49822
|
}
|
|
49693
|
-
|
|
49823
|
+
const mb = this.masterBarLookup.get(beat.voice.bar.index);
|
|
49824
|
+
return mb.start + this.beatLookup.get(beat.id).startTick;
|
|
49825
|
+
}
|
|
49826
|
+
/**
|
|
49827
|
+
* Gets the playback range in midi ticks for a given beat.
|
|
49828
|
+
* @param beat The beat to find the time period for.
|
|
49829
|
+
* @returns The relative playback range within the parent masterbar at which the beat start and ends playing
|
|
49830
|
+
*/
|
|
49831
|
+
getRelativeBeatPlaybackRange(beat) {
|
|
49832
|
+
if (!this.beatLookup.has(beat.id)) {
|
|
49833
|
+
return undefined;
|
|
49834
|
+
}
|
|
49835
|
+
return this.beatLookup.get(beat.id);
|
|
49694
49836
|
}
|
|
49695
49837
|
/**
|
|
49696
49838
|
* Adds a new {@link MasterBarTickLookup} to the lookup table.
|
|
@@ -49708,6 +49850,12 @@ class MidiTickLookup {
|
|
|
49708
49850
|
}
|
|
49709
49851
|
}
|
|
49710
49852
|
addBeat(beat, start, duration) {
|
|
49853
|
+
if (!this.beatLookup.has(beat.id)) {
|
|
49854
|
+
const playbackRange = new PlaybackRange();
|
|
49855
|
+
playbackRange.startTick = start;
|
|
49856
|
+
playbackRange.endTick = start + duration;
|
|
49857
|
+
this.beatLookup.set(beat.id, playbackRange);
|
|
49858
|
+
}
|
|
49711
49859
|
const currentMasterBar = this._currentMasterBar;
|
|
49712
49860
|
if (currentMasterBar) {
|
|
49713
49861
|
// pre-beat grace notes at the start of the bar we also add the beat to the previous bar
|
|
@@ -52291,21 +52439,6 @@ class ActiveBeatsChangedEventArgs {
|
|
|
52291
52439
|
}
|
|
52292
52440
|
}
|
|
52293
52441
|
|
|
52294
|
-
/**
|
|
52295
|
-
* Represents a range of the song that should be played.
|
|
52296
|
-
* @public
|
|
52297
|
-
*/
|
|
52298
|
-
class PlaybackRange {
|
|
52299
|
-
/**
|
|
52300
|
-
* The position in midi ticks from where the song should start.
|
|
52301
|
-
*/
|
|
52302
|
-
startTick = 0;
|
|
52303
|
-
/**
|
|
52304
|
-
* The position in midi ticks to where the song should be played.
|
|
52305
|
-
*/
|
|
52306
|
-
endTick = 0;
|
|
52307
|
-
}
|
|
52308
|
-
|
|
52309
52442
|
/**
|
|
52310
52443
|
* A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
|
|
52311
52444
|
* allowing dynamic changing of the underlying instance without loosing aspects like the
|
|
@@ -55506,23 +55639,24 @@ class AlphaTabApiBase {
|
|
|
55506
55639
|
if (this._selectionStart && this._tickCache) {
|
|
55507
55640
|
// get the start and stop ticks (which consider properly repeats)
|
|
55508
55641
|
const tickCache = this._tickCache;
|
|
55509
|
-
const
|
|
55642
|
+
const realStartMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
|
|
55643
|
+
const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionStart.beat);
|
|
55644
|
+
const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? this._selectionStart.beat.playbackStart;
|
|
55510
55645
|
// move to selection start
|
|
55511
55646
|
this._currentBeat = null; // reset current beat so it is updating the cursor
|
|
55512
55647
|
if (this._player.state === PlayerState.Paused) {
|
|
55513
|
-
this._cursorUpdateTick(
|
|
55648
|
+
this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1);
|
|
55514
55649
|
}
|
|
55515
|
-
this.tickPosition =
|
|
55650
|
+
this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart;
|
|
55516
55651
|
// set playback range
|
|
55517
55652
|
if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) {
|
|
55518
|
-
const
|
|
55653
|
+
const realEndMasterBarStart = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
|
|
55654
|
+
const endBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionEnd.beat);
|
|
55655
|
+
const endBeatPlaybackEnd = endBeatPlaybackRange?.endTick ??
|
|
55656
|
+
this._selectionEnd.beat.playbackStart + this._selectionEnd.beat.playbackDuration;
|
|
55519
55657
|
const range = new PlaybackRange();
|
|
55520
|
-
range.startTick =
|
|
55521
|
-
range.endTick =
|
|
55522
|
-
realMasterBarEnd +
|
|
55523
|
-
this._selectionEnd.beat.playbackStart +
|
|
55524
|
-
this._selectionEnd.beat.playbackDuration -
|
|
55525
|
-
50;
|
|
55658
|
+
range.startTick = realStartMasterBarStart + startBeatPlaybackStart;
|
|
55659
|
+
range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50;
|
|
55526
55660
|
this.playbackRange = range;
|
|
55527
55661
|
}
|
|
55528
55662
|
else {
|
|
@@ -61569,6 +61703,12 @@ class TieGlyph extends Glyph {
|
|
|
61569
61703
|
_tieHeight = 0;
|
|
61570
61704
|
_boundingBox;
|
|
61571
61705
|
_shouldPaint = false;
|
|
61706
|
+
// Resolved per-label paint state. Lazily grown; re-layouts mutate
|
|
61707
|
+
// existing entries in place and update `_resolvedLabelCount` to
|
|
61708
|
+
// signal how many of them are valid this pass.
|
|
61709
|
+
_resolvedLabels = [];
|
|
61710
|
+
_resolvedLabelCount = 0;
|
|
61711
|
+
_labelBaselineOffset = 0;
|
|
61572
61712
|
get checkForOverflow() {
|
|
61573
61713
|
return this._shouldPaint && this._boundingBox !== undefined;
|
|
61574
61714
|
}
|
|
@@ -61638,16 +61778,88 @@ class TieGlyph extends Glyph {
|
|
|
61638
61778
|
}
|
|
61639
61779
|
this._boundingBox = undefined;
|
|
61640
61780
|
this.y = Math.min(this._startY, this._endY);
|
|
61781
|
+
const down = this.tieDirection === BeamDirection.Down;
|
|
61641
61782
|
let tieBoundingBox;
|
|
61783
|
+
// Bezier control points for the tie. Computed once and reused
|
|
61784
|
+
// for both the bounding box (via _calculateActualTieHeightFromCps)
|
|
61785
|
+
// and label-apex sampling further below — avoids a redundant
|
|
61786
|
+
// call to _computeBezierControlPoints (and its 14-element array
|
|
61787
|
+
// allocation) per labeled slur per layout.
|
|
61788
|
+
let cps = [];
|
|
61642
61789
|
if (this.shouldDrawBendSlur()) {
|
|
61643
61790
|
this._tieHeight = 0;
|
|
61644
|
-
tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY,
|
|
61791
|
+
tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, down, this.renderer.smuflMetrics.tieHeight);
|
|
61645
61792
|
}
|
|
61646
61793
|
else {
|
|
61647
61794
|
this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY);
|
|
61648
|
-
|
|
61795
|
+
const tieThickness = this.renderer.smuflMetrics.tieMidpointThickness;
|
|
61796
|
+
cps = TieGlyph._computeBezierControlPoints(1, this._startX, this._startY, this._endX, this._endY, down, this._tieHeight, tieThickness);
|
|
61797
|
+
tieBoundingBox = TieGlyph._calculateActualTieHeightFromCps(cps, this._startX, this._startY, this._endX, this._endY, down, tieThickness);
|
|
61649
61798
|
}
|
|
61650
61799
|
this._boundingBox = tieBoundingBox;
|
|
61800
|
+
this._resolvedLabelCount = 0;
|
|
61801
|
+
const labels = this.getSlurLabels();
|
|
61802
|
+
if (labels !== null && labels.length > 0 && this.shouldPaintLabels()) {
|
|
61803
|
+
const res = this.renderer.settings.display.resources;
|
|
61804
|
+
const padding = this.renderer.smuflMetrics.oneStaffSpace * 0.25;
|
|
61805
|
+
let maxTextHeight = 0;
|
|
61806
|
+
// Single Y line for all labels — the outer arc apex.
|
|
61807
|
+
// Painted offset adds `padding` on the outward side, so
|
|
61808
|
+
// every label sits the same fixed distance from its arc.
|
|
61809
|
+
const labelLineY = cps.length > 0
|
|
61810
|
+
? 0.125 * cps[7] + 0.375 * cps[9] + 0.375 * cps[11] + 0.125 * cps[13]
|
|
61811
|
+
: (this._startY + this._endY) / 2;
|
|
61812
|
+
for (const label of labels) {
|
|
61813
|
+
const fromX = this.resolveLabelAnchorX(label.fromNote);
|
|
61814
|
+
const toX = this.resolveLabelAnchorX(label.toNote);
|
|
61815
|
+
if (fromX === null || toX === null) {
|
|
61816
|
+
continue;
|
|
61817
|
+
}
|
|
61818
|
+
const midX = (fromX + toX) / 2;
|
|
61819
|
+
if (midX < this._startX || midX > this._endX) {
|
|
61820
|
+
continue;
|
|
61821
|
+
}
|
|
61822
|
+
// Per-element font.size as an upper bound on glyph
|
|
61823
|
+
// height — avoids per-label measureText calls. All H/P
|
|
61824
|
+
// and sl. labels use the same _effectFont, so this is
|
|
61825
|
+
// typically computed once.
|
|
61826
|
+
const font = res.getFontForNotationElement(label.element);
|
|
61827
|
+
if (font.size > maxTextHeight) {
|
|
61828
|
+
maxTextHeight = font.size;
|
|
61829
|
+
}
|
|
61830
|
+
// grow cache lazily; mutate existing slot in place otherwise
|
|
61831
|
+
let slot;
|
|
61832
|
+
if (this._resolvedLabelCount < this._resolvedLabels.length) {
|
|
61833
|
+
slot = this._resolvedLabels[this._resolvedLabelCount];
|
|
61834
|
+
slot.x = midX;
|
|
61835
|
+
slot.y = labelLineY;
|
|
61836
|
+
slot.text = label.text;
|
|
61837
|
+
slot.element = label.element;
|
|
61838
|
+
}
|
|
61839
|
+
else {
|
|
61840
|
+
slot = {
|
|
61841
|
+
x: midX,
|
|
61842
|
+
y: labelLineY,
|
|
61843
|
+
text: label.text,
|
|
61844
|
+
element: label.element
|
|
61845
|
+
};
|
|
61846
|
+
this._resolvedLabels.push(slot);
|
|
61847
|
+
}
|
|
61848
|
+
this._resolvedLabelCount++;
|
|
61849
|
+
}
|
|
61850
|
+
if (this._resolvedLabelCount > 0) {
|
|
61851
|
+
// canvas.textBaseline is 'hanging' (TextBaseline.Top), so
|
|
61852
|
+
// fillText positions `y` at the glyph's top edge.
|
|
61853
|
+
if (this.tieDirection === BeamDirection.Up) {
|
|
61854
|
+
tieBoundingBox.y -= maxTextHeight + padding;
|
|
61855
|
+
this._labelBaselineOffset = -(maxTextHeight + padding);
|
|
61856
|
+
}
|
|
61857
|
+
else {
|
|
61858
|
+
this._labelBaselineOffset = padding;
|
|
61859
|
+
}
|
|
61860
|
+
tieBoundingBox.h += maxTextHeight + padding;
|
|
61861
|
+
}
|
|
61862
|
+
}
|
|
61651
61863
|
this.height = tieBoundingBox.h;
|
|
61652
61864
|
if (this.tieDirection === BeamDirection.Up) {
|
|
61653
61865
|
// the tie might go above `this.y` due to its shape
|
|
@@ -61663,12 +61875,76 @@ class TieGlyph extends Glyph {
|
|
|
61663
61875
|
if (!this._shouldPaint) {
|
|
61664
61876
|
return;
|
|
61665
61877
|
}
|
|
61878
|
+
const isDown = this.tieDirection === BeamDirection.Down;
|
|
61666
61879
|
if (this.shouldDrawBendSlur()) {
|
|
61667
|
-
TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY,
|
|
61880
|
+
TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this.renderer.smuflMetrics.tieHeight);
|
|
61881
|
+
}
|
|
61882
|
+
else {
|
|
61883
|
+
TieGlyph.paintTie(canvas, 1, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
|
|
61884
|
+
}
|
|
61885
|
+
if (this._resolvedLabelCount > 0) {
|
|
61886
|
+
const ta = canvas.textAlign;
|
|
61887
|
+
const tb = canvas.textBaseline;
|
|
61888
|
+
canvas.textAlign = TextAlign.Center;
|
|
61889
|
+
canvas.textBaseline = TextBaseline.Top;
|
|
61890
|
+
const res = this.renderer.resources;
|
|
61891
|
+
let lastElement = -1;
|
|
61892
|
+
for (let i = 0; i < this._resolvedLabelCount; i++) {
|
|
61893
|
+
const label = this._resolvedLabels[i];
|
|
61894
|
+
if (label.element !== lastElement) {
|
|
61895
|
+
canvas.font = res.getFontForNotationElement(label.element);
|
|
61896
|
+
lastElement = label.element;
|
|
61897
|
+
}
|
|
61898
|
+
canvas.fillText(label.text, cx + label.x, cy + label.y + this._labelBaselineOffset);
|
|
61899
|
+
}
|
|
61900
|
+
canvas.textAlign = ta;
|
|
61901
|
+
canvas.textBaseline = tb;
|
|
61902
|
+
}
|
|
61903
|
+
}
|
|
61904
|
+
/**
|
|
61905
|
+
* Returns the labels to paint along this slur, or `null` when there
|
|
61906
|
+
* are none. Override in subclasses.
|
|
61907
|
+
*/
|
|
61908
|
+
getSlurLabels() {
|
|
61909
|
+
return null;
|
|
61910
|
+
}
|
|
61911
|
+
/**
|
|
61912
|
+
* Whether label painting is enabled. Defaults to `true`. Subclasses
|
|
61913
|
+
* may override to disable labels on the bend-slur path or other
|
|
61914
|
+
* special cases.
|
|
61915
|
+
*/
|
|
61916
|
+
shouldPaintLabels() {
|
|
61917
|
+
return !this.shouldDrawBendSlur();
|
|
61918
|
+
}
|
|
61919
|
+
/**
|
|
61920
|
+
* Looks up the absolute X coordinate of an anchor note. Reuses
|
|
61921
|
+
* the start/end bar renderers already resolved by the subclass
|
|
61922
|
+
* (NoteTieGlyph) when the note's bar matches — most labels live
|
|
61923
|
+
* in the slur's start or end bar, so this avoids the double Map
|
|
61924
|
+
* lookup in `getRendererForBar` per label per layout. Returns
|
|
61925
|
+
* `null` when the note's bar is not rendered on this glyph's
|
|
61926
|
+
* staff (cross-system case).
|
|
61927
|
+
*/
|
|
61928
|
+
resolveLabelAnchorX(note) {
|
|
61929
|
+
const bar = note.beat.voice.bar;
|
|
61930
|
+
let renderer = null;
|
|
61931
|
+
const start = this.lookupStartBeatRenderer();
|
|
61932
|
+
if (start !== null && start.bar === bar) {
|
|
61933
|
+
renderer = start;
|
|
61668
61934
|
}
|
|
61669
61935
|
else {
|
|
61670
|
-
|
|
61936
|
+
const end = this.lookupEndBeatRenderer();
|
|
61937
|
+
if (end !== null && end.bar === bar) {
|
|
61938
|
+
renderer = end;
|
|
61939
|
+
}
|
|
61940
|
+
else {
|
|
61941
|
+
renderer = this.renderer.scoreRenderer.layout.getRendererForBar(this.renderer.staff.staffId, bar);
|
|
61942
|
+
}
|
|
61671
61943
|
}
|
|
61944
|
+
if (renderer === null) {
|
|
61945
|
+
return null;
|
|
61946
|
+
}
|
|
61947
|
+
return renderer.x + renderer.getNoteX(note, NoteXPosition.Center);
|
|
61672
61948
|
}
|
|
61673
61949
|
getTieHeight(_startX, _startY, _endX, _endY) {
|
|
61674
61950
|
return this.renderer.smuflMetrics.tieHeight;
|
|
@@ -61688,11 +61964,18 @@ class TieGlyph extends Glyph {
|
|
|
61688
61964
|
}
|
|
61689
61965
|
static calculateActualTieHeight(scale, x1, y1, x2, y2, down, offset, size) {
|
|
61690
61966
|
const cp = TieGlyph._computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size);
|
|
61967
|
+
return TieGlyph._calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size);
|
|
61968
|
+
}
|
|
61969
|
+
/**
|
|
61970
|
+
* Derives the bounding box for a tie from already-computed control
|
|
61971
|
+
* points. Splits the bbox math from cps generation so callers that
|
|
61972
|
+
* need BOTH cps and bbox (e.g. multi-label slur layout) avoid a
|
|
61973
|
+
* second call to `_computeBezierControlPoints`.
|
|
61974
|
+
*/
|
|
61975
|
+
static _calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size) {
|
|
61691
61976
|
if (cp.length === 0) {
|
|
61692
61977
|
return new Bounds(x1, y1, x2 - x1, y2 - y1);
|
|
61693
61978
|
}
|
|
61694
|
-
// For a musical tie/slur, the extrema occur predictably near the midpoint
|
|
61695
|
-
// Evaluate at midpoint (t=0.5) and check endpoints
|
|
61696
61979
|
const p0x = cp[0];
|
|
61697
61980
|
const p0y = cp[1];
|
|
61698
61981
|
const c1x = cp[2];
|
|
@@ -61701,15 +61984,12 @@ class TieGlyph extends Glyph {
|
|
|
61701
61984
|
const c2y = cp[5];
|
|
61702
61985
|
const p1x = cp[6];
|
|
61703
61986
|
const p1y = cp[7];
|
|
61704
|
-
// Evaluate at t=0.5 for midpoint
|
|
61705
61987
|
const midX = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x;
|
|
61706
61988
|
const midY = 0.125 * p0y + 0.375 * c1y + 0.375 * c2y + 0.125 * p1y;
|
|
61707
|
-
// Bounds are simply min/max of start, end, and midpoint
|
|
61708
61989
|
const xMin = Math.min(p0x, p1x, midX);
|
|
61709
61990
|
const xMax = Math.max(p0x, p1x, midX);
|
|
61710
61991
|
let yMin = Math.min(p0y, p1y, midY);
|
|
61711
61992
|
let yMax = Math.max(p0y, p1y, midY);
|
|
61712
|
-
// Account for thickness of the tie/slur
|
|
61713
61993
|
if (down) {
|
|
61714
61994
|
yMax += size;
|
|
61715
61995
|
}
|
|
@@ -68898,11 +69178,43 @@ class TabTieGlyph extends NoteTieGlyph {
|
|
|
68898
69178
|
}
|
|
68899
69179
|
}
|
|
68900
69180
|
|
|
69181
|
+
/**
|
|
69182
|
+
* Helpers for building `TieGlyphLabel` instances from model-side
|
|
69183
|
+
* {@link SlurSegment}s.
|
|
69184
|
+
* @internal
|
|
69185
|
+
*/
|
|
69186
|
+
class TieGlyphLabels {
|
|
69187
|
+
/**
|
|
69188
|
+
* Builds a `TieGlyphLabel` for one segment of a slur. The
|
|
69189
|
+
* `isAscending` flag selects between the H/P glyph for hammer-on
|
|
69190
|
+
* vs. pull-off — score side passes a comparison on `realValue`,
|
|
69191
|
+
* tab side passes a comparison on `fret`.
|
|
69192
|
+
*/
|
|
69193
|
+
static build(s, isAscending) {
|
|
69194
|
+
if (s.kind === SlurSegmentKind.LegatoSlide) {
|
|
69195
|
+
return {
|
|
69196
|
+
fromNote: s.fromNote,
|
|
69197
|
+
toNote: s.toNote,
|
|
69198
|
+
text: s.text !== null ? s.text : 'sl.',
|
|
69199
|
+
element: NotationElement.EffectSlideText
|
|
69200
|
+
};
|
|
69201
|
+
}
|
|
69202
|
+
// HammerPull
|
|
69203
|
+
return {
|
|
69204
|
+
fromNote: s.fromNote,
|
|
69205
|
+
toNote: s.toNote,
|
|
69206
|
+
text: s.text !== null ? s.text : isAscending ? 'H' : 'P',
|
|
69207
|
+
element: NotationElement.EffectHammerOnPullOffText
|
|
69208
|
+
};
|
|
69209
|
+
}
|
|
69210
|
+
}
|
|
69211
|
+
|
|
68901
69212
|
/**
|
|
68902
69213
|
* @internal
|
|
68903
69214
|
*/
|
|
68904
69215
|
class TabSlurGlyph extends TabTieGlyph {
|
|
68905
69216
|
_forSlide;
|
|
69217
|
+
_labels = null;
|
|
68906
69218
|
constructor(slurEffectId, startNote, endNote, forSlide, forEnd) {
|
|
68907
69219
|
super(slurEffectId, startNote, endNote, forEnd);
|
|
68908
69220
|
this._forSlide = forSlide;
|
|
@@ -68910,6 +69222,22 @@ class TabSlurGlyph extends TabTieGlyph {
|
|
|
68910
69222
|
getTieHeight(startX, _startY, endX, _endY) {
|
|
68911
69223
|
return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
|
|
68912
69224
|
}
|
|
69225
|
+
getSlurLabels() {
|
|
69226
|
+
if (this._labels === null) {
|
|
69227
|
+
this._labels = [];
|
|
69228
|
+
const slur = this.startNote.effectSlur;
|
|
69229
|
+
if (slur !== null) {
|
|
69230
|
+
const notationSettings = this.renderer.settings.notation;
|
|
69231
|
+
for (const s of slur.segments) {
|
|
69232
|
+
const label = TieGlyphLabels.build(s, s.toNote.fret >= s.fromNote.fret);
|
|
69233
|
+
if (notationSettings.isNotationElementVisible(label.element)) {
|
|
69234
|
+
this._labels.push(label);
|
|
69235
|
+
}
|
|
69236
|
+
}
|
|
69237
|
+
}
|
|
69238
|
+
}
|
|
69239
|
+
return this._labels.length > 0 ? this._labels : null;
|
|
69240
|
+
}
|
|
68913
69241
|
tryExpand(startNote, endNote, forSlide, forEnd) {
|
|
68914
69242
|
// same type required
|
|
68915
69243
|
if (this._forSlide !== forSlide) {
|
|
@@ -68935,6 +69263,7 @@ class TabSlurGlyph extends TabTieGlyph {
|
|
|
68935
69263
|
case BeamDirection.Up:
|
|
68936
69264
|
if (startNote.realValue > this.startNote.realValue) {
|
|
68937
69265
|
this.startNote = startNote;
|
|
69266
|
+
this._labels = null; // invalidate cache — labels live on startNote
|
|
68938
69267
|
}
|
|
68939
69268
|
if (endNote.realValue > this.endNote.realValue) {
|
|
68940
69269
|
this.endNote = endNote;
|
|
@@ -68943,6 +69272,7 @@ class TabSlurGlyph extends TabTieGlyph {
|
|
|
68943
69272
|
case BeamDirection.Down:
|
|
68944
69273
|
if (startNote.realValue < this.startNote.realValue) {
|
|
68945
69274
|
this.startNote = startNote;
|
|
69275
|
+
this._labels = null;
|
|
68946
69276
|
}
|
|
68947
69277
|
if (endNote.realValue < this.endNote.realValue) {
|
|
68948
69278
|
this.endNote = endNote;
|
|
@@ -73680,9 +74010,26 @@ class ScoreTieGlyph extends NoteTieGlyph {
|
|
|
73680
74010
|
* @internal
|
|
73681
74011
|
*/
|
|
73682
74012
|
class ScoreSlurGlyph extends ScoreTieGlyph {
|
|
74013
|
+
_labels = null;
|
|
73683
74014
|
getTieHeight(startX, _startY, endX, _endY) {
|
|
73684
74015
|
return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
|
|
73685
74016
|
}
|
|
74017
|
+
getSlurLabels() {
|
|
74018
|
+
if (this._labels === null) {
|
|
74019
|
+
this._labels = [];
|
|
74020
|
+
const slur = this.startNote.beat.effectSlur;
|
|
74021
|
+
if (slur !== null) {
|
|
74022
|
+
const notationSettings = this.renderer.settings.notation;
|
|
74023
|
+
for (const s of slur.segments) {
|
|
74024
|
+
const label = TieGlyphLabels.build(s, s.toNote.realValue >= s.fromNote.realValue);
|
|
74025
|
+
if (notationSettings.isNotationElementVisible(label.element)) {
|
|
74026
|
+
this._labels.push(label);
|
|
74027
|
+
}
|
|
74028
|
+
}
|
|
74029
|
+
}
|
|
74030
|
+
}
|
|
74031
|
+
return this._labels.length > 0 ? this._labels : null;
|
|
74032
|
+
}
|
|
73686
74033
|
calculateStartX() {
|
|
73687
74034
|
return (this.renderer.x +
|
|
73688
74035
|
(this._isStartCentered()
|