Skip to content

Commit a1bd11d

Browse files
authored
fix: numbered notation ties and dash placement (#2438)
1 parent b7dacb7 commit a1bd11d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+822
-281
lines changed

packages/alphatab/src/EngravingSettings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ export class EngravingSettings {
454454
//
455455
// custom alphatab sizes
456456
this.numberedBarRendererBarSize = this.staffLineThickness * 2;
457-
this.numberedBarRendererBarSpacing = this.beamSpacing + this.numberedBarRendererBarSize;
457+
this.numberedBarRendererBarSpacing = this.beamSpacing;
458458
this.preNoteEffectPadding = 0.4 * this.oneStaffSpace;
459459
this.postNoteEffectPadding = 0.2 * this.oneStaffSpace;
460460
this.lineRangedGlyphDashGap = 0.5 * this.oneStaffSpace;
@@ -539,7 +539,7 @@ export class EngravingSettings {
539539
public numberedBarRendererBarSpacing = 0;
540540

541541
/**
542-
* The size of the dashed drawn in numbered notation to indicate the durations.
542+
* The padding minimum between the duration dashes.
543543
*/
544544
public numberedDashGlyphPadding = 0;
545545

packages/alphatab/src/Environment.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { LetRingEffectInfo } from '@coderline/alphatab/rendering/effects/LetRing
4141
import { LyricsEffectInfo } from '@coderline/alphatab/rendering/effects/LyricsEffectInfo';
4242
import { MarkerEffectInfo } from '@coderline/alphatab/rendering/effects/MarkerEffectInfo';
4343
import { NoteOrnamentEffectInfo } from '@coderline/alphatab/rendering/effects/NoteOrnamentEffectInfo';
44+
import { NumberedBarKeySignatureEffectInfo } from '@coderline/alphatab/rendering/effects/NumberedBarKeySignatureEffectInfo';
4445
import { OttaviaEffectInfo } from '@coderline/alphatab/rendering/effects/OttaviaEffectInfo';
4546
import { PalmMuteEffectInfo } from '@coderline/alphatab/rendering/effects/PalmMuteEffectInfo';
4647
import { PickSlideEffectInfo } from '@coderline/alphatab/rendering/effects/PickSlideEffectInfo';
@@ -530,7 +531,9 @@ export class Environment {
530531

531532
//
532533
// Numbered
533-
new NumberedBarRendererFactory([]),
534+
new NumberedBarRendererFactory([
535+
{ effect: new NumberedBarKeySignatureEffectInfo(), mode: EffectBandMode.OwnedTop, order: 1000 }
536+
]),
534537

535538
//
536539
// Tabs
@@ -861,5 +864,4 @@ export class Environment {
861864
// so we need to declare this specific helper function and implement it in Kotlin ourselves.
862865
array.sort((a, b) => b - a);
863866
}
864-
865867
}

packages/alphatab/src/NotationSettings.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ export enum NotationElement {
348348
*/
349349
EffectWhammyBarLine = 50,
350350

351+
/**
352+
* The key signature for numbered notation staff.
353+
*/
354+
EffectNumberedNotationKeySignature = 51,
355+
351356
}
352357

353358
/**

packages/alphatab/src/generated/EngravingSettingsJson.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export interface EngravingSettingsJson {
215215
*/
216216
numberedBarRendererBarSpacing?: number;
217217
/**
218-
* The size of the dashed drawn in numbered notation to indicate the durations.
218+
* The padding minimum between the duration dashes.
219219
*/
220220
numberedDashGlyphPadding?: number;
221221
/**

packages/alphatab/src/rendering/BarRendererBase.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { Note } from '@coderline/alphatab/model/Note';
55
import { SimileMark } from '@coderline/alphatab/model/SimileMark';
66
import { type Voice, VoiceSubElement } from '@coderline/alphatab/model/Voice';
77
import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas';
8-
import type { RenderingResources } from '@coderline/alphatab/RenderingResources';
98
import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition';
109
import { EffectBandContainer } from '@coderline/alphatab/rendering/EffectBandContainer';
1110
import {
@@ -28,6 +27,7 @@ import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingH
2827
import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds';
2928
import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper';
3029
import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds';
30+
import type { RenderingResources } from '@coderline/alphatab/RenderingResources';
3131
import type { Settings } from '@coderline/alphatab/Settings';
3232

3333
/**
@@ -677,13 +677,13 @@ export class BarRendererBase {
677677
return this.beatGlyphsStart + this.voiceContainer.getBeatX(beat, requestedPosition, useSharedSizes);
678678
}
679679

680-
public getRatioPositionX(ticks: number): number {
680+
public getRatioPositionX(ratio: number): number {
681681
const firstOnNoteX = this.bar.isEmpty
682682
? this.beatGlyphsStart
683683
: this.getBeatX(this.bar.voices[0].beats[0], BeatXPosition.MiddleNotes);
684684
const x = firstOnNoteX;
685685
const w = this.postBeatGlyphsStart - firstOnNoteX;
686-
return x + w * ticks;
686+
return x + w * ratio;
687687
}
688688

689689
public getNoteX(note: Note, requestedPosition: NoteXPosition): number {

packages/alphatab/src/rendering/LineBarRenderer.ts

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export abstract class LineBarRenderer extends BarRendererBase {
7171

7272
protected updateFirstLineY() {
7373
const fullLineHeight = this.lineOffset * (this.heightLineCount - 1);
74-
const actualLineHeight = (this.drawnLineCount - 1) * this.lineOffset;
74+
const actualLineHeight = this.drawnLineCount === 0 ? 0 : (this.drawnLineCount - 1) * this.lineOffset;
7575
const lineYOffset = this.smuflMetrics.staffLineThickness / 2;
7676

7777
this.firstLineY = (((fullLineHeight - actualLineHeight) / 2) | 0) - lineYOffset;
@@ -471,7 +471,7 @@ export abstract class LineBarRenderer extends BarRendererBase {
471471
beamsElement: BeatSubElement
472472
): void {
473473
canvas.color = h.voice!.index === 0 ? this.resources.mainGlyphColor : this.resources.secondaryGlyphColor;
474-
if (!h.isRestBeamHelper) {
474+
if (this.shouldPaintBeamingHelper(h)) {
475475
if (this.drawBeamHelperAsFlags(h)) {
476476
this.paintFlag(cx, cy, canvas, h, flagsElement);
477477
} else {
@@ -480,8 +480,13 @@ export abstract class LineBarRenderer extends BarRendererBase {
480480
}
481481
}
482482

483+
protected shouldPaintBeamingHelper(h: BeamingHelper) {
484+
return !h.isRestBeamHelper;
485+
}
486+
483487
protected abstract getFlagTopY(beat: Beat, direction: BeamDirection): number;
484488
protected abstract getFlagBottomY(beat: Beat, direction: BeamDirection): number;
489+
485490
protected shouldPaintFlag(beat: Beat): boolean {
486491
// no flags for bend grace beats
487492
if (beat.graceType === GraceType.BendGrace) {
@@ -654,8 +659,8 @@ export abstract class LineBarRenderer extends BarRendererBase {
654659
const direction: BeamDirection = this.getBeamDirection(h);
655660
const isGrace: boolean = h.graceType !== GraceType.None;
656661
const scaleMod: number = isGrace ? EngravingSettings.GraceScale : 1;
657-
let barSpacing: number = (this.smuflMetrics.beamSpacing + this.smuflMetrics.beamThickness) * scaleMod;
658-
let barSize: number = this.smuflMetrics.beamThickness * scaleMod;
662+
let barSpacing: number = (this.beamSpacing + this.beamThickness) * scaleMod;
663+
let barSize: number = this.beamThickness * scaleMod;
659664
if (direction === BeamDirection.Down) {
660665
barSpacing = -barSpacing;
661666
barSize = -barSize;
@@ -833,30 +838,49 @@ export abstract class LineBarRenderer extends BarRendererBase {
833838

834839
for (const v of this.helpers.beamHelpers) {
835840
for (const h of v) {
836-
if (h.isRestBeamHelper) {
837-
// no stems or beams to consider
841+
if (!this.shouldPaintBeamingHelper(h)) {
842+
// no visible helper
838843
}
839844
// notes with stems
840845
else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) {
846+
const tupletDirection = this.getTupletBeamDirection(h);
841847
if (h.direction === BeamDirection.Up) {
842848
let topY = this.getFlagTopY(h.beats[0], h.direction);
843-
if (h.hasTuplet) {
849+
if (h.hasTuplet && tupletDirection === h.direction) {
844850
topY -= this.tupletSize + this.tupletOffset;
845851
}
846852
if (topY < maxNoteY) {
847853
maxNoteY = topY;
848854
}
849855

856+
if (h.hasTuplet && tupletDirection !== h.direction) {
857+
let bottomY = this.getFlagBottomY(h.beats[0], tupletDirection);
858+
bottomY += this.tupletSize + this.tupletOffset;
859+
860+
if (bottomY > minNoteY) {
861+
minNoteY = bottomY;
862+
}
863+
}
864+
850865
// bottom handled via beat container bBox
851866
} else {
852867
let bottomY = this.getFlagBottomY(h.beats[0], h.direction);
853-
if (h.hasTuplet) {
868+
if (h.hasTuplet && tupletDirection === h.direction) {
854869
bottomY += this.tupletSize + this.tupletOffset;
855870
}
856871
if (bottomY > minNoteY) {
857872
minNoteY = bottomY;
858873
}
859874

875+
if (h.hasTuplet && tupletDirection !== h.direction) {
876+
let topY = this.getFlagTopY(h.beats[0], tupletDirection);
877+
topY -= this.tupletSize + this.tupletOffset;
878+
879+
if (topY < maxNoteY) {
880+
maxNoteY = topY;
881+
}
882+
}
883+
860884
// top handled via beat container bBox
861885
}
862886
}
@@ -865,35 +889,43 @@ export abstract class LineBarRenderer extends BarRendererBase {
865889
else {
866890
this.ensureBeamDrawingInfo(h, h.direction);
867891
const drawingInfo = h.drawingInfos.get(h.direction)!;
892+
const tupletDirection = this.getTupletBeamDirection(h);
868893

869894
if (h.direction === BeamDirection.Up) {
870895
let topY = Math.min(drawingInfo.startY, drawingInfo.endY);
871-
if (h.hasTuplet) {
896+
if (h.hasTuplet && tupletDirection === h.direction) {
872897
topY -= this.tupletSize + this.tupletOffset;
873898
}
874899

875900
if (topY < maxNoteY) {
876901
maxNoteY = topY;
877902
}
878903

879-
const bottomY: number =
904+
let bottomY: number =
880905
this.getBarLineStart(h.beatOfLowestNote, h.direction) + noteOverflowPadding;
906+
if (h.hasTuplet && tupletDirection !== h.direction) {
907+
bottomY += this.tupletSize + this.tupletOffset;
908+
}
909+
881910
if (bottomY > minNoteY) {
882911
minNoteY = bottomY;
883912
}
884913
} else {
885914
let bottomY = Math.max(drawingInfo.startY, drawingInfo.endY);
886915

887-
if (h.hasTuplet) {
916+
if (h.hasTuplet && tupletDirection === h.direction) {
888917
bottomY += this.tupletSize + this.tupletOffset;
889918
}
890919

891920
if (bottomY > minNoteY) {
892921
minNoteY = bottomY;
893922
}
894923

895-
const topY: number =
896-
this.getBarLineStart(h.beatOfHighestNote, h.direction) - noteOverflowPadding;
924+
let topY: number = this.getBarLineStart(h.beatOfHighestNote, h.direction) - noteOverflowPadding;
925+
if (h.hasTuplet && tupletDirection !== h.direction) {
926+
topY -= this.tupletSize + this.tupletOffset;
927+
}
928+
897929
if (topY < maxNoteY) {
898930
maxNoteY = topY;
899931
}
@@ -911,27 +943,12 @@ export abstract class LineBarRenderer extends BarRendererBase {
911943
}
912944
}
913945

914-
protected ensureBeamDrawingInfo(h: BeamingHelper, direction: BeamDirection): void {
915-
if (h.drawingInfos.has(direction)) {
916-
return;
917-
}
918-
const scale = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1;
919-
const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2;
920-
946+
protected initializeBeamDrawingInfo(h: BeamingHelper, direction: BeamDirection) {
921947
const drawingInfo = new BeamingHelperDrawInfo();
922-
h.drawingInfos.set(direction, drawingInfo);
923-
924-
// the beaming logic works like this:
925-
// 1. we take the first and last note, add the stem, and put a diagnal line between them.
926-
// 2. the height of the diagonal line must not exceed a max height,
927-
// - if this is the case, the line on the more distant note just gets longer
928-
// 3. any middle elements (notes or rests) shift this diagonal line up/down to avoid overlaps
929948

930949
const firstBeat = h.beats[0];
931950
const lastBeat = h.beats[h.beats.length - 1];
932951

933-
const isRest = h.isRestBeamHelper;
934-
935952
// 1. put direct diagonal line.
936953
drawingInfo.startBeat = firstBeat;
937954
drawingInfo.startX = this.getBeatX(firstBeat, BeatXPosition.Stem);
@@ -980,13 +997,41 @@ export abstract class LineBarRenderer extends BarRendererBase {
980997
drawingInfo.startY = drawingInfo.endY + maxSlope;
981998
}
982999

1000+
return drawingInfo;
1001+
}
1002+
1003+
protected get beamSpacing() {
1004+
return this.smuflMetrics.beamSpacing;
1005+
}
1006+
protected get beamThickness() {
1007+
return this.smuflMetrics.beamThickness;
1008+
}
1009+
1010+
protected ensureBeamDrawingInfo(h: BeamingHelper, direction: BeamDirection): void {
1011+
if (h.drawingInfos.has(direction)) {
1012+
return;
1013+
}
1014+
1015+
// the beaming logic works like this:
1016+
// 1. we take the first and last note, add the stem, and put a diagnal line between them.
1017+
// 2. the height of the diagonal line must not exceed a max height,
1018+
// - if this is the case, the line on the more distant note just gets longer
1019+
// 3. any middle elements (notes or rests) shift this diagonal line up/down to avoid overlaps
1020+
1021+
const drawingInfo = this.initializeBeamDrawingInfo(h, direction);
1022+
h.drawingInfos.set(direction, drawingInfo);
1023+
1024+
const isRest = h.isRestBeamHelper;
1025+
const scale = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1;
1026+
const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2;
1027+
9831028
// 3. adjust beam drawing order
9841029
// we can only draw up to 2 beams towards the noteheads, then we have to grow to the other side
9851030
// here we shift accordingly
9861031
let barDrawingShift = 0;
9871032
if (barCount > 2 && !isRest) {
988-
const beamSpacing = this.smuflMetrics.beamSpacing * scale;
989-
const beamThickness = this.smuflMetrics.beamThickness * scale;
1033+
const beamSpacing = this.beamSpacing * scale;
1034+
const beamThickness = this.beamThickness * scale;
9901035
const totalBarsHeight = barCount * beamThickness + (barCount - 1) * beamSpacing;
9911036

9921037
if (direction === BeamDirection.Up) {
@@ -1038,7 +1083,7 @@ export abstract class LineBarRenderer extends BarRendererBase {
10381083
if (h.restBeats.length > 0) {
10391084
// space needed for the bars, rests need to be below them
10401085
const scaleMod: number = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1;
1041-
barSpacing = barCount * (this.smuflMetrics.beamSpacing + this.smuflMetrics.beamThickness) * scaleMod;
1086+
barSpacing = barCount * (this.beamSpacing + this.beamThickness) * scaleMod;
10421087
}
10431088

10441089
for (const b of h.restBeats) {

packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyphBase {
2323
public override get absoluteDisplayStart(): number {
2424
return this.renderer.bar.masterBar.start;
2525
}
26+
public override get beatId(): number {
27+
return -1;
28+
}
29+
2630
public override get onTimeX(): number {
2731
return 0;
2832
}
@@ -99,7 +103,6 @@ export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyphBase {
99103
case BeatXPosition.OnNotes:
100104
case BeatXPosition.MiddleNotes:
101105
case BeatXPosition.Stem:
102-
return g.x + g.width / 2;
103106
case BeatXPosition.PostNotes:
104107
return g.x + g.width;
105108
case BeatXPosition.EndBeat:

0 commit comments

Comments
 (0)