@coderline/alphatab 1.9.0-alpha.1785 → 1.9.0-alpha.1803
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alphaTab.core.min.mjs +2 -2
- package/dist/alphaTab.core.mjs +387 -45
- package/dist/alphaTab.d.ts +15 -1
- package/dist/alphaTab.js +387 -45
- 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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* alphaTab v1.9.0-alpha.
|
|
2
|
+
* alphaTab v1.9.0-alpha.1803 (develop, build 1803)
|
|
3
3
|
*
|
|
4
4
|
* Copyright © 2026, Daniel Kuschny and Contributors, All rights reserved.
|
|
5
5
|
*
|
|
@@ -209,9 +209,9 @@
|
|
|
209
209
|
* @internal
|
|
210
210
|
*/
|
|
211
211
|
class VersionInfo {
|
|
212
|
-
static version = '1.9.0-alpha.
|
|
213
|
-
static date = '2026-
|
|
214
|
-
static commit = '
|
|
212
|
+
static version = '1.9.0-alpha.1803';
|
|
213
|
+
static date = '2026-05-15T04:13:04.606Z';
|
|
214
|
+
static commit = 'a87a8635a0a0306cdab1c7fe2e9e72576ed6f795';
|
|
215
215
|
static print(print) {
|
|
216
216
|
print(`alphaTab ${VersionInfo.version}`);
|
|
217
217
|
print(`commit: ${VersionInfo.commit}`);
|
|
@@ -998,6 +998,38 @@
|
|
|
998
998
|
SlideOutType[SlideOutType["PickSlideUp"] = 6] = "PickSlideUp";
|
|
999
999
|
})(SlideOutType || (SlideOutType = {}));
|
|
1000
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* A slur arc spanning two notes, optionally with inner articulation
|
|
1003
|
+
* segments. Corresponds conceptually to a MusicXML `<slur>` element
|
|
1004
|
+
* plus the technique spans inside it.
|
|
1005
|
+
*
|
|
1006
|
+
* For this PR only effect slurs (hammer-pull + legato-slide chains)
|
|
1007
|
+
* are derived in `Note.finish()`. Phrase and legato slurs may join
|
|
1008
|
+
* this type in a future PR; a discriminator will be added at that
|
|
1009
|
+
* point.
|
|
1010
|
+
* @internal
|
|
1011
|
+
*/
|
|
1012
|
+
class Slur {
|
|
1013
|
+
originNote;
|
|
1014
|
+
destinationNote;
|
|
1015
|
+
segments = [];
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Articulation kind for an inner span of a {@link Slur}.
|
|
1020
|
+
*
|
|
1021
|
+
* Drives the renderer's font selection (which {@link NotationElement} to
|
|
1022
|
+
* use) and the default label text when {@link SlurSegment.text} is null.
|
|
1023
|
+
* `Note.finish()` classifies the kind once when building the slur; the
|
|
1024
|
+
* renderer never re-derives it.
|
|
1025
|
+
* @internal
|
|
1026
|
+
*/
|
|
1027
|
+
var SlurSegmentKind;
|
|
1028
|
+
(function (SlurSegmentKind) {
|
|
1029
|
+
SlurSegmentKind[SlurSegmentKind["HammerPull"] = 0] = "HammerPull";
|
|
1030
|
+
SlurSegmentKind[SlurSegmentKind["LegatoSlide"] = 1] = "LegatoSlide";
|
|
1031
|
+
})(SlurSegmentKind || (SlurSegmentKind = {}));
|
|
1032
|
+
|
|
1001
1033
|
/**
|
|
1002
1034
|
* This public enum lists all vibrato types that can be performed.
|
|
1003
1035
|
* @public
|
|
@@ -1338,6 +1370,14 @@
|
|
|
1338
1370
|
* The slurs shown on bend effects within the score staff.
|
|
1339
1371
|
*/
|
|
1340
1372
|
NotationElement[NotationElement["ScoreBendSlur"] = 55] = "ScoreBendSlur";
|
|
1373
|
+
/**
|
|
1374
|
+
* The hammer-on pull-off text shown on slurs.
|
|
1375
|
+
*/
|
|
1376
|
+
NotationElement[NotationElement["EffectHammerOnPullOffText"] = 56] = "EffectHammerOnPullOffText";
|
|
1377
|
+
/**
|
|
1378
|
+
* The slide text shown on slurs.
|
|
1379
|
+
*/
|
|
1380
|
+
NotationElement[NotationElement["EffectSlideText"] = 57] = "EffectSlideText";
|
|
1341
1381
|
})(exports.NotationElement || (exports.NotationElement = {}));
|
|
1342
1382
|
/**
|
|
1343
1383
|
* The notation settings control how various music notation elements are shown and behaving
|
|
@@ -6134,6 +6174,16 @@
|
|
|
6134
6174
|
* @json_ignore
|
|
6135
6175
|
*/
|
|
6136
6176
|
effectSlurDestination = null;
|
|
6177
|
+
/**
|
|
6178
|
+
* The {@link Slur} object whose origin is this note. Populated by
|
|
6179
|
+
* `finish()`; non-null only on the chain-origin note of an effect
|
|
6180
|
+
* slur. Carries the inner articulation segments used by the
|
|
6181
|
+
* renderer to paint H/P/sl. labels along the arc.
|
|
6182
|
+
* @clone_ignore
|
|
6183
|
+
* @json_ignore
|
|
6184
|
+
* @internal
|
|
6185
|
+
*/
|
|
6186
|
+
effectSlur = null;
|
|
6137
6187
|
/**
|
|
6138
6188
|
* The ornament applied on the note.
|
|
6139
6189
|
*/
|
|
@@ -6418,23 +6468,50 @@
|
|
|
6418
6468
|
break;
|
|
6419
6469
|
}
|
|
6420
6470
|
let effectSlurDestination = null;
|
|
6471
|
+
let effectSlurSegmentKind = null;
|
|
6421
6472
|
if (this.isHammerPullOrigin && this.hammerPullDestination) {
|
|
6422
6473
|
effectSlurDestination = this.hammerPullDestination;
|
|
6474
|
+
effectSlurSegmentKind = SlurSegmentKind.HammerPull;
|
|
6423
6475
|
}
|
|
6424
6476
|
else if (this.slideOutType === SlideOutType.Legato && this.slideTarget) {
|
|
6425
6477
|
effectSlurDestination = this.slideTarget;
|
|
6478
|
+
effectSlurSegmentKind = SlurSegmentKind.LegatoSlide;
|
|
6426
6479
|
}
|
|
6427
6480
|
if (effectSlurDestination) {
|
|
6428
6481
|
this.hasEffectSlur = true;
|
|
6429
6482
|
if (this.effectSlurOrigin && this.beat.pickStroke === PickStroke.None) {
|
|
6430
|
-
this.effectSlurOrigin
|
|
6431
|
-
|
|
6483
|
+
const chainOrigin = this.effectSlurOrigin;
|
|
6484
|
+
chainOrigin.effectSlurDestination = effectSlurDestination;
|
|
6485
|
+
effectSlurDestination.effectSlurOrigin = chainOrigin;
|
|
6432
6486
|
this.effectSlurOrigin = null;
|
|
6487
|
+
if (effectSlurSegmentKind !== null && chainOrigin.effectSlur !== null) {
|
|
6488
|
+
chainOrigin.effectSlur.destinationNote = effectSlurDestination;
|
|
6489
|
+
chainOrigin.effectSlur.segments.push({
|
|
6490
|
+
fromNote: this,
|
|
6491
|
+
toNote: effectSlurDestination,
|
|
6492
|
+
kind: effectSlurSegmentKind,
|
|
6493
|
+
text: null
|
|
6494
|
+
});
|
|
6495
|
+
}
|
|
6433
6496
|
}
|
|
6434
6497
|
else {
|
|
6435
6498
|
this.isEffectSlurOrigin = true;
|
|
6436
6499
|
this.effectSlurDestination = effectSlurDestination;
|
|
6437
|
-
|
|
6500
|
+
effectSlurDestination.effectSlurOrigin = this;
|
|
6501
|
+
// Always allocate a fresh Slur — finish() may run twice (worker re-finish);
|
|
6502
|
+
// overwriting unconditionally keeps the derivation idempotent.
|
|
6503
|
+
const slur = new Slur();
|
|
6504
|
+
slur.originNote = this;
|
|
6505
|
+
slur.destinationNote = effectSlurDestination;
|
|
6506
|
+
if (effectSlurSegmentKind !== null) {
|
|
6507
|
+
slur.segments.push({
|
|
6508
|
+
fromNote: this,
|
|
6509
|
+
toNote: effectSlurDestination,
|
|
6510
|
+
kind: effectSlurSegmentKind,
|
|
6511
|
+
text: null
|
|
6512
|
+
});
|
|
6513
|
+
}
|
|
6514
|
+
this.effectSlur = slur;
|
|
6438
6515
|
}
|
|
6439
6516
|
}
|
|
6440
6517
|
// try to detect what kind of bend was used and cleans unneeded points if required
|
|
@@ -7684,6 +7761,23 @@
|
|
|
7684
7761
|
* @json_ignore
|
|
7685
7762
|
*/
|
|
7686
7763
|
effectSlurDestination = null;
|
|
7764
|
+
/**
|
|
7765
|
+
* Convenience accessor for the {@link Slur} of this beat. Returns
|
|
7766
|
+
* the effect slur of whichever note in this beat owns it (the
|
|
7767
|
+
* chain-origin note populated during `Note.finish()`), or `null`
|
|
7768
|
+
* when no note in the beat is an effect-slur origin.
|
|
7769
|
+
* @clone_ignore
|
|
7770
|
+
* @json_ignore
|
|
7771
|
+
* @internal
|
|
7772
|
+
*/
|
|
7773
|
+
get effectSlur() {
|
|
7774
|
+
for (const n of this.notes) {
|
|
7775
|
+
if (n.effectSlur !== null) {
|
|
7776
|
+
return n.effectSlur;
|
|
7777
|
+
}
|
|
7778
|
+
}
|
|
7779
|
+
return null;
|
|
7780
|
+
}
|
|
7687
7781
|
/**
|
|
7688
7782
|
* Gets or sets how the beaming should be done for this beat.
|
|
7689
7783
|
*/
|
|
@@ -31948,7 +32042,9 @@
|
|
|
31948
32042
|
[exports.NotationElement.RepeatCount, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
|
|
31949
32043
|
[exports.NotationElement.BarNumber, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
|
|
31950
32044
|
[exports.NotationElement.ScoreBendSlur, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
|
|
31951
|
-
[exports.NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)]
|
|
32045
|
+
[exports.NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)],
|
|
32046
|
+
[exports.NotationElement.EffectHammerOnPullOffText, RenderingResources._effectFont],
|
|
32047
|
+
[exports.NotationElement.EffectSlideText, RenderingResources._effectFont]
|
|
31952
32048
|
]);
|
|
31953
32049
|
/**
|
|
31954
32050
|
* The name of the SMuFL Font to use for rendering music symbols.
|
|
@@ -32239,9 +32335,16 @@
|
|
|
32239
32335
|
notationElement = exports.NotationElement.ScoreWords;
|
|
32240
32336
|
break;
|
|
32241
32337
|
}
|
|
32338
|
+
return this.getFontForNotationElement(notationElement);
|
|
32339
|
+
}
|
|
32340
|
+
/**
|
|
32341
|
+
* @internal
|
|
32342
|
+
* @param element
|
|
32343
|
+
*/
|
|
32344
|
+
getFontForNotationElement(notationElement) {
|
|
32242
32345
|
return this.elementFonts.has(notationElement)
|
|
32243
32346
|
? this.elementFonts.get(notationElement)
|
|
32244
|
-
: RenderingResources.defaultFonts.get(
|
|
32347
|
+
: RenderingResources.defaultFonts.get(notationElement);
|
|
32245
32348
|
}
|
|
32246
32349
|
}
|
|
32247
32350
|
|
|
@@ -49167,6 +49270,21 @@
|
|
|
49167
49270
|
}
|
|
49168
49271
|
}
|
|
49169
49272
|
|
|
49273
|
+
/**
|
|
49274
|
+
* Represents a range of the song that should be played.
|
|
49275
|
+
* @public
|
|
49276
|
+
*/
|
|
49277
|
+
class PlaybackRange {
|
|
49278
|
+
/**
|
|
49279
|
+
* The position in midi ticks from where the song should start.
|
|
49280
|
+
*/
|
|
49281
|
+
startTick = 0;
|
|
49282
|
+
/**
|
|
49283
|
+
* The position in midi ticks to where the song should be played.
|
|
49284
|
+
*/
|
|
49285
|
+
endTick = 0;
|
|
49286
|
+
}
|
|
49287
|
+
|
|
49170
49288
|
/**
|
|
49171
49289
|
* Describes how a cursor should be moving.
|
|
49172
49290
|
* @public
|
|
@@ -49327,6 +49445,13 @@
|
|
|
49327
49445
|
* @internal
|
|
49328
49446
|
*/
|
|
49329
49447
|
masterBarLookup = new Map();
|
|
49448
|
+
/**
|
|
49449
|
+
* A dictionary of all beat played. The index is the id to {@link Beat.id}.
|
|
49450
|
+
* The value is the bar relative tick time at which the beat was registered during midi generation.
|
|
49451
|
+
* This lookup only contains the first time a Beat is played.
|
|
49452
|
+
* @internal
|
|
49453
|
+
*/
|
|
49454
|
+
beatLookup = new Map();
|
|
49330
49455
|
/**
|
|
49331
49456
|
* A list of all {@link MasterBarTickLookup} sorted by time.
|
|
49332
49457
|
*/
|
|
@@ -49693,10 +49818,22 @@
|
|
|
49693
49818
|
* @returns The time in midi ticks at which the beat is played the first time or 0 if the beat is not contained
|
|
49694
49819
|
*/
|
|
49695
49820
|
getBeatStart(beat) {
|
|
49696
|
-
if (!this.masterBarLookup.has(beat.voice.bar.index)) {
|
|
49821
|
+
if (!this.masterBarLookup.has(beat.voice.bar.index) || !this.beatLookup.has(beat.id)) {
|
|
49697
49822
|
return 0;
|
|
49698
49823
|
}
|
|
49699
|
-
|
|
49824
|
+
const mb = this.masterBarLookup.get(beat.voice.bar.index);
|
|
49825
|
+
return mb.start + this.beatLookup.get(beat.id).startTick;
|
|
49826
|
+
}
|
|
49827
|
+
/**
|
|
49828
|
+
* Gets the playback range in midi ticks for a given beat.
|
|
49829
|
+
* @param beat The beat to find the time period for.
|
|
49830
|
+
* @returns The relative playback range within the parent masterbar at which the beat start and ends playing
|
|
49831
|
+
*/
|
|
49832
|
+
getRelativeBeatPlaybackRange(beat) {
|
|
49833
|
+
if (!this.beatLookup.has(beat.id)) {
|
|
49834
|
+
return undefined;
|
|
49835
|
+
}
|
|
49836
|
+
return this.beatLookup.get(beat.id);
|
|
49700
49837
|
}
|
|
49701
49838
|
/**
|
|
49702
49839
|
* Adds a new {@link MasterBarTickLookup} to the lookup table.
|
|
@@ -49714,6 +49851,12 @@
|
|
|
49714
49851
|
}
|
|
49715
49852
|
}
|
|
49716
49853
|
addBeat(beat, start, duration) {
|
|
49854
|
+
if (!this.beatLookup.has(beat.id)) {
|
|
49855
|
+
const playbackRange = new PlaybackRange();
|
|
49856
|
+
playbackRange.startTick = start;
|
|
49857
|
+
playbackRange.endTick = start + duration;
|
|
49858
|
+
this.beatLookup.set(beat.id, playbackRange);
|
|
49859
|
+
}
|
|
49717
49860
|
const currentMasterBar = this._currentMasterBar;
|
|
49718
49861
|
if (currentMasterBar) {
|
|
49719
49862
|
// pre-beat grace notes at the start of the bar we also add the beat to the previous bar
|
|
@@ -52297,21 +52440,6 @@
|
|
|
52297
52440
|
}
|
|
52298
52441
|
}
|
|
52299
52442
|
|
|
52300
|
-
/**
|
|
52301
|
-
* Represents a range of the song that should be played.
|
|
52302
|
-
* @public
|
|
52303
|
-
*/
|
|
52304
|
-
class PlaybackRange {
|
|
52305
|
-
/**
|
|
52306
|
-
* The position in midi ticks from where the song should start.
|
|
52307
|
-
*/
|
|
52308
|
-
startTick = 0;
|
|
52309
|
-
/**
|
|
52310
|
-
* The position in midi ticks to where the song should be played.
|
|
52311
|
-
*/
|
|
52312
|
-
endTick = 0;
|
|
52313
|
-
}
|
|
52314
|
-
|
|
52315
52443
|
/**
|
|
52316
52444
|
* A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
|
|
52317
52445
|
* allowing dynamic changing of the underlying instance without loosing aspects like the
|
|
@@ -55512,23 +55640,24 @@
|
|
|
55512
55640
|
if (this._selectionStart && this._tickCache) {
|
|
55513
55641
|
// get the start and stop ticks (which consider properly repeats)
|
|
55514
55642
|
const tickCache = this._tickCache;
|
|
55515
|
-
const
|
|
55643
|
+
const realStartMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
|
|
55644
|
+
const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionStart.beat);
|
|
55645
|
+
const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? this._selectionStart.beat.playbackStart;
|
|
55516
55646
|
// move to selection start
|
|
55517
55647
|
this._currentBeat = null; // reset current beat so it is updating the cursor
|
|
55518
55648
|
if (this._player.state === PlayerState.Paused) {
|
|
55519
|
-
this._cursorUpdateTick(
|
|
55649
|
+
this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1);
|
|
55520
55650
|
}
|
|
55521
|
-
this.tickPosition =
|
|
55651
|
+
this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart;
|
|
55522
55652
|
// set playback range
|
|
55523
55653
|
if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) {
|
|
55524
|
-
const
|
|
55654
|
+
const realEndMasterBarStart = tickCache.getMasterBarStart(this._selectionEnd.beat.voice.bar.masterBar);
|
|
55655
|
+
const endBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionEnd.beat);
|
|
55656
|
+
const endBeatPlaybackEnd = endBeatPlaybackRange?.endTick ??
|
|
55657
|
+
this._selectionEnd.beat.playbackStart + this._selectionEnd.beat.playbackDuration;
|
|
55525
55658
|
const range = new PlaybackRange();
|
|
55526
|
-
range.startTick =
|
|
55527
|
-
range.endTick =
|
|
55528
|
-
realMasterBarEnd +
|
|
55529
|
-
this._selectionEnd.beat.playbackStart +
|
|
55530
|
-
this._selectionEnd.beat.playbackDuration -
|
|
55531
|
-
50;
|
|
55659
|
+
range.startTick = realStartMasterBarStart + startBeatPlaybackStart;
|
|
55660
|
+
range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50;
|
|
55532
55661
|
this.playbackRange = range;
|
|
55533
55662
|
}
|
|
55534
55663
|
else {
|
|
@@ -61575,6 +61704,12 @@
|
|
|
61575
61704
|
_tieHeight = 0;
|
|
61576
61705
|
_boundingBox;
|
|
61577
61706
|
_shouldPaint = false;
|
|
61707
|
+
// Resolved per-label paint state. Lazily grown; re-layouts mutate
|
|
61708
|
+
// existing entries in place and update `_resolvedLabelCount` to
|
|
61709
|
+
// signal how many of them are valid this pass.
|
|
61710
|
+
_resolvedLabels = [];
|
|
61711
|
+
_resolvedLabelCount = 0;
|
|
61712
|
+
_labelBaselineOffset = 0;
|
|
61578
61713
|
get checkForOverflow() {
|
|
61579
61714
|
return this._shouldPaint && this._boundingBox !== undefined;
|
|
61580
61715
|
}
|
|
@@ -61644,16 +61779,88 @@
|
|
|
61644
61779
|
}
|
|
61645
61780
|
this._boundingBox = undefined;
|
|
61646
61781
|
this.y = Math.min(this._startY, this._endY);
|
|
61782
|
+
const down = this.tieDirection === BeamDirection.Down;
|
|
61647
61783
|
let tieBoundingBox;
|
|
61784
|
+
// Bezier control points for the tie. Computed once and reused
|
|
61785
|
+
// for both the bounding box (via _calculateActualTieHeightFromCps)
|
|
61786
|
+
// and label-apex sampling further below — avoids a redundant
|
|
61787
|
+
// call to _computeBezierControlPoints (and its 14-element array
|
|
61788
|
+
// allocation) per labeled slur per layout.
|
|
61789
|
+
let cps = [];
|
|
61648
61790
|
if (this.shouldDrawBendSlur()) {
|
|
61649
61791
|
this._tieHeight = 0;
|
|
61650
|
-
tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY,
|
|
61792
|
+
tieBoundingBox = TieGlyph.calculateBendSlurHeight(this._startX, this._startY, this._endX, this._endY, down, this.renderer.smuflMetrics.tieHeight);
|
|
61651
61793
|
}
|
|
61652
61794
|
else {
|
|
61653
61795
|
this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY);
|
|
61654
|
-
|
|
61796
|
+
const tieThickness = this.renderer.smuflMetrics.tieMidpointThickness;
|
|
61797
|
+
cps = TieGlyph._computeBezierControlPoints(1, this._startX, this._startY, this._endX, this._endY, down, this._tieHeight, tieThickness);
|
|
61798
|
+
tieBoundingBox = TieGlyph._calculateActualTieHeightFromCps(cps, this._startX, this._startY, this._endX, this._endY, down, tieThickness);
|
|
61655
61799
|
}
|
|
61656
61800
|
this._boundingBox = tieBoundingBox;
|
|
61801
|
+
this._resolvedLabelCount = 0;
|
|
61802
|
+
const labels = this.getSlurLabels();
|
|
61803
|
+
if (labels !== null && labels.length > 0 && this.shouldPaintLabels()) {
|
|
61804
|
+
const res = this.renderer.settings.display.resources;
|
|
61805
|
+
const padding = this.renderer.smuflMetrics.oneStaffSpace * 0.25;
|
|
61806
|
+
let maxTextHeight = 0;
|
|
61807
|
+
// Single Y line for all labels — the outer arc apex.
|
|
61808
|
+
// Painted offset adds `padding` on the outward side, so
|
|
61809
|
+
// every label sits the same fixed distance from its arc.
|
|
61810
|
+
const labelLineY = cps.length > 0
|
|
61811
|
+
? 0.125 * cps[7] + 0.375 * cps[9] + 0.375 * cps[11] + 0.125 * cps[13]
|
|
61812
|
+
: (this._startY + this._endY) / 2;
|
|
61813
|
+
for (const label of labels) {
|
|
61814
|
+
const fromX = this.resolveLabelAnchorX(label.fromNote);
|
|
61815
|
+
const toX = this.resolveLabelAnchorX(label.toNote);
|
|
61816
|
+
if (fromX === null || toX === null) {
|
|
61817
|
+
continue;
|
|
61818
|
+
}
|
|
61819
|
+
const midX = (fromX + toX) / 2;
|
|
61820
|
+
if (midX < this._startX || midX > this._endX) {
|
|
61821
|
+
continue;
|
|
61822
|
+
}
|
|
61823
|
+
// Per-element font.size as an upper bound on glyph
|
|
61824
|
+
// height — avoids per-label measureText calls. All H/P
|
|
61825
|
+
// and sl. labels use the same _effectFont, so this is
|
|
61826
|
+
// typically computed once.
|
|
61827
|
+
const font = res.getFontForNotationElement(label.element);
|
|
61828
|
+
if (font.size > maxTextHeight) {
|
|
61829
|
+
maxTextHeight = font.size;
|
|
61830
|
+
}
|
|
61831
|
+
// grow cache lazily; mutate existing slot in place otherwise
|
|
61832
|
+
let slot;
|
|
61833
|
+
if (this._resolvedLabelCount < this._resolvedLabels.length) {
|
|
61834
|
+
slot = this._resolvedLabels[this._resolvedLabelCount];
|
|
61835
|
+
slot.x = midX;
|
|
61836
|
+
slot.y = labelLineY;
|
|
61837
|
+
slot.text = label.text;
|
|
61838
|
+
slot.element = label.element;
|
|
61839
|
+
}
|
|
61840
|
+
else {
|
|
61841
|
+
slot = {
|
|
61842
|
+
x: midX,
|
|
61843
|
+
y: labelLineY,
|
|
61844
|
+
text: label.text,
|
|
61845
|
+
element: label.element
|
|
61846
|
+
};
|
|
61847
|
+
this._resolvedLabels.push(slot);
|
|
61848
|
+
}
|
|
61849
|
+
this._resolvedLabelCount++;
|
|
61850
|
+
}
|
|
61851
|
+
if (this._resolvedLabelCount > 0) {
|
|
61852
|
+
// canvas.textBaseline is 'hanging' (TextBaseline.Top), so
|
|
61853
|
+
// fillText positions `y` at the glyph's top edge.
|
|
61854
|
+
if (this.tieDirection === BeamDirection.Up) {
|
|
61855
|
+
tieBoundingBox.y -= maxTextHeight + padding;
|
|
61856
|
+
this._labelBaselineOffset = -(maxTextHeight + padding);
|
|
61857
|
+
}
|
|
61858
|
+
else {
|
|
61859
|
+
this._labelBaselineOffset = padding;
|
|
61860
|
+
}
|
|
61861
|
+
tieBoundingBox.h += maxTextHeight + padding;
|
|
61862
|
+
}
|
|
61863
|
+
}
|
|
61657
61864
|
this.height = tieBoundingBox.h;
|
|
61658
61865
|
if (this.tieDirection === BeamDirection.Up) {
|
|
61659
61866
|
// the tie might go above `this.y` due to its shape
|
|
@@ -61669,12 +61876,76 @@
|
|
|
61669
61876
|
if (!this._shouldPaint) {
|
|
61670
61877
|
return;
|
|
61671
61878
|
}
|
|
61879
|
+
const isDown = this.tieDirection === BeamDirection.Down;
|
|
61672
61880
|
if (this.shouldDrawBendSlur()) {
|
|
61673
|
-
TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY,
|
|
61881
|
+
TieGlyph.drawBendSlur(canvas, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this.renderer.smuflMetrics.tieHeight);
|
|
61882
|
+
}
|
|
61883
|
+
else {
|
|
61884
|
+
TieGlyph.paintTie(canvas, 1, cx + this._startX, cy + this._startY, cx + this._endX, cy + this._endY, isDown, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness);
|
|
61885
|
+
}
|
|
61886
|
+
if (this._resolvedLabelCount > 0) {
|
|
61887
|
+
const ta = canvas.textAlign;
|
|
61888
|
+
const tb = canvas.textBaseline;
|
|
61889
|
+
canvas.textAlign = TextAlign.Center;
|
|
61890
|
+
canvas.textBaseline = TextBaseline.Top;
|
|
61891
|
+
const res = this.renderer.resources;
|
|
61892
|
+
let lastElement = -1;
|
|
61893
|
+
for (let i = 0; i < this._resolvedLabelCount; i++) {
|
|
61894
|
+
const label = this._resolvedLabels[i];
|
|
61895
|
+
if (label.element !== lastElement) {
|
|
61896
|
+
canvas.font = res.getFontForNotationElement(label.element);
|
|
61897
|
+
lastElement = label.element;
|
|
61898
|
+
}
|
|
61899
|
+
canvas.fillText(label.text, cx + label.x, cy + label.y + this._labelBaselineOffset);
|
|
61900
|
+
}
|
|
61901
|
+
canvas.textAlign = ta;
|
|
61902
|
+
canvas.textBaseline = tb;
|
|
61903
|
+
}
|
|
61904
|
+
}
|
|
61905
|
+
/**
|
|
61906
|
+
* Returns the labels to paint along this slur, or `null` when there
|
|
61907
|
+
* are none. Override in subclasses.
|
|
61908
|
+
*/
|
|
61909
|
+
getSlurLabels() {
|
|
61910
|
+
return null;
|
|
61911
|
+
}
|
|
61912
|
+
/**
|
|
61913
|
+
* Whether label painting is enabled. Defaults to `true`. Subclasses
|
|
61914
|
+
* may override to disable labels on the bend-slur path or other
|
|
61915
|
+
* special cases.
|
|
61916
|
+
*/
|
|
61917
|
+
shouldPaintLabels() {
|
|
61918
|
+
return !this.shouldDrawBendSlur();
|
|
61919
|
+
}
|
|
61920
|
+
/**
|
|
61921
|
+
* Looks up the absolute X coordinate of an anchor note. Reuses
|
|
61922
|
+
* the start/end bar renderers already resolved by the subclass
|
|
61923
|
+
* (NoteTieGlyph) when the note's bar matches — most labels live
|
|
61924
|
+
* in the slur's start or end bar, so this avoids the double Map
|
|
61925
|
+
* lookup in `getRendererForBar` per label per layout. Returns
|
|
61926
|
+
* `null` when the note's bar is not rendered on this glyph's
|
|
61927
|
+
* staff (cross-system case).
|
|
61928
|
+
*/
|
|
61929
|
+
resolveLabelAnchorX(note) {
|
|
61930
|
+
const bar = note.beat.voice.bar;
|
|
61931
|
+
let renderer = null;
|
|
61932
|
+
const start = this.lookupStartBeatRenderer();
|
|
61933
|
+
if (start !== null && start.bar === bar) {
|
|
61934
|
+
renderer = start;
|
|
61674
61935
|
}
|
|
61675
61936
|
else {
|
|
61676
|
-
|
|
61937
|
+
const end = this.lookupEndBeatRenderer();
|
|
61938
|
+
if (end !== null && end.bar === bar) {
|
|
61939
|
+
renderer = end;
|
|
61940
|
+
}
|
|
61941
|
+
else {
|
|
61942
|
+
renderer = this.renderer.scoreRenderer.layout.getRendererForBar(this.renderer.staff.staffId, bar);
|
|
61943
|
+
}
|
|
61944
|
+
}
|
|
61945
|
+
if (renderer === null) {
|
|
61946
|
+
return null;
|
|
61677
61947
|
}
|
|
61948
|
+
return renderer.x + renderer.getNoteX(note, NoteXPosition.Center);
|
|
61678
61949
|
}
|
|
61679
61950
|
getTieHeight(_startX, _startY, _endX, _endY) {
|
|
61680
61951
|
return this.renderer.smuflMetrics.tieHeight;
|
|
@@ -61694,11 +61965,18 @@
|
|
|
61694
61965
|
}
|
|
61695
61966
|
static calculateActualTieHeight(scale, x1, y1, x2, y2, down, offset, size) {
|
|
61696
61967
|
const cp = TieGlyph._computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size);
|
|
61968
|
+
return TieGlyph._calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size);
|
|
61969
|
+
}
|
|
61970
|
+
/**
|
|
61971
|
+
* Derives the bounding box for a tie from already-computed control
|
|
61972
|
+
* points. Splits the bbox math from cps generation so callers that
|
|
61973
|
+
* need BOTH cps and bbox (e.g. multi-label slur layout) avoid a
|
|
61974
|
+
* second call to `_computeBezierControlPoints`.
|
|
61975
|
+
*/
|
|
61976
|
+
static _calculateActualTieHeightFromCps(cp, x1, y1, x2, y2, down, size) {
|
|
61697
61977
|
if (cp.length === 0) {
|
|
61698
61978
|
return new Bounds(x1, y1, x2 - x1, y2 - y1);
|
|
61699
61979
|
}
|
|
61700
|
-
// For a musical tie/slur, the extrema occur predictably near the midpoint
|
|
61701
|
-
// Evaluate at midpoint (t=0.5) and check endpoints
|
|
61702
61980
|
const p0x = cp[0];
|
|
61703
61981
|
const p0y = cp[1];
|
|
61704
61982
|
const c1x = cp[2];
|
|
@@ -61707,15 +61985,12 @@
|
|
|
61707
61985
|
const c2y = cp[5];
|
|
61708
61986
|
const p1x = cp[6];
|
|
61709
61987
|
const p1y = cp[7];
|
|
61710
|
-
// Evaluate at t=0.5 for midpoint
|
|
61711
61988
|
const midX = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x;
|
|
61712
61989
|
const midY = 0.125 * p0y + 0.375 * c1y + 0.375 * c2y + 0.125 * p1y;
|
|
61713
|
-
// Bounds are simply min/max of start, end, and midpoint
|
|
61714
61990
|
const xMin = Math.min(p0x, p1x, midX);
|
|
61715
61991
|
const xMax = Math.max(p0x, p1x, midX);
|
|
61716
61992
|
let yMin = Math.min(p0y, p1y, midY);
|
|
61717
61993
|
let yMax = Math.max(p0y, p1y, midY);
|
|
61718
|
-
// Account for thickness of the tie/slur
|
|
61719
61994
|
if (down) {
|
|
61720
61995
|
yMax += size;
|
|
61721
61996
|
}
|
|
@@ -68904,11 +69179,43 @@
|
|
|
68904
69179
|
}
|
|
68905
69180
|
}
|
|
68906
69181
|
|
|
69182
|
+
/**
|
|
69183
|
+
* Helpers for building `TieGlyphLabel` instances from model-side
|
|
69184
|
+
* {@link SlurSegment}s.
|
|
69185
|
+
* @internal
|
|
69186
|
+
*/
|
|
69187
|
+
class TieGlyphLabels {
|
|
69188
|
+
/**
|
|
69189
|
+
* Builds a `TieGlyphLabel` for one segment of a slur. The
|
|
69190
|
+
* `isAscending` flag selects between the H/P glyph for hammer-on
|
|
69191
|
+
* vs. pull-off — score side passes a comparison on `realValue`,
|
|
69192
|
+
* tab side passes a comparison on `fret`.
|
|
69193
|
+
*/
|
|
69194
|
+
static build(s, isAscending) {
|
|
69195
|
+
if (s.kind === SlurSegmentKind.LegatoSlide) {
|
|
69196
|
+
return {
|
|
69197
|
+
fromNote: s.fromNote,
|
|
69198
|
+
toNote: s.toNote,
|
|
69199
|
+
text: s.text !== null ? s.text : 'sl.',
|
|
69200
|
+
element: exports.NotationElement.EffectSlideText
|
|
69201
|
+
};
|
|
69202
|
+
}
|
|
69203
|
+
// HammerPull
|
|
69204
|
+
return {
|
|
69205
|
+
fromNote: s.fromNote,
|
|
69206
|
+
toNote: s.toNote,
|
|
69207
|
+
text: s.text !== null ? s.text : isAscending ? 'H' : 'P',
|
|
69208
|
+
element: exports.NotationElement.EffectHammerOnPullOffText
|
|
69209
|
+
};
|
|
69210
|
+
}
|
|
69211
|
+
}
|
|
69212
|
+
|
|
68907
69213
|
/**
|
|
68908
69214
|
* @internal
|
|
68909
69215
|
*/
|
|
68910
69216
|
class TabSlurGlyph extends TabTieGlyph {
|
|
68911
69217
|
_forSlide;
|
|
69218
|
+
_labels = null;
|
|
68912
69219
|
constructor(slurEffectId, startNote, endNote, forSlide, forEnd) {
|
|
68913
69220
|
super(slurEffectId, startNote, endNote, forEnd);
|
|
68914
69221
|
this._forSlide = forSlide;
|
|
@@ -68916,6 +69223,22 @@
|
|
|
68916
69223
|
getTieHeight(startX, _startY, endX, _endY) {
|
|
68917
69224
|
return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
|
|
68918
69225
|
}
|
|
69226
|
+
getSlurLabels() {
|
|
69227
|
+
if (this._labels === null) {
|
|
69228
|
+
this._labels = [];
|
|
69229
|
+
const slur = this.startNote.effectSlur;
|
|
69230
|
+
if (slur !== null) {
|
|
69231
|
+
const notationSettings = this.renderer.settings.notation;
|
|
69232
|
+
for (const s of slur.segments) {
|
|
69233
|
+
const label = TieGlyphLabels.build(s, s.toNote.fret >= s.fromNote.fret);
|
|
69234
|
+
if (notationSettings.isNotationElementVisible(label.element)) {
|
|
69235
|
+
this._labels.push(label);
|
|
69236
|
+
}
|
|
69237
|
+
}
|
|
69238
|
+
}
|
|
69239
|
+
}
|
|
69240
|
+
return this._labels.length > 0 ? this._labels : null;
|
|
69241
|
+
}
|
|
68919
69242
|
tryExpand(startNote, endNote, forSlide, forEnd) {
|
|
68920
69243
|
// same type required
|
|
68921
69244
|
if (this._forSlide !== forSlide) {
|
|
@@ -68941,6 +69264,7 @@
|
|
|
68941
69264
|
case BeamDirection.Up:
|
|
68942
69265
|
if (startNote.realValue > this.startNote.realValue) {
|
|
68943
69266
|
this.startNote = startNote;
|
|
69267
|
+
this._labels = null; // invalidate cache — labels live on startNote
|
|
68944
69268
|
}
|
|
68945
69269
|
if (endNote.realValue > this.endNote.realValue) {
|
|
68946
69270
|
this.endNote = endNote;
|
|
@@ -68949,6 +69273,7 @@
|
|
|
68949
69273
|
case BeamDirection.Down:
|
|
68950
69274
|
if (startNote.realValue < this.startNote.realValue) {
|
|
68951
69275
|
this.startNote = startNote;
|
|
69276
|
+
this._labels = null;
|
|
68952
69277
|
}
|
|
68953
69278
|
if (endNote.realValue < this.endNote.realValue) {
|
|
68954
69279
|
this.endNote = endNote;
|
|
@@ -73686,9 +74011,26 @@
|
|
|
73686
74011
|
* @internal
|
|
73687
74012
|
*/
|
|
73688
74013
|
class ScoreSlurGlyph extends ScoreTieGlyph {
|
|
74014
|
+
_labels = null;
|
|
73689
74015
|
getTieHeight(startX, _startY, endX, _endY) {
|
|
73690
74016
|
return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2;
|
|
73691
74017
|
}
|
|
74018
|
+
getSlurLabels() {
|
|
74019
|
+
if (this._labels === null) {
|
|
74020
|
+
this._labels = [];
|
|
74021
|
+
const slur = this.startNote.beat.effectSlur;
|
|
74022
|
+
if (slur !== null) {
|
|
74023
|
+
const notationSettings = this.renderer.settings.notation;
|
|
74024
|
+
for (const s of slur.segments) {
|
|
74025
|
+
const label = TieGlyphLabels.build(s, s.toNote.realValue >= s.fromNote.realValue);
|
|
74026
|
+
if (notationSettings.isNotationElementVisible(label.element)) {
|
|
74027
|
+
this._labels.push(label);
|
|
74028
|
+
}
|
|
74029
|
+
}
|
|
74030
|
+
}
|
|
74031
|
+
}
|
|
74032
|
+
return this._labels.length > 0 ? this._labels : null;
|
|
74033
|
+
}
|
|
73692
74034
|
calculateStartX() {
|
|
73693
74035
|
return (this.renderer.x +
|
|
73694
74036
|
(this._isStartCentered()
|