diff --git a/packages/alphatab/src/rendering/BarRendererBase.ts b/packages/alphatab/src/rendering/BarRendererBase.ts index 9bc6f7c05..5c96c5a4a 100644 --- a/packages/alphatab/src/rendering/BarRendererBase.ts +++ b/packages/alphatab/src/rendering/BarRendererBase.ts @@ -12,7 +12,7 @@ import type { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGly import type { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { LeftToRightLayoutingGlyphGroup } from '@coderline/alphatab/rendering/glyphs/LeftToRightLayoutingGlyphGroup'; -import type { ITieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { ContinuationTieGlyph, type ITieGlyph, type TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import { VoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/VoiceContainerGlyph'; import { InternalSystemsLayoutMode } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { MultiBarRestBeatContainerGlyph } from '@coderline/alphatab/rendering/MultiBarRestBeatContainerGlyph'; @@ -94,6 +94,8 @@ export class BarRendererBase { private _ties: ITieGlyph[] = []; + private _multiSystemSlurs?: ContinuationTieGlyph[]; + public topEffects: EffectBandContainer; public bottomEffects: EffectBandContainer; @@ -101,18 +103,18 @@ export class BarRendererBase { if (!this.bar || !this.bar.nextBar) { return null; } - return this.scoreRenderer.layout!.getRendererForBar(this.staff.staffId, this.bar.nextBar); + return this.scoreRenderer.layout!.getRendererForBar(this.staff!.staffId, this.bar.nextBar); } public get previousRenderer(): BarRendererBase | null { if (!this.bar || !this.bar.previousBar) { return null; } - return this.scoreRenderer.layout!.getRendererForBar(this.staff.staffId, this.bar.previousBar); + return this.scoreRenderer.layout!.getRendererForBar(this.staff!.staffId, this.bar.previousBar); } public scoreRenderer: ScoreRenderer; - public staff!: RenderStaff; + public staff?: RenderStaff; public layoutingInfo!: BarLayoutingInfo; public bar: Bar; public additionalMultiRestBars: Bar[] | null = null; @@ -239,7 +241,7 @@ export class BarRendererBase { * scale should be respected. */ public get barDisplayScale(): number { - return this.staff.system.staves.length > 1 ? this.bar.masterBar.displayScale : this.bar.displayScale; + return this.staff!.system.staves.length > 1 ? this.bar.masterBar.displayScale : this.bar.displayScale; } /** @@ -247,7 +249,7 @@ export class BarRendererBase { * scale should be respected. */ public get barDisplayWidth(): number { - return this.staff.system.staves.length > 1 ? this.bar.masterBar.displayWidth : this.bar.displayWidth; + return this.staff!.system.staves.length > 1 ? this.bar.masterBar.displayWidth : this.bar.displayWidth; } protected wasFirstOfLine: boolean = false; @@ -282,6 +284,12 @@ export class BarRendererBase { private _appliedLayoutingInfo: number = 0; + public afterReverted() { + this.staff = undefined; + this.registerMultiSystemSlurs(undefined); + this.isFinalized = false; + } + public afterStaffBarReverted() { this.topEffects.afterStaffBarReverted(); this.bottomEffects.afterStaffBarReverted(); @@ -337,14 +345,30 @@ export class BarRendererBase { public isFinalized: boolean = false; - public finalizeRenderer(): boolean { - this.isFinalized = true; + public registerMultiSystemSlurs(startedTies: Generator | undefined) { + if (!startedTies) { + this._multiSystemSlurs = undefined; + return; + } + + let ties: ContinuationTieGlyph[] | undefined = undefined; + for (const g of startedTies) { + const continuation = new ContinuationTieGlyph(g); + continuation.renderer = this; + continuation.tieDirection = g.tieDirection; + + if (!ties) { + ties = []; + } + ties.push(continuation); + } + + this._multiSystemSlurs = ties; + } + private _finalizeTies(ties: Iterable, barTop: number, barBottom: number): boolean { let didChangeOverflows = false; - // allow spacing to be used for tie overflows - const barTop = this.y; - const barBottom = this.y + this.height; - for (const t of this._ties) { + for (const t of ties) { const tie = t as unknown as Glyph; tie.doLayout(); @@ -367,6 +391,25 @@ export class BarRendererBase { } } } + return didChangeOverflows; + } + + public finalizeRenderer(): boolean { + this.isFinalized = true; + + let didChangeOverflows = false; + // allow spacing to be used for tie overflows + const barTop = this.y; + const barBottom = this.y + this.height; + + if (this._finalizeTies(this._ties, barTop, barBottom)) { + didChangeOverflows = true; + } + + const multiSystemSlurs = this._multiSystemSlurs; + if (multiSystemSlurs && this._finalizeTies(multiSystemSlurs, barTop, barBottom)) { + didChangeOverflows = true; + } const topHeightChanged = this.topEffects.finalizeEffects(); const bottomHeightChanged = this.bottomEffects.finalizeEffects(); @@ -383,8 +426,8 @@ export class BarRendererBase { } private _registerStaffOverflow() { - this.staff.registerOverflowTop(this.topOverflow); - this.staff.registerOverflowBottom(this.bottomOverflow); + this.staff!.registerOverflowTop(this.topOverflow); + this.staff!.registerOverflowBottom(this.bottomOverflow); } public doLayout(): void { @@ -500,7 +543,7 @@ export class BarRendererBase { } protected updateSizes(): void { - this.staff.registerStaffTop(0); + this.staff!.registerStaffTop(0); const voiceContainers: Map = this._voiceContainers; const beatGlyphsStart: number = this.beatGlyphsStart; let postBeatStart: number = beatGlyphsStart; @@ -524,7 +567,7 @@ export class BarRendererBase { this.height += this.layoutingInfo.height; this.height = Math.ceil(this.height); - this.staff.registerStaffBottom(this.height); + this.staff!.registerStaffBottom(this.height); } protected addPreBeatGlyph(g: Glyph): void { @@ -566,10 +609,10 @@ export class BarRendererBase { this.paintContent(cx, cy, canvas); - const topEffectBandY = cy + this.y - this.staff.topOverflow; + const topEffectBandY = cy + this.y - this.staff!.topOverflow; this.topEffects.paint(cx + this.x, topEffectBandY, canvas); - const bottomEffectBandY = cy + this.y + this.height + this.staff.bottomOverflow - this.bottomEffects.height; + const bottomEffectBandY = cy + this.y + this.height + this.staff!.bottomOverflow - this.bottomEffects.height; this.bottomEffects.paint(cx + this.x, bottomEffectBandY, canvas); } @@ -585,6 +628,19 @@ export class BarRendererBase { canvas.color = this.resources.mainGlyphColor; this._postBeatGlyphs.paint(cx + this.x, cy + this.y, canvas); + + this._paintMultiSystemSlurs(cx, cy, canvas); + } + + private _paintMultiSystemSlurs(cx: number, cy: number, canvas: ICanvas) { + const multiSystemSlurs = this._multiSystemSlurs; + if (!multiSystemSlurs) { + return; + } + + for (const slur of multiSystemSlurs) { + slur.paint(cx, cy, canvas); + } } protected paintBackground(cx: number, cy: number, canvas: ICanvas): void { diff --git a/packages/alphatab/src/rendering/EffectBand.ts b/packages/alphatab/src/rendering/EffectBand.ts index 540bc8d91..c0d5e8058 100644 --- a/packages/alphatab/src/rendering/EffectBand.ts +++ b/packages/alphatab/src/rendering/EffectBand.ts @@ -60,7 +60,7 @@ export class EffectBand extends Glyph { public static shouldCreateGlyph(beat: Beat, info: EffectInfo, renderer: BarRendererBase) { return ( info.shouldCreateGlyph(renderer.settings, beat) && - (!info.hideOnMultiTrack || renderer.staff.trackIndex === 0) + (!info.hideOnMultiTrack || renderer.staff!.trackIndex === 0) ); } diff --git a/packages/alphatab/src/rendering/LineBarRenderer.ts b/packages/alphatab/src/rendering/LineBarRenderer.ts index fee8ebe13..6aab55362 100644 --- a/packages/alphatab/src/rendering/LineBarRenderer.ts +++ b/packages/alphatab/src/rendering/LineBarRenderer.ts @@ -4,6 +4,7 @@ import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import type { Note } from '@coderline/alphatab/model/Note'; import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; import { NotationMode } from '@coderline/alphatab/NotationSettings'; import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; @@ -138,7 +139,7 @@ export abstract class LineBarRenderer extends BarRendererBase { // during system fitting it can happen that we have fraction widths // but to have lines until the full end-pixel we round up. - // this way we avoid holes, + // this way we avoid holes, const lineWidth = this.width; // we want the lines to be exactly virtually aligned with the respective Y-position @@ -1120,8 +1121,10 @@ export abstract class LineBarRenderer extends BarRendererBase { protected getMinLineOfBeat(_beat: Beat): number { return 0; } - + protected getMaxLineOfBeat(_beat: Beat): number { return 0; } + + public abstract getNoteLine(note: Note): number; } diff --git a/packages/alphatab/src/rendering/NumberedBarRenderer.ts b/packages/alphatab/src/rendering/NumberedBarRenderer.ts index f1b712afc..19bf27ac4 100644 --- a/packages/alphatab/src/rendering/NumberedBarRenderer.ts +++ b/packages/alphatab/src/rendering/NumberedBarRenderer.ts @@ -233,7 +233,7 @@ export class NumberedBarRenderer extends LineBarRenderer { } } - public getNoteLine() { + public getNoteLine(_note: Note) { return 0; } diff --git a/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts b/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts index 8cb87cab1..90681d405 100644 --- a/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts @@ -1,31 +1,41 @@ import type { Note } from '@coderline/alphatab/model/Note'; -import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { NumberedTieGlyph } from '@coderline/alphatab/rendering//glyphs/NumberedTieGlyph'; +import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { NumberedSlurGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedSlurGlyph'; +import type { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; /** * @internal */ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { + private _slurs: Map = new Map(); private _effectSlurs: NumberedSlurGlyph[] = []; + public override doLayout(): void { + this._slurs.clear(); + this._effectSlurs = []; + super.doLayout(); + } + protected override createTies(n: Note): void { // create a tie if any effect requires it if (!n.isVisible) { return; } - if (n.isTieOrigin && n.tieDestination!.isVisible) { - const tie = new NumberedTieGlyph(n, n.tieDestination!, false); + if (n.isTieOrigin && n.tieDestination!.isVisible && !this._slurs.has('numbered.tie')) { + const tie = new NumberedTieGlyph(`numbered.tie.${n.beat.id}`, n, n.tieDestination!, false); this.addTie(tie); + this._slurs.set(tie.slurEffectId, tie); } if (n.isTieDestination) { - const tie = new NumberedTieGlyph(n.tieOrigin!, n, true); + const tie = new NumberedTieGlyph(`numbered.tie.${n.tieOrigin!.beat.id}`, n.tieOrigin!, n, true); this.addTie(tie); } - if (n.isLeftHandTapped && !n.isHammerPullDestination) { - const tapSlur = new NumberedTieGlyph(n, n, false); + if (n.isLeftHandTapped && !n.isHammerPullDestination && !this._slurs.has(`numbered.tie.leftHandTap.${n.beat.id}`)) { + const tapSlur = new NumberedTieGlyph(`numbered.tie.leftHandTap.${n.beat.id}`, n, n, false); this.addTie(tapSlur); + this._slurs.set(tapSlur.slurEffectId, tapSlur); } // start effect slur on first beat if (n.isEffectSlurOrigin && n.effectSlurDestination) { @@ -36,12 +46,22 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { break; } } + if (!expanded) { - const effectSlur = new NumberedSlurGlyph(n, n.effectSlurDestination, false, false); + const effectSlur = new NumberedSlurGlyph( + `numbered.slur.effect`, + n, + n.effectSlurDestination, + false, + false + ); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); + this._slurs.set(effectSlur.slurEffectId, effectSlur); + this._slurs.set('numbered.slur.effect', effectSlur); } } + // end effect slur on last beat if (n.isEffectSlurDestination && n.effectSlurOrigin) { let expanded: boolean = false; @@ -52,9 +72,17 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { } } if (!expanded) { - const effectSlur = new NumberedSlurGlyph(n.effectSlurOrigin, n, false, true); + const effectSlur = new NumberedSlurGlyph( + `numbered.slur.effect`, + n.effectSlurOrigin, + n, + false, + true + ); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); + this._slurs.set(effectSlur.slurEffectId, effectSlur); + this._slurs.set('numbered.slur.effect', effectSlur); } } } diff --git a/packages/alphatab/src/rendering/ScoreBarRenderer.ts b/packages/alphatab/src/rendering/ScoreBarRenderer.ts index b650fec07..178c090ae 100644 --- a/packages/alphatab/src/rendering/ScoreBarRenderer.ts +++ b/packages/alphatab/src/rendering/ScoreBarRenderer.ts @@ -484,6 +484,10 @@ export class ScoreBarRenderer extends LineBarRenderer { } } + public override getNoteLine(note: Note): number { + return this.accidentalHelper.getNoteSteps(note) / 2; + } + public getNoteSteps(n: Note): number { return this.accidentalHelper.getNoteSteps(n); } diff --git a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts index b49256981..77eb17dd9 100644 --- a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts @@ -3,13 +3,13 @@ import { GraceType } from '@coderline/alphatab/model/GraceType'; import type { Note } from '@coderline/alphatab/model/Note'; import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; +import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { ScoreBendGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBendGlyph'; import { ScoreLegatoGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreLegatoGlyph'; import { ScoreSlideLineGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreSlideLineGlyph'; import { ScoreSlurGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreSlurGlyph'; import { ScoreTieGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTieGlyph'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal @@ -61,12 +61,11 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { n.tieDestination && n.tieDestination.isVisible ) { - // tslint:disable-next-line: no-unnecessary-type-assertion - const tie: ScoreTieGlyph = new ScoreTieGlyph(n, n.tieDestination!, false); + const tie: ScoreTieGlyph = new ScoreTieGlyph(`score.tie.${n.id}`, n, n.tieDestination!, false); this.addTie(tie); } if (n.isTieDestination && !n.tieOrigin!.hasBend && !n.beat.hasWhammyBar) { - const tie: ScoreTieGlyph = new ScoreTieGlyph(n.tieOrigin!, n, true); + const tie: ScoreTieGlyph = new ScoreTieGlyph(`score.tie.${n.tieOrigin!.id}`, n.tieOrigin!, n, true); this.addTie(tie); } // TODO: depending on the type we have other positioning @@ -76,27 +75,26 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { this.addTie(l); } if (n.isSlurOrigin && n.slurDestination && n.slurDestination.isVisible) { - // tslint:disable-next-line: no-unnecessary-type-assertion - const tie: ScoreSlurGlyph = new ScoreSlurGlyph(n, n.slurDestination!, false); + const tie: ScoreSlurGlyph = new ScoreSlurGlyph(`score.slur.${n.id}`, n, n.slurDestination!, false); this.addTie(tie); } if (n.isSlurDestination) { - const tie: ScoreSlurGlyph = new ScoreSlurGlyph(n.slurOrigin!, n, true); + const tie: ScoreSlurGlyph = new ScoreSlurGlyph(`score.slur.${n.slurOrigin!.id}`, n.slurOrigin!, n, true); this.addTie(tie); } // start effect slur on first beat if (!this._effectSlur && n.isEffectSlurOrigin && n.effectSlurDestination) { - const effectSlur = new ScoreSlurGlyph(n, n.effectSlurDestination, false); + const effectSlur = new ScoreSlurGlyph(`score.slur.effect.${n.beat.id}`, n, n.effectSlurDestination, false); this._effectSlur = effectSlur; this.addTie(effectSlur); } // end effect slur on last beat if (!this._effectEndSlur && n.beat.isEffectSlurDestination && n.beat.effectSlurOrigin) { - const direction: BeamDirection = this.onNotes.beamingHelper.direction; - const startNote: Note = + const direction = this.onNotes.beamingHelper.direction; + const startNote = direction === BeamDirection.Up ? n.beat.effectSlurOrigin.minNote! : n.beat.effectSlurOrigin.maxNote!; - const endNote: Note = direction === BeamDirection.Up ? n.beat.minNote! : n.beat.maxNote!; - const effectEndSlur = new ScoreSlurGlyph(startNote, endNote, true); + const endNote = direction === BeamDirection.Up ? n.beat.minNote! : n.beat.maxNote!; + const effectEndSlur = new ScoreSlurGlyph(`score.slur.effect.${startNote.beat.id}`, startNote, endNote, true); this._effectEndSlur = effectEndSlur; this.addTie(effectEndSlur); } @@ -119,7 +117,7 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { while (destination.nextBeat && destination.nextBeat.isLegatoDestination) { destination = destination.nextBeat; } - this.addTie(new ScoreLegatoGlyph(this.beat, destination, false)); + this.addTie(new ScoreLegatoGlyph(`score.legato.${this.beat.id}`, this.beat, destination, false)); } } else if (this.beat.isLegatoDestination) { // only create slur for last destination of "group" @@ -128,7 +126,7 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { while (origin.previousBeat && origin.previousBeat.isLegatoOrigin) { origin = origin.previousBeat; } - this.addTie(new ScoreLegatoGlyph(origin, this.beat, true)); + this.addTie(new ScoreLegatoGlyph(`score.legato.${origin.id}`, origin, this.beat, true)); } } } diff --git a/packages/alphatab/src/rendering/SlashBarRenderer.ts b/packages/alphatab/src/rendering/SlashBarRenderer.ts index 738630128..279ae2561 100644 --- a/packages/alphatab/src/rendering/SlashBarRenderer.ts +++ b/packages/alphatab/src/rendering/SlashBarRenderer.ts @@ -98,7 +98,7 @@ export class SlashBarRenderer extends LineBarRenderer { } } - public getNoteLine() { + public getNoteLine(_note: Note) { return 0; } diff --git a/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts b/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts index b0b580817..2e7274e65 100644 --- a/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts @@ -15,12 +15,12 @@ export class SlashBeatContainerGlyph extends BeatContainerGlyph { } if (!this._tiedNoteTie && n.isTieOrigin && n.tieDestination!.isVisible) { - const tie: SlashTieGlyph = new SlashTieGlyph(n, n.tieDestination!, false); + const tie: SlashTieGlyph = new SlashTieGlyph('slash.tie', n, n.tieDestination!, false); this._tiedNoteTie = tie; this.addTie(tie); } if (!this._tiedNoteTie && n.isTieDestination) { - const tie: SlashTieGlyph = new SlashTieGlyph(n.tieOrigin!, n, true); + const tie: SlashTieGlyph = new SlashTieGlyph('slash.tie', n.tieOrigin!, n, true); this._tiedNoteTie = tie; this.addTie(tie); } diff --git a/packages/alphatab/src/rendering/TabBarRenderer.ts b/packages/alphatab/src/rendering/TabBarRenderer.ts index c512d095d..3eb241a96 100644 --- a/packages/alphatab/src/rendering/TabBarRenderer.ts +++ b/packages/alphatab/src/rendering/TabBarRenderer.ts @@ -3,6 +3,7 @@ import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import type { Note } from '@coderline/alphatab/model/Note'; import type { Voice } from '@coderline/alphatab/model/Voice'; import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; @@ -78,18 +79,8 @@ export class TabBarRenderer extends LineBarRenderer { return mode; } - /** - * Gets the relative y position of the given steps relative to first line. - * @param line the line of the particular string where 0 is the most top line - * @param correction - * @returns - */ - public getTabY(line: number): number { - return super.getLineY(line); - } - - public getTabHeight(line: number): number { - return super.getLineHeight(line); + public override getNoteLine(note: Note): number { + return this.bar.staff.tuning.length - note.string; } public minString = Number.NaN; @@ -192,7 +183,7 @@ export class TabBarRenderer extends LineBarRenderer { if (this.isFirstOfLine) { const center: number = (this.bar.staff.tuning.length - 1) / 2; this.createStartSpacing(); - this.addPreBeatGlyph(new TabClefGlyph(0, this.getTabY(center))); + this.addPreBeatGlyph(new TabClefGlyph(0, this.getLineY(center))); } // Time Signature if ( @@ -220,7 +211,7 @@ export class TabBarRenderer extends LineBarRenderer { this.addPreBeatGlyph( new TabTimeSignatureGlyph( 0, - this.getTabY(lines), + this.getLineY(lines), this.bar.masterBar.timeSignatureNumerator, this.bar.masterBar.timeSignatureDenominator, this.bar.masterBar.timeSignatureCommon, diff --git a/packages/alphatab/src/rendering/effects/TabWhammyEffectInfo.ts b/packages/alphatab/src/rendering/effects/TabWhammyEffectInfo.ts index 055a6fd6a..37068bfb8 100644 --- a/packages/alphatab/src/rendering/effects/TabWhammyEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/TabWhammyEffectInfo.ts @@ -47,11 +47,11 @@ export class TabWhammyEffectInfo extends EffectInfo { public override onAlignGlyphs(band: EffectBand): void { // re-register the sizes so they are available during finalization later - const info = band.renderer.staff.getSharedLayoutData<[number, number]>( + const info = band.renderer.staff!.getSharedLayoutData<[number, number]>( TabWhammyEffectInfo.offsetSharedDataKey, [0, 0] ); - band.renderer.staff.setSharedLayoutData(TabWhammyEffectInfo.offsetSharedDataKey, info); + band.renderer.staff!.setSharedLayoutData(TabWhammyEffectInfo.offsetSharedDataKey, info); for (const g of band.iterateAllGlyphs()) { const tb = g as TabWhammyBarGlyph; if (tb.originalTopOffset > info[0]) { @@ -64,7 +64,7 @@ export class TabWhammyEffectInfo extends EffectInfo { } public override finalizeBand(band: EffectBand): void { - const info = band.renderer.staff.getSharedLayoutData<[number, number]>( + const info = band.renderer.staff!.getSharedLayoutData<[number, number]>( TabWhammyEffectInfo.offsetSharedDataKey, [0, 0] ); diff --git a/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts b/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts index 764f501ba..99c84990a 100644 --- a/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts @@ -334,8 +334,8 @@ export class BarLineGlyph extends LeftToRightLayoutingGlyphGroup { // extending across systems needs some more dynamic lookup, we do that during drawing // as during layout things are still moving let actualLineHeight = this.height; - const thisStaff = renderer.staff; - const allStaves = renderer.staff.system.allStaves; + const thisStaff = renderer.staff!; + const allStaves = thisStaff.system.allStaves; let isExtended = false; if (this._extendToNextStaff && thisStaff.index < allStaves.length - 1) { const nextStaff = allStaves[thisStaff.index + 1]; diff --git a/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts b/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts index fdf142f28..500599a0c 100644 --- a/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts @@ -24,7 +24,7 @@ export class BarNumberGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - if (!this.renderer.staff.isFirstInSystem) { + if (!this.renderer.staff!.isFirstInSystem) { return; } diff --git a/packages/alphatab/src/rendering/glyphs/GroupedEffectGlyph.ts b/packages/alphatab/src/rendering/glyphs/GroupedEffectGlyph.ts index bd9dcd392..e6994b7d6 100644 --- a/packages/alphatab/src/rendering/glyphs/GroupedEffectGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/GroupedEffectGlyph.ts @@ -18,14 +18,14 @@ export abstract class GroupedEffectGlyph extends EffectGlyph { } public get isLinkedWithPrevious(): boolean { - return !!this.previousGlyph && this.previousGlyph.renderer.staff.system === this.renderer.staff.system; + return !!this.previousGlyph && this.previousGlyph.renderer.staff?.system === this.renderer.staff!.system; } public get isLinkedWithNext(): boolean { return ( !!this.nextGlyph && this.nextGlyph.renderer.isFinalized && - this.nextGlyph.renderer.staff.system === this.renderer.staff.system + this.nextGlyph.renderer.staff?.system === this.renderer.staff!.system ); } diff --git a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts index 95e19da08..7f417698a 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts @@ -242,11 +242,11 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { sr.shortestDuration = this.container.beat.duration; } - const glyphY = sr.getLineY(sr.getNoteLine()); - if (!this.container.beat.isEmpty) { + const glyphY = sr.getLineY(0); let numberWithinOctave = '0'; if (this.container.beat.notes.length > 0) { + const note = this.container.beat.notes[0]; const kst = this.renderer.bar.keySignatureType; const ks = this.renderer.bar.keySignature as number; const ksi = ks + 7; @@ -257,8 +257,6 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { : NumberedBeatGlyph.majorKeySignatureOneValues; const oneNoteValue = oneNoteValues[ksi]; - const note = this.container.beat.notes[0]; - if (note.isDead) { numberWithinOctave = 'X'; } else { @@ -320,7 +318,7 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { // Note dots if (this.container.beat.dots > 0 && this.container.beat.duration >= Duration.Quarter) { for (let i: number = 0; i < this.container.beat.dots; i++) { - const dot = new AugmentationDotGlyph(0, sr.getLineY(0)); + const dot = new AugmentationDotGlyph(0, glyphY); dot.renderer = this.renderer; this.addEffect(dot); } @@ -350,7 +348,7 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { numberOfQuarterNotes += numberOfAddedQuarters; } for (let i = 0; i < numberOfQuarterNotes - 1; i++) { - const dash = new NumberedDashGlyph(0, sr.getLineY(0), this.container.beat); + const dash = new NumberedDashGlyph(0, glyphY, this.container.beat); dash.renderer = this.renderer; this.addNormal(dash); } diff --git a/packages/alphatab/src/rendering/glyphs/NumberedSlurGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedSlurGlyph.ts index 90b892af6..7c08afc64 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedSlurGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedSlurGlyph.ts @@ -1,79 +1,11 @@ -import type { Note } from '@coderline/alphatab/model/Note'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TabTieGlyph } from '@coderline/alphatab/rendering/glyphs/TabTieGlyph'; +import { TabSlurGlyph } from '@coderline/alphatab/rendering/glyphs/TabSlurGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class NumberedSlurGlyph extends TabTieGlyph { - private _direction: BeamDirection; - private _forSlide: boolean; - - public constructor(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean = false) { - super(startNote, endNote, forEnd); - this._direction = BeamDirection.Up; - this._forSlide = forSlide; - } - - protected override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { - return Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight / 2; - } - - public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean): boolean { - // same type required - if (this._forSlide !== forSlide) { - return false; - } - if (this.forEnd !== forEnd) { - return false; - } - // same start and endbeat - if (this.startNote.beat.id !== startNote.beat.id) { - return false; - } - if (this.endNote.beat.id !== endNote.beat.id) { - return false; - } - // if we can expand, expand in correct direction - switch (this._direction) { - case BeamDirection.Up: - if (startNote.realValue > this.startNote.realValue) { - this.startNote = startNote; - this.startBeat = startNote.beat; - } - if (endNote.realValue > this.endNote.realValue) { - this.endNote = endNote; - this.endBeat = endNote.beat; - } - break; - case BeamDirection.Down: - if (startNote.realValue < this.startNote.realValue) { - this.startNote = startNote; - this.startBeat = startNote.beat; - } - if (endNote.realValue < this.endNote.realValue) { - this.endNote = endNote; - this.endBeat = endNote.beat; - } - break; - } - return true; - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - const startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, - this.startBeat!.voice.bar - )!; - const direction: BeamDirection = this.getBeamDirection(this.startBeat!, startNoteRenderer); - const slurId: string = `numbered.slur.${this.startNote.beat.id}.${this.endNote.beat.id}.${direction}`; - const renderer = this.renderer; - const isSlurRendered: boolean = renderer.staff.getSharedLayoutData(slurId, false); - if (!isSlurRendered) { - renderer.staff.setSharedLayoutData(slurId, true); - super.paint(cx, cy, canvas); - } +export class NumberedSlurGlyph extends TabSlurGlyph { + protected override calculateTieDirection(): BeamDirection { + return BeamDirection.Up; } } diff --git a/packages/alphatab/src/rendering/glyphs/NumberedTieGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedTieGlyph.ts index 35646ca96..68345e7c1 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedTieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedTieGlyph.ts @@ -1,26 +1,10 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; -import type { Note } from '@coderline/alphatab/model/Note'; -import { type BarRendererBase, NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { NoteTieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class NumberedTieGlyph extends TieGlyph { - protected startNote: Note; - protected endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(!startNote ? null : startNote.beat, !endNote ? null : endNote.beat, forEnd); - this.startNote = startNote; - this.endNote = endNote; - } - - private get _isLeftHandTap() { - return this.startNote === this.endNote; - } - +export class NumberedTieGlyph extends NoteTieGlyph { protected override shouldDrawBendSlur() { return ( this.renderer.settings.notation.extendBendArrowsOnTiedNotes && @@ -29,33 +13,7 @@ export class NumberedTieGlyph extends TieGlyph { ); } - public override doLayout(): void { - super.doLayout(); - } - - protected override getBeamDirection(_beat: Beat, _noteRenderer: BarRendererBase): BeamDirection { + protected override calculateTieDirection(): BeamDirection { return BeamDirection.Up; } - - protected override getStartY(): number { - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); - } - - protected override getEndY(): number { - return this.getStartY(); - } - - protected override getStartX(): number { - if (this._isLeftHandTap) { - return this.getEndX() - this.startNoteRenderer!.smuflMetrics.leftHandTabTieWidth; - } - return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Center); - } - - protected override getEndX(): number { - if (this._isLeftHandTap) { - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); - } - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Center); - } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts index 455a045ca..cfb6b0e04 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts @@ -201,7 +201,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly public override paint(cx: number, cy: number, canvas: ICanvas): void { // Draw note heads const startNoteRenderer: ScoreBarRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, this._beat.voice.bar )! as ScoreBarRenderer; const startX: number = @@ -256,7 +256,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly const endNoteRenderer: ScoreBarRenderer | null = !endNote ? null : (this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, endNote.beat.voice.bar ) as ScoreBarRenderer); // if we have a line break we draw only a line until the end diff --git a/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts index 00c206b87..a542f212d 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts @@ -1,30 +1,60 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; import { type BarRendererBase, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; -import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; /** * @internal */ export class ScoreLegatoGlyph extends TieGlyph { - public constructor(startBeat: Beat, endBeat: Beat, forEnd: boolean = false) { - super(startBeat, endBeat, forEnd); + protected startBeat: Beat; + protected endBeat: Beat; + protected startBeatRenderer: BarRendererBase | null = null; + protected endBeatRenderer: BarRendererBase | null = null; + + public constructor(slurEffectId: string, startBeat: Beat, endBeat: Beat, forEnd:boolean) { + super(slurEffectId, forEnd); + this.startBeat = startBeat; + this.endBeat = endBeat; } public override doLayout(): void { super.doLayout(); } - protected override getBeamDirection(beat: Beat, noteRenderer: BarRendererBase): BeamDirection { - if (beat.isRest) { + protected override lookupStartBeatRenderer(): BarRendererBase { + if (!this.startBeatRenderer) { + this.startBeatRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff!.staffId, + this.startBeat.voice.bar + )!; + } + return this.startBeatRenderer; + } + + protected override lookupEndBeatRenderer(): BarRendererBase | null { + if (!this.endBeatRenderer) { + this.endBeatRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff!.staffId, + this.endBeat.voice.bar + ); + } + return this.endBeatRenderer; + } + + protected override shouldDrawBendSlur(): boolean { + return false; + } + + protected override calculateTieDirection(): BeamDirection { + if (this.startBeat.isRest) { return BeamDirection.Up; } // invert direction (if stems go up, ties go down to not cross them) - switch ((noteRenderer as ScoreBarRenderer).getBeatDirection(beat)) { + switch (this.lookupStartBeatRenderer().getBeatDirection(this.startBeat)) { case BeamDirection.Up: return BeamDirection.Down; default: @@ -32,76 +62,116 @@ export class ScoreLegatoGlyph extends TieGlyph { } } - protected override getStartY(): number { + protected override calculateStartX(): number { + const startBeatRenderer = this.lookupStartBeatRenderer(); + return startBeatRenderer.x + startBeatRenderer.getBeatX(this.startBeat!, BeatXPosition.MiddleNotes); + } + + protected override calculateStartY(): number { + const startBeatRenderer = this.lookupStartBeatRenderer(); if (this.startBeat!.isRest) { - // below all lines - return (this.startNoteRenderer as ScoreBarRenderer).getScoreY(9); + switch (this.tieDirection) { + case BeamDirection.Up: + return ( + startBeatRenderer.y + + startBeatRenderer.getBeatContainer(this.startBeat)!.onNotes.getBoundingBoxTop() + ); + default: + return ( + startBeatRenderer.y + + startBeatRenderer.getBeatContainer(this.startBeat)!.onNotes.getBoundingBoxBottom() + ); + } } + switch (this.tieDirection) { case BeamDirection.Up: // below lowest note - return this.startNoteRenderer!.getNoteY(this.startBeat!.maxNote!, NoteYPosition.Top); + return startBeatRenderer.y + startBeatRenderer.getNoteY(this.startBeat!.maxNote!, NoteYPosition.Top); default: - return this.startNoteRenderer!.getNoteY(this.startBeat!.minNote!, NoteYPosition.Bottom); + return startBeatRenderer.y + startBeatRenderer.getNoteY(this.startBeat!.minNote!, NoteYPosition.Bottom); } } - protected override getEndY(): number { - const endNoteScoreRenderer = this.endNoteRenderer as ScoreBarRenderer; - if (this.endBeat!.isRest) { + protected override calculateEndX(): number { + const endBeatRenderer = this.lookupEndBeatRenderer(); + if (!endBeatRenderer) { + return this.calculateStartX() + this.renderer.smuflMetrics.leftHandTabTieWidth; + } + const endBeamDirection = endBeatRenderer.getBeatDirection(this.endBeat); + return ( + endBeatRenderer.x + + endBeatRenderer.getBeatX( + this.endBeat, + this.endBeat.duration > Duration.Whole && endBeamDirection === this.tieDirection + ? BeatXPosition.Stem + : BeatXPosition.MiddleNotes + ) + ); + } + + protected override caclculateEndY(): number { + const endBeatRenderer = this.lookupEndBeatRenderer(); + if (!endBeatRenderer) { + return this.calculateStartY(); + } + + if (this.endBeat.isRest) { switch (this.tieDirection) { case BeamDirection.Up: - return endNoteScoreRenderer.getScoreY(9); + return ( + endBeatRenderer.y + endBeatRenderer.getBeatContainer(this.endBeat)!.onNotes.getBoundingBoxTop() + ); default: - return endNoteScoreRenderer.getScoreY(0); + return ( + endBeatRenderer.y + + endBeatRenderer.getBeatContainer(this.endBeat)!.onNotes.getBoundingBoxBottom() + ); } } - const startBeamDirection = (this.startNoteRenderer as ScoreBarRenderer).getBeatDirection(this.startBeat!); - const endBeamDirection = endNoteScoreRenderer.getBeatDirection(this.endBeat!); + const startBeamDirection = this.lookupStartBeatRenderer().getBeatDirection(this.startBeat!); + const endBeamDirection = endBeatRenderer.getBeatDirection(this.endBeat!); if (startBeamDirection !== endBeamDirection && this.startBeat!.graceType === GraceType.None) { if (endBeamDirection === this.tieDirection) { switch (this.tieDirection) { case BeamDirection.Up: // stem upper end - return endNoteScoreRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.TopWithStem); + return ( + endBeatRenderer.y + + endBeatRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.TopWithStem) + ); default: // stem lower end - return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.BottomWithStem); + return ( + endBeatRenderer.y + + endBeatRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.BottomWithStem) + ); } } switch (this.tieDirection) { case BeamDirection.Up: // stem upper end - return endNoteScoreRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.BottomWithStem); + return ( + endBeatRenderer.y + + endBeatRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.BottomWithStem) + ); default: // stem lower end - return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.TopWithStem); + return ( + endBeatRenderer.y + endBeatRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.TopWithStem) + ); } } switch (this.tieDirection) { case BeamDirection.Up: // below lowest note - return endNoteScoreRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.Top); + return endBeatRenderer.y + endBeatRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.Top); default: // above highest note - return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.Bottom); + return endBeatRenderer.y + endBeatRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.Bottom); } } - - protected override getStartX(): number { - return this.startNoteRenderer!.getBeatX(this.startBeat!, BeatXPosition.MiddleNotes); - } - - protected override getEndX(): number { - const endBeamDirection = (this.endNoteRenderer as ScoreBarRenderer).getBeatDirection(this.endBeat!); - return this.endNoteRenderer!.getBeatX( - this.endBeat!, - this.endBeat!.duration > Duration.Whole && endBeamDirection === this.tieDirection - ? BeatXPosition.Stem - : BeatXPosition.MiddleNotes - ); - } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts index 28ea5d987..55d1ff768 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts @@ -100,7 +100,7 @@ export class ScoreSlideLineGlyph extends Glyph implements ITieGlyph { if (this._startNote.slideTarget) { const endNoteRenderer: BarRendererBase | null = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, this._startNote.slideTarget.beat.voice.bar ); if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreSlurGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreSlurGlyph.ts index bcb5a2717..8735bd156 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreSlurGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreSlurGlyph.ts @@ -1,98 +1,100 @@ -import type { Note } from '@coderline/alphatab/model/Note'; -import { ScoreLegatoGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreLegatoGlyph'; -import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; -import { NoteYPosition, NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { ScoreTieGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTieGlyph'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class ScoreSlurGlyph extends ScoreLegatoGlyph { - private _startNote: Note; - private _endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(startNote.beat, endNote.beat, forEnd); - this._startNote = startNote; - this._endNote = endNote; +export class ScoreSlurGlyph extends ScoreTieGlyph { + public override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { + return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2; } - protected override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { - return Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight / 2; + protected override calculateStartX(): number { + return ( + this.renderer.x + + (this._isStartCentered() + ? this.renderer!.getBeatX(this.startNote.beat, BeatXPosition.MiddleNotes) + : this.renderer!.getNoteX(this.startNote, NoteXPosition.Right)) + ); } - protected override getStartY(): number { + protected override calculateStartY(): number { if (this._isStartCentered()) { switch (this.tieDirection) { case BeamDirection.Up: - // below lowest note - return this.startNoteRenderer!.getNoteY(this._startNote, NoteYPosition.Top); + return this.renderer.y + this.renderer!.getNoteY(this.startNote, NoteYPosition.Top); default: - return this.startNoteRenderer!.getNoteY(this._startNote, NoteYPosition.Bottom); + return this.renderer.y + this.renderer!.getNoteY(this.startNote, NoteYPosition.Bottom); } } - return this.startNoteRenderer!.getNoteY(this._startNote, NoteYPosition.Center); + return this.renderer.y + this.renderer!.getNoteY(this.startNote, NoteYPosition.Center); + } + + protected override calculateEndX(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartX() + this.renderer.smuflMetrics.leftHandTabTieWidth; + } + + if (this._isEndCentered()) { + if (this._isEndOnStem()) { + return endNoteRenderer.x + endNoteRenderer.getBeatX(this.endNote.beat, BeatXPosition.Stem); + } + return endNoteRenderer.x + endNoteRenderer.getNoteX(this.endNote, NoteXPosition.Center); + } + return endNoteRenderer.x + endNoteRenderer.getBeatX(this.endNote.beat, BeatXPosition.PreNotes); } - protected override getEndY(): number { + protected override caclculateEndY(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartY(); + } + if (this._isEndCentered()) { if (this._isEndOnStem()) { switch (this.tieDirection) { case BeamDirection.Up: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.TopWithStem); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.TopWithStem); default: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.BottomWithStem); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.BottomWithStem); } } switch (this.tieDirection) { case BeamDirection.Up: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Top); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.Top); default: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Bottom); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.Bottom); } } - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Center); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.Center); } private _isStartCentered() { return ( - (this._startNote === this._startNote.beat.maxNote && this.tieDirection === BeamDirection.Up) || - (this._startNote === this._startNote.beat.minNote && this.tieDirection === BeamDirection.Down) + (this.startNote === this.startNote.beat.maxNote && this.tieDirection === BeamDirection.Up) || + (this.startNote === this.startNote.beat.minNote && this.tieDirection === BeamDirection.Down) ); } private _isEndCentered() { return ( - this._startNote.beat.graceType === GraceType.None && - ((this._endNote === this._endNote.beat.maxNote && this.tieDirection === BeamDirection.Up) || - (this._endNote === this._endNote.beat.minNote && this.tieDirection === BeamDirection.Down)) + this.startNote.beat.graceType === GraceType.None && + ((this.endNote === this.endNote.beat.maxNote && this.tieDirection === BeamDirection.Up) || + (this.endNote === this.endNote.beat.minNote && this.tieDirection === BeamDirection.Down)) ); } private _isEndOnStem() { - const endNoteScoreRenderer = this.endNoteRenderer as ScoreBarRenderer; - - const startBeamDirection = (this.startNoteRenderer as ScoreBarRenderer).getBeatDirection(this.startBeat!); - const endBeamDirection = endNoteScoreRenderer.getBeatDirection(this.endBeat!); - - return startBeamDirection !== endBeamDirection && this.startBeat!.graceType === GraceType.None; - } + const startBeamDirection = this.lookupStartBeatRenderer().getBeatDirection(this.startNote.beat); + const endBeatRenderer = this.lookupEndBeatRenderer(); + const endBeamDirection = endBeatRenderer + ? endBeatRenderer.getBeatDirection(this.endNote.beat) + : startBeamDirection; - protected override getStartX(): number { - return this._isStartCentered() - ? this.startNoteRenderer!.getBeatX(this._startNote.beat, BeatXPosition.MiddleNotes) - : this.startNoteRenderer!.getNoteX(this._startNote, NoteXPosition.Right); - } - - protected override getEndX(): number { - if (this._isEndCentered()) { - if (this._isEndOnStem()) { - return this.endNoteRenderer!.getBeatX(this._endNote.beat, BeatXPosition.Stem); - } - return this.endNoteRenderer!.getNoteX(this._endNote, NoteXPosition.Center); - } - return this.endNoteRenderer!.getBeatX(this._endNote.beat, BeatXPosition.PreNotes); + return startBeamDirection !== endBeamDirection && this.startNote.beat!.graceType === GraceType.None; } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreTieGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreTieGlyph.ts index 933142279..17a9effa2 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreTieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreTieGlyph.ts @@ -1,24 +1,11 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; -import type { Note } from '@coderline/alphatab/model/Note'; -import { type BarRendererBase, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; -import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { NoteTieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; /** * @internal */ -export class ScoreTieGlyph extends TieGlyph { - protected startNote: Note; - protected endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(!startNote ? null : startNote.beat, !endNote ? null : endNote.beat, forEnd); - this.startNote = startNote; - this.endNote = endNote; - } - +export class ScoreTieGlyph extends NoteTieGlyph { protected override shouldDrawBendSlur() { return ( this.renderer.settings.notation.extendBendArrowsOnTiedNotes && @@ -27,58 +14,21 @@ export class ScoreTieGlyph extends TieGlyph { ); } - public override doLayout(): void { - super.doLayout(); - } - - protected override getBeamDirection(beat: Beat, noteRenderer: BarRendererBase): BeamDirection { - // invert direction (if stems go up, ties go down to not cross them) - switch ((noteRenderer as ScoreBarRenderer).getBeatDirection(beat)) { - case BeamDirection.Up: - return BeamDirection.Down; - default: - return BeamDirection.Up; - } - } - - protected override getStartY(): number { - if (this.startBeat!.isRest) { - // below all lines - return (this.startNoteRenderer as ScoreBarRenderer).getScoreY(9); - } - switch (this.tieDirection) { - case BeamDirection.Up: - // below lowest note - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); - default: - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Bottom); + protected override calculateStartX(): number { + if (this.isLeftHandTap) { + return this.calculateEndX() - this.renderer.smuflMetrics.leftHandTabTieWidth; } + return this.renderer.x + this.renderer!.getBeatX(this.startNote.beat, BeatXPosition.PostNotes); } - protected override getEndY(): number { - const endNoteScoreRenderer = this.endNoteRenderer as ScoreBarRenderer; - if (this.endBeat!.isRest) { - switch (this.tieDirection) { - case BeamDirection.Up: - return endNoteScoreRenderer.getScoreY(9); - default: - return endNoteScoreRenderer.getScoreY(0); - } + protected override calculateEndX(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartX() + this.renderer.smuflMetrics.leftHandTabTieWidth; } - - switch (this.tieDirection) { - case BeamDirection.Up: - return endNoteScoreRenderer.getNoteY(this.endNote, NoteYPosition.Top); - default: - return endNoteScoreRenderer.getNoteY(this.endNote, NoteYPosition.Bottom); + if (this.isLeftHandTap) { + return endNoteRenderer.x + endNoteRenderer.getNoteX(this.endNote, NoteXPosition.Left); } - } - - protected override getStartX(): number { - return this.startNoteRenderer!.getBeatX(this.startNote.beat, BeatXPosition.PostNotes); - } - - protected override getEndX(): number { - return this.endNoteRenderer!.getBeatX(this.endNote.beat, BeatXPosition.PreNotes); + return endNoteRenderer.x + endNoteRenderer.getBeatX(this.endNote.beat, BeatXPosition.PreNotes); } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts index b5c11f7e7..4d0eba77e 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts @@ -138,7 +138,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT } const whammyMode: NotationMode = this.renderer.settings.notation.notationMode; const startNoteRenderer: ScoreBarRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, beat.voice.bar )! as ScoreBarRenderer; @@ -180,7 +180,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT let endNoteRenderer: ScoreBarRenderer | null = null; if (note.isTieOrigin) { endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, note.tieDestination!.beat.voice.bar ) as ScoreBarRenderer | null; if (endNoteRenderer && endNoteRenderer.staff === startNoteRenderer.staff) { diff --git a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts index c8ef97f81..56527782d 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts @@ -136,8 +136,7 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { // create glyphs const sr = this.renderer as SlashBarRenderer; - const line: number = sr.getNoteLine(); - const glyphY = sr.getLineY(line); + const glyphY = sr.getLineY(0); if (this.container.beat.deadSlapped) { const deadSlapped = new DeadSlappedBeatGlyph(); deadSlapped.renderer = this.renderer; @@ -179,7 +178,7 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { this.addEffect( new AugmentationDotGlyph( 0, - sr.getLineY(sr.getNoteLine()) - sr.getLineHeight(0.5) + glyphY - sr.getLineHeight(0.5) ) ); } diff --git a/packages/alphatab/src/rendering/glyphs/SlashTieGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashTieGlyph.ts index 2548b7f66..de4f432bb 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashTieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashTieGlyph.ts @@ -1,57 +1,20 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; -import type { Note } from '@coderline/alphatab/model/Note'; -import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { NoteTieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class SlashTieGlyph extends TieGlyph { - protected startNote: Note; - protected endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(startNote.beat, endNote.beat, forEnd); - this.startNote = startNote; - this.endNote = endNote; - } - - private get _isLeftHandTap() { - return this.startNote === this.endNote; - } - - protected override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { - if (this._isLeftHandTap) { - return this.startNoteRenderer!.smuflMetrics.tieHeight; - } - return super.getTieHeight(startX, startY, endX, endY); - } - - protected override getBeamDirection(_beat: Beat, _noteRenderer: BarRendererBase): BeamDirection { +export class SlashTieGlyph extends NoteTieGlyph { + protected override calculateTieDirection(): BeamDirection { return BeamDirection.Down; } - protected static getBeamDirectionForNote(_note: Note): BeamDirection { - return BeamDirection.Down; - } - - protected override getStartY(): number { - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Center); - } - - protected override getEndY(): number { - return this.getStartY(); - } - - protected override getStartX(): number { - if (this._isLeftHandTap) { - return this.getEndX() - this.renderer.smuflMetrics.leftHandTabTieWidth; - } - return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Right); + protected override getStartNotePosition() { + return NoteXPosition.Right; } - protected override getEndX(): number { - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); + protected override getEndNotePosition(): NoteXPosition { + return NoteXPosition.Left; } } diff --git a/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts index af74b0e0a..2af0ea003 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts @@ -36,15 +36,15 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { } const renderer: TabBarRenderer = this.renderer as TabBarRenderer; if (n.isTieOrigin && renderer.showTiedNotes && n.tieDestination!.isVisible) { - const tie: TabTieGlyph = new TabTieGlyph(n, n.tieDestination!, false); + const tie: TabTieGlyph = new TabTieGlyph(`tab.tie.${n.id}`, n, n.tieDestination!, false); this.addTie(tie); } if (n.isTieDestination && renderer.showTiedNotes) { - const tie: TabTieGlyph = new TabTieGlyph(n.tieOrigin!, n, true); + const tie: TabTieGlyph = new TabTieGlyph(`tab.tie.${n.tieOrigin!.id}`, n.tieOrigin!, n, true); this.addTie(tie); } if (n.isLeftHandTapped && !n.isHammerPullDestination) { - const tapSlur: TabTieGlyph = new TabTieGlyph(n, n, false); + const tapSlur: TabTieGlyph = new TabTieGlyph(`tab.tie.leftHandTap.${n.id}`, n, n, false); this.addTie(tapSlur); } // start effect slur on first beat @@ -57,7 +57,13 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { } } if (!expanded) { - const effectSlur: TabSlurGlyph = new TabSlurGlyph(n, n.effectSlurDestination, false, false); + const effectSlur: TabSlurGlyph = new TabSlurGlyph( + `tab.slur.effect.${n.id}`, + n, + n.effectSlurDestination, + false, + false + ); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); } @@ -72,7 +78,7 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { } } if (!expanded) { - const effectSlur: TabSlurGlyph = new TabSlurGlyph(n.effectSlurOrigin, n, false, true); + const effectSlur: TabSlurGlyph = new TabSlurGlyph(`tab.slur.effect.${n.effectSlurOrigin.id}`, n.effectSlurOrigin, n, false, true); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); } diff --git a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts index 72c4e74a3..7567f5092 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts @@ -117,7 +117,7 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { } } else { const line = Math.floor((this.renderer.bar.staff.tuning.length - 1) / 2); - const y: number = tabRenderer.getTabY(line); + const y: number = tabRenderer.getLineY(line); const restGlyph = new TabRestGlyph(0, y, tabRenderer.showRests, this.container.beat.duration); this.restGlyph = restGlyph; restGlyph.beat = this.container.beat; @@ -174,8 +174,8 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { private _createNoteGlyph(n: Note): void { const tr: TabBarRenderer = this.renderer as TabBarRenderer; const noteNumberGlyph: NoteNumberGlyph = new NoteNumberGlyph(0, 0, n); - const l: number = n.beat.voice.bar.staff.tuning.length - n.string; - noteNumberGlyph.y = tr.getTabY(l); + const l: number = tr.getNoteLine(n); + noteNumberGlyph.y = tr.getLineY(l); noteNumberGlyph.renderer = this.renderer; noteNumberGlyph.doLayout(); this.noteNumbers!.addNoteGlyph(noteNumberGlyph, n); diff --git a/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts index f1cfd3888..6bf9d8acf 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts @@ -243,7 +243,7 @@ export class TabBendGlyph extends Glyph implements ITieGlyph { while (endNote.isTieOrigin) { const nextNote = endNote.tieDestination!; endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, nextNote.beat.voice.bar ); if (!endNoteRenderer || startNoteRenderer.staff !== endNoteRenderer.staff) { @@ -263,7 +263,7 @@ export class TabBendGlyph extends Glyph implements ITieGlyph { endBeat = endNote.beat; endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, endBeat.voice.bar ) as TabBarRenderer; if ( diff --git a/packages/alphatab/src/rendering/glyphs/TabSlideLineGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabSlideLineGlyph.ts index 5266e4970..53c4011f0 100644 --- a/packages/alphatab/src/rendering/glyphs/TabSlideLineGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabSlideLineGlyph.ts @@ -116,7 +116,7 @@ export class TabSlideLineGlyph extends Glyph implements ITieGlyph { if (this._startNote.slideTarget) { const endNoteRenderer: BarRendererBase | null = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, this._startNote.slideTarget.beat.voice.bar ); if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { diff --git a/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts index 01f8576b5..97ba66030 100644 --- a/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts @@ -1,25 +1,20 @@ import type { Note } from '@coderline/alphatab/model/Note'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { TabTieGlyph } from '@coderline/alphatab/rendering/glyphs/TabTieGlyph'; -import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ export class TabSlurGlyph extends TabTieGlyph { - private _direction: BeamDirection; private _forSlide: boolean; - public constructor(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean = false) { - super(startNote, endNote, forEnd); - this._direction = TabTieGlyph.getBeamDirectionForNote(startNote); + public constructor(slurEffectId: string, startNote: Note, endNote: Note, forSlide: boolean, forEnd:boolean) { + super(slurEffectId, startNote, endNote, forEnd); this._forSlide = forSlide; } - protected override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { - return Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight / 2; + public override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { + return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2; } public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean): boolean { @@ -27,9 +22,6 @@ export class TabSlurGlyph extends TabTieGlyph { if (this._forSlide !== forSlide) { return false; } - if (this.forEnd !== forEnd) { - return false; - } // same start and endbeat if (this.startNote.beat.id !== startNote.beat.id) { return false; @@ -37,48 +29,33 @@ export class TabSlurGlyph extends TabTieGlyph { if (this.endNote.beat.id !== endNote.beat.id) { return false; } + const isForEnd = this.renderer === this.lookupEndBeatRenderer(); + if (isForEnd !== forEnd) { + return false; + } // same draw direction - if (this._direction !== TabTieGlyph.getBeamDirectionForNote(startNote)) { + if (this.tieDirection !== TabTieGlyph.getBeamDirectionForNote(startNote)) { return false; } // if we can expand, expand in correct direction - switch (this._direction) { + switch (this.tieDirection) { case BeamDirection.Up: if (startNote.realValue > this.startNote.realValue) { this.startNote = startNote; - this.startBeat = startNote.beat; } if (endNote.realValue > this.endNote.realValue) { this.endNote = endNote; - this.endBeat = endNote.beat; } break; case BeamDirection.Down: if (startNote.realValue < this.startNote.realValue) { this.startNote = startNote; - this.startBeat = startNote.beat; } if (endNote.realValue < this.endNote.realValue) { this.endNote = endNote; - this.endBeat = endNote.beat; } break; } return true; } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - const startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, - this.startBeat!.voice.bar - )!; - const direction: BeamDirection = this.getBeamDirection(this.startBeat!, startNoteRenderer); - const slurId: string = `tab.slur.${this.startNote.beat.id}.${this.endNote.beat.id}.${direction}`; - const renderer: TabBarRenderer = this.renderer as TabBarRenderer; - const isSlurRendered: boolean = renderer.staff.getSharedLayoutData(slurId, false); - if (!isSlurRendered) { - renderer.staff.setSharedLayoutData(slurId, true); - super.paint(cx, cy, canvas); - } - } } diff --git a/packages/alphatab/src/rendering/glyphs/TabTieGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabTieGlyph.ts index def857990..4e5963185 100644 --- a/packages/alphatab/src/rendering/glyphs/TabTieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabTieGlyph.ts @@ -1,35 +1,13 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; -import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { NoteTieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class TabTieGlyph extends TieGlyph { - protected startNote: Note; - protected endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(startNote.beat, endNote.beat, forEnd); - this.startNote = startNote; - this.endNote = endNote; - } - - private get _isLeftHandTap() { - return this.startNote === this.endNote; - } - - protected override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { - if (this._isLeftHandTap) { - return this.startNoteRenderer!.smuflMetrics.tieHeight; - } - return super.getTieHeight(startX, startY, endX, endY); - } - - protected override getBeamDirection(_beat: Beat, _noteRenderer: BarRendererBase): BeamDirection { - if (this._isLeftHandTap) { +export class TabTieGlyph extends NoteTieGlyph { + protected override calculateTieDirection(): BeamDirection { + if (this.isLeftHandTap) { return BeamDirection.Up; } return TabTieGlyph.getBeamDirectionForNote(this.startNote); @@ -38,33 +16,4 @@ export class TabTieGlyph extends TieGlyph { protected static getBeamDirectionForNote(note: Note): BeamDirection { return note.string > 3 ? BeamDirection.Up : BeamDirection.Down; } - - protected override getStartY(): number { - if (this._isLeftHandTap) { - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Center); - } - - if (this.tieDirection === BeamDirection.Up) { - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); - } - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Bottom); - } - - protected override getEndY(): number { - return this.getStartY(); - } - - protected override getStartX(): number { - if (this._isLeftHandTap) { - return this.getEndX() - this.renderer.smuflMetrics.leftHandTabTieWidth; - } - return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Center); - } - - protected override getEndX(): number { - if (this._isLeftHandTap) { - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); - } - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Center); - } } diff --git a/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts index 8e72b6ac2..d80c39c9a 100644 --- a/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts @@ -132,7 +132,7 @@ export class TabWhammyBarGlyph extends EffectGlyph { let endXPositionType: BeatXPosition = BeatXPosition.PreNotes; if (endBeat) { endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, endBeat.voice.bar ) as LineBarRenderer | null; if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { diff --git a/packages/alphatab/src/rendering/glyphs/TieGlyph.ts b/packages/alphatab/src/rendering/glyphs/TieGlyph.ts index 5ab1401fc..84d3af21d 100644 --- a/packages/alphatab/src/rendering/glyphs/TieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TieGlyph.ts @@ -1,6 +1,6 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; +import type { Note } from '@coderline/alphatab/model/Note'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import { type BarRendererBase, NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; @@ -9,27 +9,25 @@ import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; * @internal */ export interface ITieGlyph { + /** + * Whether the tie is relevant for checking on bar renderer overflows. + * If set, the tie bounds will be requested and the overflow is applied. + */ readonly checkForOverflow: boolean; } /** * @internal */ -export class TieGlyph extends Glyph implements ITieGlyph { - protected startBeat: Beat | null; - protected endBeat: Beat | null; - protected yOffset: number = 0; - protected forEnd: boolean; +export abstract class TieGlyph extends Glyph implements ITieGlyph { + public tieDirection: BeamDirection = BeamDirection.Up; + public readonly slurEffectId: string; + protected isForEnd:boolean; - protected startNoteRenderer: BarRendererBase | null = null; - protected endNoteRenderer: BarRendererBase | null = null; - protected tieDirection: BeamDirection = BeamDirection.Up; - - public constructor(startBeat: Beat | null, endBeat: Beat | null, forEnd: boolean) { + public constructor(slurEffectId: string, forEnd:boolean) { super(0, 0); - this.startBeat = startBeat; - this.endBeat = endBeat; - this.forEnd = forEnd; + this.slurEffectId = slurEffectId; + this.isForEnd = forEnd; } private _startX: number = 0; @@ -37,11 +35,11 @@ export class TieGlyph extends Glyph implements ITieGlyph { private _endX: number = 0; private _endY: number = 0; private _tieHeight: number = 0; - private _shouldDraw: boolean = false; private _boundingBox?: Bounds; + private _shouldPaint: boolean = false; public get checkForOverflow() { - return this._boundingBox !== undefined; + return this._shouldPaint && this._boundingBox !== undefined; } public override getBoundingBoxTop(): number { @@ -60,152 +58,161 @@ export class TieGlyph extends Glyph implements ITieGlyph { public override doLayout(): void { this.width = 0; - // TODO fix nullability of start/end beat, - if (!this.endBeat) { - this._shouldDraw = false; - return; - } - const startNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, - this.startBeat!.voice.bar - ); - this.startNoteRenderer = startNoteRenderer; - const endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, - this.endBeat.voice.bar - ); - this.endNoteRenderer = endNoteRenderer; + const startNoteRenderer = this.lookupStartBeatRenderer(); + const endNoteRenderer = this.lookupEndBeatRenderer(); this._startX = 0; this._endX = 0; this._startY = 0; this._endY = 0; this.height = 0; - this._shouldDraw = false; // if we are on the tie start, we check if we // either can draw till the end note, or we just can draw till the bar end - this.tieDirection = !startNoteRenderer - ? this.getBeamDirection(this.endBeat, endNoteRenderer!) - : this.getBeamDirection(this.startBeat!, startNoteRenderer); - if (!this.forEnd && startNoteRenderer) { - // line break or bar break + this.tieDirection = this.calculateTieDirection(); + + const forEnd = this.isForEnd; + this._shouldPaint = false; + + if (!forEnd) { if (startNoteRenderer !== endNoteRenderer) { - this._startX = startNoteRenderer.x + this.getStartX(); - this._startY = startNoteRenderer.y + this.getStartY() + this.yOffset; - // line break: to bar end + this._startX = this.calculateStartX(); + this._startY = this.calculateStartY(); if (!endNoteRenderer || startNoteRenderer.staff !== endNoteRenderer.staff) { - this._endX = startNoteRenderer.x + startNoteRenderer.width; + const lastRendererInStaff = + startNoteRenderer.staff!.barRenderers[startNoteRenderer.staff!.barRenderers.length - 1]; + + this._endX = lastRendererInStaff.x + lastRendererInStaff.width; this._endY = this._startY; + + startNoteRenderer.scoreRenderer.layout!.slurRegistry.startMultiSystemSlur(this); } else { - this._endX = endNoteRenderer.x + this.getEndX(); - this._endY = endNoteRenderer.y + this.getEndY() + this.yOffset; + this._endX = this.calculateEndX(); + this._endY = this.caclculateEndY(); } } else { - this._startX = startNoteRenderer.x + this.getStartX(); - this._endX = endNoteRenderer.x + this.getEndX(); - this._startY = startNoteRenderer.y + this.getStartY() + this.yOffset; - this._endY = endNoteRenderer.y + this.getEndY() + this.yOffset; + this._shouldPaint = true; + this._startX = this.calculateStartX(); + this._endX = this.calculateEndX(); + this._startY = this.calculateStartY(); + this._endY = this.caclculateEndY(); } - this._shouldDraw = true; - } else if (!startNoteRenderer || startNoteRenderer.staff !== endNoteRenderer!.staff) { - this._startX = endNoteRenderer!.x; - this._endX = endNoteRenderer!.x + this.getEndX(); - this._startY = endNoteRenderer!.y + this.getEndY() + this.yOffset; - this._endY = this._startY; - this._shouldDraw = true; + this._shouldPaint = true; + } else if (startNoteRenderer.staff !== endNoteRenderer!.staff) { + const firstRendererInStaff = startNoteRenderer.staff!.barRenderers[0]; + this._startX = firstRendererInStaff!.x; + + this._endX = this.calculateEndX(); + + const startGlyph = startNoteRenderer.scoreRenderer.layout!.slurRegistry.completeMultiSystemSlur(this); + if (startGlyph) { + this._startY = startGlyph.calculateMultiSystemSlurY(endNoteRenderer!); + } else { + this._startY = this.caclculateEndY(); + } + + this._endY = this.caclculateEndY(); + + this._shouldPaint = startNoteRenderer.staff !== endNoteRenderer!.staff; } this._boundingBox = undefined; - if (this._shouldDraw) { - this.y = Math.min(this._startY, this._endY); - if (this.shouldDrawBendSlur()) { - this._tieHeight = 0; // TODO: Bend slur height to be considered? - } else { - this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY); - - const tieBoundingBox = TieGlyph.calculateActualTieHeight( - 1, - this._startX, - this._startY, - this._endX, - this._endY, - this.tieDirection === BeamDirection.Down, - this._tieHeight, - this.renderer.smuflMetrics.tieMidpointThickness - ); - this._boundingBox = tieBoundingBox; - - this.height = tieBoundingBox.h; - - if (this.tieDirection === BeamDirection.Up) { - // the tie might go above `this.y` due to its shape - // here we calculate how much this is so we can consider the - // respective overflow - const overlap = this.y - tieBoundingBox.y; - if (overlap > 0) { - this.y -= overlap; - } + this.y = Math.min(this._startY, this._endY); + if (this.shouldDrawBendSlur()) { + this._tieHeight = 0; // TODO: Bend slur height to be considered? + } else { + this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY); + + const tieBoundingBox = TieGlyph.calculateActualTieHeight( + 1, + this._startX, + this._startY, + this._endX, + this._endY, + this.tieDirection === BeamDirection.Down, + this._tieHeight, + this.renderer.smuflMetrics.tieMidpointThickness + ); + this._boundingBox = tieBoundingBox; + + this.height = tieBoundingBox.h; + + if (this.tieDirection === BeamDirection.Up) { + // the tie might go above `this.y` due to its shape + // here we calculate how much this is so we can consider the + // respective overflow + const overlap = this.y - tieBoundingBox.y; + if (overlap > 0) { + this.y -= overlap; } } } } public override paint(cx: number, cy: number, canvas: ICanvas): void { - if (this._shouldDraw) { - if (this.shouldDrawBendSlur()) { - TieGlyph.drawBendSlur( - canvas, - cx + this._startX, - cy + this._startY, - cx + this._endX, - cy + this._endY, - this.tieDirection === BeamDirection.Down, - 1, - this.renderer.smuflMetrics.tieHeight - ); - } else { - TieGlyph.paintTie( - canvas, - 1, - cx + this._startX, - cy + this._startY, - cx + this._endX, - cy + this._endY, - this.tieDirection === BeamDirection.Down, - this._tieHeight, - this.renderer.smuflMetrics.tieMidpointThickness - ); - } + if (!this._shouldPaint) { + return; } - } - protected shouldDrawBendSlur() { - return false; + if (this.shouldDrawBendSlur()) { + TieGlyph.drawBendSlur( + canvas, + cx + this._startX, + cy + this._startY, + cx + this._endX, + cy + this._endY, + this.tieDirection === BeamDirection.Down, + 1, + this.renderer.smuflMetrics.tieHeight + ); + } else { + TieGlyph.paintTie( + canvas, + 1, + cx + this._startX, + cy + this._startY, + cx + this._endX, + cy + this._endY, + this.tieDirection === BeamDirection.Down, + this._tieHeight, + this.renderer.smuflMetrics.tieMidpointThickness + ); + } } - protected getTieHeight(_startX: number, _startY: number, _endX: number, _endY: number): number { + protected abstract shouldDrawBendSlur(): boolean; + + public getTieHeight(_startX: number, _startY: number, _endX: number, _endY: number): number { return this.renderer.smuflMetrics.tieHeight; } - protected getBeamDirection(_beat: Beat, _noteRenderer: BarRendererBase): BeamDirection { - return BeamDirection.Down; - } + protected abstract calculateTieDirection(): BeamDirection; - protected getStartY(): number { - return 0; - } + protected abstract lookupStartBeatRenderer(): BarRendererBase; + protected abstract lookupEndBeatRenderer(): BarRendererBase | null; - protected getEndY(): number { - return 0; - } + protected abstract calculateStartY(): number; + + protected abstract caclculateEndY(): number; + + protected abstract calculateStartX(): number; + + protected abstract calculateEndX(): number; - protected getStartX(): number { - return 0; + public calculateMultiSystemSlurY(renderer: BarRendererBase) { + const startRenderer = this.lookupStartBeatRenderer(); + const startY = this.calculateStartY(); + const relY = startY - startRenderer.y; + return renderer.y + relY; } - protected getEndX(): number { - return 0; + public shouldCreateMultiSystemSlur(renderer: BarRendererBase) { + const endStaff = this.lookupEndBeatRenderer()?.staff; + if (!endStaff) { + return true; + } + + return renderer.staff!.system.index < endStaff.system.index; } public static calculateActualTieHeight( @@ -219,6 +226,9 @@ export class TieGlyph extends Glyph implements ITieGlyph { size: number ): Bounds { const cp = TieGlyph._computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size); + if (cp.length === 0){ + return new Bounds(x1, y1, x2 - x1, y2 - y1); + } // For a musical tie/slur, the extrema occur predictably near the midpoint // Evaluate at midpoint (t=0.5) and check endpoints @@ -451,3 +461,169 @@ export class TieGlyph extends Glyph implements ITieGlyph { } } } + +/** + * A common tie implementation using note details for positioning + * @internal + */ +export abstract class NoteTieGlyph extends TieGlyph { + protected startNote: Note; + protected endNote: Note; + protected startNoteRenderer: BarRendererBase | null = null; + protected endNoteRenderer: BarRendererBase | null = null; + + public constructor(slurEffectId: string, startNote: Note, endNote: Note, forEnd:boolean) { + super(slurEffectId, forEnd); + this.startNote = startNote; + this.endNote = endNote; + } + + protected get isLeftHandTap() { + return this.startNote === this.endNote; + } + + public override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { + if (this.isLeftHandTap) { + return this.renderer!.smuflMetrics.tieHeight; + } + return super.getTieHeight(startX, startY, endX, endY); + } + + protected override calculateTieDirection(): BeamDirection { + // invert direction (if stems go up, ties go down to not cross them) + switch (this.lookupStartBeatRenderer().getBeatDirection(this.startNote.beat)) { + case BeamDirection.Up: + return BeamDirection.Down; + default: + return BeamDirection.Up; + } + } + + protected override calculateStartX(): number { + const startNoteRenderer = this.lookupStartBeatRenderer(); + if (this.isLeftHandTap) { + return this.calculateEndX() - startNoteRenderer.smuflMetrics.leftHandTabTieWidth; + } + return startNoteRenderer.x + startNoteRenderer!.getNoteX(this.startNote, this.getStartNotePosition()); + } + + protected getStartNotePosition() { + return NoteXPosition.Center; + } + + protected override calculateStartY(): number { + const startNoteRenderer = this.lookupStartBeatRenderer(); + if (this.isLeftHandTap) { + return startNoteRenderer.y + startNoteRenderer.getNoteY(this.startNote, NoteYPosition.Center); + } + + switch (this.tieDirection) { + case BeamDirection.Up: + return startNoteRenderer.y + startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); + default: + return startNoteRenderer.y + startNoteRenderer.getNoteY(this.startNote, NoteYPosition.Bottom); + } + } + + protected override calculateEndX(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartY() + this.renderer.smuflMetrics.leftHandTabTieWidth; + } + if (this.isLeftHandTap) { + return endNoteRenderer!.x + endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); + } + return endNoteRenderer.x + endNoteRenderer.getNoteX(this.endNote, NoteXPosition.Center); + } + + protected getEndNotePosition() { + return NoteXPosition.Center; + } + + protected override caclculateEndY(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartY(); + } + + if (this.isLeftHandTap) { + return endNoteRenderer.y + endNoteRenderer!.getNoteY(this.endNote, NoteYPosition.Center); + } + + switch (this.tieDirection) { + case BeamDirection.Up: + return endNoteRenderer.y + endNoteRenderer!.getNoteY(this.endNote, NoteYPosition.Top); + default: + return endNoteRenderer.y + endNoteRenderer!.getNoteY(this.endNote, NoteYPosition.Bottom); + } + } + + protected override lookupEndBeatRenderer(): BarRendererBase | null { + if (!this.endNoteRenderer) { + this.endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff!.staffId, + this.endNote.beat.voice.bar + ); + } + return this.endNoteRenderer; + } + + protected override lookupStartBeatRenderer(): BarRendererBase { + if (!this.startNoteRenderer) { + this.startNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff!.staffId, + this.startNote.beat.voice.bar + )!; + } + return this.startNoteRenderer; + } + + protected override shouldDrawBendSlur(): boolean { + return false; + } +} + +/** + * A tie glyph for continued multi-system ties/slurs + * @internal + */ +export class ContinuationTieGlyph extends TieGlyph { + private _startTie: TieGlyph; + + public constructor(startTie: TieGlyph) { + super(startTie.slurEffectId, false); + this._startTie = startTie; + } + + protected override lookupStartBeatRenderer(): BarRendererBase { + return this.renderer; + } + + protected override lookupEndBeatRenderer(): BarRendererBase { + return this.renderer; + } + + protected override shouldDrawBendSlur(): boolean { + return false; + } + + protected override calculateTieDirection(): BeamDirection { + return this._startTie.tieDirection; + } + + protected override calculateStartY(): number { + return this._startTie.calculateMultiSystemSlurY(this.renderer); + } + protected override caclculateEndY(): number { + return this.calculateStartY(); + } + + protected override calculateStartX(): number { + return this.renderer.staff!.barRenderers[0].x; + } + + protected override calculateEndX(): number { + const last = this.renderer.staff!.barRenderers[this.renderer.staff!.barRenderers.length - 1]; + return last.x + last.width; + } +} diff --git a/packages/alphatab/src/rendering/layout/PageViewLayout.ts b/packages/alphatab/src/rendering/layout/PageViewLayout.ts index fe26e9672..ef2c5c932 100644 --- a/packages/alphatab/src/rendering/layout/PageViewLayout.ts +++ b/packages/alphatab/src/rendering/layout/PageViewLayout.ts @@ -216,6 +216,14 @@ export class PageViewLayout extends ScoreLayout { y += this._paintSystem(system, oldHeight); } } else { + // clear out staves during re-layout, this info is outdated during + // re-layout of the bars + for (const r of this._allMasterBarRenderers) { + for (const b of r.renderers) { + b.afterReverted(); + } + } + this._systems = []; let currentIndex: number = 0; const maxWidth: number = this._maxWidth; diff --git a/packages/alphatab/src/rendering/layout/ScoreLayout.ts b/packages/alphatab/src/rendering/layout/ScoreLayout.ts index 1ccc47c2b..d33a5be29 100644 --- a/packages/alphatab/src/rendering/layout/ScoreLayout.ts +++ b/packages/alphatab/src/rendering/layout/ScoreLayout.ts @@ -15,6 +15,7 @@ import { ChordDiagramContainerGlyph } from '@coderline/alphatab/rendering/glyphs import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; import { TuningContainerGlyph } from '@coderline/alphatab/rendering/glyphs/TuningContainerGlyph'; import { TuningGlyph } from '@coderline/alphatab/rendering/glyphs/TuningGlyph'; +import { SlurRegistry } from '@coderline/alphatab/rendering/layout/SlurRegistry'; import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; @@ -94,14 +95,20 @@ export abstract class ScoreLayout { public abstract get firstBarX(): number; public abstract get supportsResize(): boolean; + public slurRegistry = new SlurRegistry(); + public resize(): void { this._lazyPartials.clear(); + this.slurRegistry.clear(); this.doResize(); } public abstract doResize(): void; public layoutAndRender(): void { this._lazyPartials.clear(); + this.slurRegistry.clear(); + this._barRendererLookup.clear(); + this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile)!; const score: Score = this.renderer.score!; @@ -439,18 +446,6 @@ export abstract class ScoreLayout { } } - public unregisterBarRenderer(key: string, renderer: BarRendererBase): void { - if (this._barRendererLookup.has(key)) { - const lookup: Map = this._barRendererLookup.get(key)!; - lookup.delete(renderer.bar.id); - if (renderer.additionalMultiRestBars) { - for (const b of renderer.additionalMultiRestBars) { - lookup.delete(b.id); - } - } - } - } - public getRendererForBar(key: string, bar: Bar): BarRendererBase | null { const barRendererId: number = bar.id; if (this._barRendererLookup.has(key) && this._barRendererLookup.get(key)!.has(barRendererId)) { diff --git a/packages/alphatab/src/rendering/layout/SlurRegistry.ts b/packages/alphatab/src/rendering/layout/SlurRegistry.ts new file mode 100644 index 000000000..9a1a0d0e3 --- /dev/null +++ b/packages/alphatab/src/rendering/layout/SlurRegistry.ts @@ -0,0 +1,87 @@ +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; + +/** + * @internal + * @record + */ +interface SlurRegistration { + startGlyph: TieGlyph; + endGlyph?: TieGlyph; +} + +/** + * Holds the slur information specific for an individual staff + * @internal + * @record + */ +interface SlurInfoContainer { + /** + * A set of started slurs and ties. + */ + startedSlurs: Map; +} + +/** + * This registry keeps track of which slurs and ties were started and needs completion. + * Slurs might span multiple systems, and in such cases we need to create additional + * slur/ties in the intermediate and end system. + * + * @internal + * + */ +export class SlurRegistry { + private _staffLookup = new Map(); + + public clear() { + this._staffLookup.clear(); + } + + public startMultiSystemSlur(startGlyph: TieGlyph) { + const staffId = SlurRegistry._staffId(startGlyph.renderer.staff!); + let container: SlurInfoContainer; + if (!this._staffLookup.has(staffId)) { + container = { + startedSlurs: new Map() + }; + this._staffLookup.set(staffId, container); + } else { + container = this._staffLookup.get(staffId)!; + } + + container.startedSlurs.set(startGlyph.slurEffectId, { startGlyph }); + } + + private static _staffId(staff: RenderStaff): string { + return `${staff.modelStaff.index}.${staff.modelStaff.track.index}.${staff.staffId}`; + } + + public completeMultiSystemSlur(endGlyph: TieGlyph) { + const staffId = SlurRegistry._staffId(endGlyph.renderer.staff!); + if (!this._staffLookup.has(staffId)) { + return undefined; + } + const container = this._staffLookup.get(staffId)!; + if (container.startedSlurs.has(endGlyph.slurEffectId)) { + const info = container.startedSlurs.get(endGlyph.slurEffectId)!; + info.endGlyph = endGlyph; + return info.startGlyph; + } + return undefined; + } + + public *getAllContinuations(renderer: BarRendererBase): Generator { + const staffId = SlurRegistry._staffId(renderer.staff!); + if (!this._staffLookup.has(staffId) || renderer.index > 0) { + return; + } + + const container = this._staffLookup.get(staffId)!; + for (const g of container.startedSlurs.values()) { + if (g.startGlyph.shouldCreateMultiSystemSlur(renderer)) { + yield g.startGlyph; + } + } + } +} diff --git a/packages/alphatab/src/rendering/staves/RenderStaff.ts b/packages/alphatab/src/rendering/staves/RenderStaff.ts index e039671e3..706f6c79d 100644 --- a/packages/alphatab/src/rendering/staves/RenderStaff.ts +++ b/packages/alphatab/src/rendering/staves/RenderStaff.ts @@ -30,7 +30,9 @@ export class RenderStaff { public index: number = 0; public staffIndex: number = 0; - public get isFirstInSystem() { return this.index === 0} + public get isFirstInSystem() { + return this.index === 0; + } public topEffectInfos: EffectBandInfo[] = []; public bottomEffectInfos: EffectBandInfo[] = []; @@ -155,14 +157,14 @@ export class RenderStaff { public revertLastBar(): BarRendererBase { this._sharedLayoutData = new Map(); + const lastBar: BarRendererBase = this.barRenderers[this.barRenderers.length - 1]; this.barRenderers.splice(this.barRenderers.length - 1, 1); - this.system.layout.unregisterBarRenderer(this.staffId, lastBar); this.topOverflow = 0; this.bottomOverflow = 0; for (const r of this.barRenderers) { r.afterStaffBarReverted(); - } + } return lastBar; } @@ -267,25 +269,25 @@ export class RenderStaff { // changes in the overflows let needsSecondPass = false; let topOverflow: number = this.topOverflow; - for (let i: number = 0; i < this.barRenderers.length; i++) { - this.barRenderers[i].y = this.topPadding + topOverflow; - if (this.barRenderers[i].finalizeRenderer()) { + for (const renderer of this.barRenderers) { + renderer.registerMultiSystemSlurs(this.system.layout!.slurRegistry.getAllContinuations(renderer)); + if (renderer.finalizeRenderer()) { needsSecondPass = true; } - this.height = Math.max(this.height, this.barRenderers[i].height); + this.height = Math.max(this.height, renderer.height); } // 2nd pass: move renderers to correct position respecting the new overflows if (needsSecondPass) { topOverflow = this.topOverflow; // shift all the renderers to the new position to match required spacing - for (let i: number = 0; i < this.barRenderers.length; i++) { - this.barRenderers[i].y = this.topPadding + topOverflow; + for (const renderer of this.barRenderers) { + renderer.y = this.topPadding + topOverflow; } // finalize again (to align ties) - for (let i: number = 0; i < this.barRenderers.length; i++) { - this.barRenderers[i].finalizeRenderer(); + for (const renderer of this.barRenderers) { + renderer.finalizeRenderer(); } } diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index ea3efb4b2..7e3c14335 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -253,6 +253,7 @@ export class StaffSystem { if (newBarDisplayScale > barDisplayScale) { barDisplayScale = newBarDisplayScale; } + lastBar.afterReverted(); } this.width -= width; this.computedWidth -= width; diff --git a/packages/alphatab/src/rendering/utils/AccidentalHelper.ts b/packages/alphatab/src/rendering/utils/AccidentalHelper.ts index dead551fc..bafc75714 100644 --- a/packages/alphatab/src/rendering/utils/AccidentalHelper.ts +++ b/packages/alphatab/src/rendering/utils/AccidentalHelper.ts @@ -204,7 +204,7 @@ export class AccidentalHelper { if (note && note.isTieDestination && note.beat.index === 0) { // candidate for skip, check further if start note is on the same steps const tieOriginBarRenderer = this._barRenderer.scoreRenderer.layout?.getRendererForBar( - this._barRenderer.staff.staffId, + this._barRenderer.staff!.staffId, note.tieOrigin!.beat.voice.bar ) as ScoreBarRenderer | null; if (tieOriginBarRenderer && tieOriginBarRenderer.staff === this._barRenderer.staff) { diff --git a/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png b/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png index de10b7e0e..6d35891d6 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png and b/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png b/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png index f724f38f9..21be0a155 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png and b/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozartTrio.png b/packages/alphatab/test-data/musicxml-samples/MozartTrio.png index 9dac93ed1..3335d8b3e 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozartTrio.png and b/packages/alphatab/test-data/musicxml-samples/MozartTrio.png differ diff --git a/packages/alphatab/test-data/musicxml3/chord-diagram.png b/packages/alphatab/test-data/musicxml3/chord-diagram.png index 8a5005c91..fb7e246d2 100644 Binary files a/packages/alphatab/test-data/musicxml3/chord-diagram.png and b/packages/alphatab/test-data/musicxml3/chord-diagram.png differ diff --git a/packages/alphatab/test-data/musicxml3/tie-destination.png b/packages/alphatab/test-data/musicxml3/tie-destination.png index eda41762a..6b002da92 100644 Binary files a/packages/alphatab/test-data/musicxml3/tie-destination.png and b/packages/alphatab/test-data/musicxml3/tie-destination.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-0-1300.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-0-1300.png new file mode 100644 index 000000000..0b8712152 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-0-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-1-600.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-1-600.png new file mode 100644 index 000000000..aafe16457 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-1-600.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-2-300.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-2-300.png new file mode 100644 index 000000000..ba8acf74f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-2-300.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-3-700.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-3-700.png new file mode 100644 index 000000000..169e4ecc7 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-3-700.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-0-600.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-0-600.png new file mode 100644 index 000000000..a3f77c935 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-0-600.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-1-1300.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-1-1300.png new file mode 100644 index 000000000..b07c2fa24 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-1-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-2-700.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-2-700.png new file mode 100644 index 000000000..7580008c8 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-2-700.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-3-300.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-3-300.png new file mode 100644 index 000000000..ba8acf74f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-3-300.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/slash.png b/packages/alphatab/test-data/visual-tests/special-tracks/slash.png index 854c44cff..ce550c7aa 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-tracks/slash.png and b/packages/alphatab/test-data/visual-tests/special-tracks/slash.png differ diff --git a/packages/alphatab/test/visualTests/VisualTestHelper.ts b/packages/alphatab/test/visualTests/VisualTestHelper.ts index c3dcc14be..99f700115 100644 --- a/packages/alphatab/test/visualTests/VisualTestHelper.ts +++ b/packages/alphatab/test/visualTests/VisualTestHelper.ts @@ -32,22 +32,18 @@ export class VisualTestRun { export class VisualTestOptions { public score: Score; public runs: VisualTestRun[]; - public settings?: Settings; + public settings: Settings; public tracks?: number[]; public tolerancePercent?: number; public prepareFullImage?: (run: VisualTestRun, api: AlphaTabApiBase, fullImage: AlphaSkiaCanvas) => void; - public constructor(score: Score, runs: VisualTestRun[], settings?: Settings) { + public constructor(score: Score, runs: VisualTestRun[], settings: Settings | undefined) { this.score = score; this.runs = runs; - this.settings = settings; + this.settings = settings ?? new Settings(); } public static async file(inputFile: string, runs: VisualTestRun[], settings?: Settings) { - if (!settings) { - settings = new Settings(); - } - const inputFileData = await TestPlatform.loadFile(`test-data/visual-tests/${inputFile}`); const score: Score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); @@ -91,8 +87,17 @@ export class VisualTestHelper { await VisualTestHelper.runVisualTestFull(o); } - public static runVisualTestTex(tex: string, referenceFileName: string, settings?: Settings): Promise { - return VisualTestHelper.runVisualTestFull(VisualTestOptions.tex(tex, referenceFileName, settings)); + public static runVisualTestTex( + tex: string, + referenceFileName: string, + settings?: Settings, + configure?: (o: VisualTestOptions) => void + ): Promise { + const o = VisualTestOptions.tex(tex, referenceFileName, settings); + if (configure) { + configure(o); + } + return VisualTestHelper.runVisualTestFull(o); } public static async runVisualTestFull(options: VisualTestOptions): Promise { diff --git a/packages/alphatab/test/visualTests/features/Layout.test.ts b/packages/alphatab/test/visualTests/features/Layout.test.ts index 56038f0a2..94ed0628f 100644 --- a/packages/alphatab/test/visualTests/features/Layout.test.ts +++ b/packages/alphatab/test/visualTests/features/Layout.test.ts @@ -166,4 +166,46 @@ describe('LayoutTests', () => { o.tracks = [0, 1]; }); }); + + it('multi-system-slur-scale-down', async () => { + await VisualTestHelper.runVisualTestTex( + ` + C4 {slur S1} + | r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r + A4 {slur S1} + `, + '', + undefined, + o => { + o.score.stylesheet.extendBarLines = true; + o.runs = [ + new VisualTestRun(1300, 'test-data/visual-tests/layout/multi-system-slur-scale-down-0-1300.png'), + new VisualTestRun(600, 'test-data/visual-tests/layout/multi-system-slur-scale-down-1-600.png'), + new VisualTestRun(300, 'test-data/visual-tests/layout/multi-system-slur-scale-down-2-300.png'), + new VisualTestRun(300, 'test-data/visual-tests/layout/multi-system-slur-scale-down-3-700.png') + ]; + } + ); + }); + + it('multi-system-slur-scale-up', async () => { + await VisualTestHelper.runVisualTestTex( + ` + C4 {slur S1} + | r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r + A4 {slur S1} + `, + '', + undefined, + o => { + o.score.stylesheet.extendBarLines = true; + o.runs = [ + new VisualTestRun(600, 'test-data/visual-tests/layout/multi-system-slur-scale-up-0-600.png'), + new VisualTestRun(1300, 'test-data/visual-tests/layout/multi-system-slur-scale-up-1-1300.png'), + new VisualTestRun(700, 'test-data/visual-tests/layout/multi-system-slur-scale-up-2-700.png'), + new VisualTestRun(300, 'test-data/visual-tests/layout/multi-system-slur-scale-up-3-300.png') + ]; + } + ); + }); }); diff --git a/packages/lsp/src/server/utils.ts b/packages/lsp/src/server/utils.ts index 2a54a24d1..ef89014cd 100644 --- a/packages/lsp/src/server/utils.ts +++ b/packages/lsp/src/server/utils.ts @@ -1,6 +1,6 @@ import * as alphaTab from '@coderline/alphatab'; import type { ParameterDefinition, SignatureDefinition } from '@coderline/alphatab-alphatex/types'; -import { ClientCapabilities } from 'vscode-languageserver'; +import type { ClientCapabilities } from 'vscode-languageserver'; function binaryNodeSearchInner( items: T[], diff --git a/packages/transpiler/src/csharp/CSharpAstTransformer.ts b/packages/transpiler/src/csharp/CSharpAstTransformer.ts index e7814ef41..f242c7dff 100644 --- a/packages/transpiler/src/csharp/CSharpAstTransformer.ts +++ b/packages/transpiler/src/csharp/CSharpAstTransformer.ts @@ -2371,6 +2371,23 @@ export default class CSharpAstTransformer { } protected visitReturnStatement(parent: cs.Node, s: ts.ReturnStatement) { + if(this.currentClassElement && ts.isMethodDeclaration(this.currentClassElement) && !!this.currentClassElement.asteriskToken) { + const yieldExpressionStmt = { + expression: null!, + nodeType: cs.SyntaxKind.ExpressionStatement, + parent: parent, + tsNode: s + } as cs.ExpressionStatement + const yieldExpression = { + expression: null, + parent: yieldExpressionStmt, + tsNode: s, + nodeType: cs.SyntaxKind.YieldExpression + } as cs.YieldExpression; + yieldExpressionStmt.expression = yieldExpression; + return yieldExpressionStmt; + } + const returnStatement = { nodeType: cs.SyntaxKind.ReturnStatement, parent: parent, diff --git a/packages/transpiler/src/kotlin/KotlinAstPrinter.ts b/packages/transpiler/src/kotlin/KotlinAstPrinter.ts index 974039b42..665aa2969 100644 --- a/packages/transpiler/src/kotlin/KotlinAstPrinter.ts +++ b/packages/transpiler/src/kotlin/KotlinAstPrinter.ts @@ -1961,7 +1961,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeExpression(expr.expression); this.write(')'); } else { - this.write('return'); + this.write('return@iterator'); } }