diff --git a/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts b/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts index 382dfc8d8..7e450f081 100644 --- a/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts +++ b/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts @@ -1,6 +1,8 @@ +import { Duration } from '@coderline/alphatab/model/Duration'; import type { Note } from '@coderline/alphatab/model/Note'; -import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { type NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; +import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; /** @@ -17,4 +19,26 @@ export abstract class BeatOnNoteGlyphBase extends BeatGlyphBase { public abstract getRestY(requestedPosition: NoteYPosition): number; public abstract getHighestNoteY(requestedPosition: NoteYPosition): number; public abstract getLowestNoteY(requestedPosition: NoteYPosition): number; + + /** + * Resolves the requested position on the given rest glyph. + * Rests have no stems, positions with stems reserve the space of a quarter note stem (e.g. for beams passing by). + */ + protected getRestGlyphY(g: Glyph | null, requestedPosition: NoteYPosition): number { + if (!g) { + return 0; + } + switch (requestedPosition) { + case NoteYPosition.TopWithStem: + return g.getBoundingBoxTop() - this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + case NoteYPosition.Top: + return g.getBoundingBoxTop(); + case NoteYPosition.Bottom: + return g.getBoundingBoxBottom(); + case NoteYPosition.BottomWithStem: + return g.getBoundingBoxBottom() + this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + } + // center and stem positions + return g.getBoundingBoxTop() + g.height / 2; + } } diff --git a/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts index 53735156c..18defc648 100644 --- a/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts @@ -1,8 +1,10 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { Duration } from '@coderline/alphatab/model/Duration'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal @@ -19,6 +21,54 @@ export class NoteHeadGlyphBase extends MusicFontGlyph { } super.paint(cx, cy, canvas); } + + /** + * Gets the x-offset from the left of the note head at which a stem in the given direction attaches. + * @remarks The metrics are passed explicitly as positions are also requested before the note head is laid out. + */ + public getStemX(smufl: EngravingSettings, direction: BeamDirection): number { + if (direction === BeamDirection.Up) { + // stem-up attaches on the right side + return ( + (smufl.stemUp.has(this.symbol) + ? smufl.stemUp.get(this.symbol)!.x + : smufl.glyphWidths.get(this.symbol)!) * this.glyphScale + ); + } + // stem-down attaches on the left side + return (smufl.stemDown.has(this.symbol) ? smufl.stemDown.get(this.symbol)!.x : 0) * this.glyphScale; + } + + /** + * Gets the y-position of the note head at the given position (in the same coordinate space as {@link y}). + * Positions depending on the stem length cannot be resolved by the note head and + * are treated like {@link NoteYPosition.Top} and {@link NoteYPosition.Bottom}. + * @remarks The metrics are passed explicitly as positions are also requested before the note head is laid out. + */ + public getNoteHeadY(smufl: EngravingSettings, requestedPosition: NoteYPosition): number { + switch (requestedPosition) { + case NoteYPosition.TopWithStem: + case NoteYPosition.Top: + return this.y - this.height / 2; + case NoteYPosition.BottomWithStem: + case NoteYPosition.Bottom: + return this.y + this.height / 2; + case NoteYPosition.StemUp: + return ( + this.y - + (smufl.stemUp.has(this.symbol) ? smufl.stemUp.get(this.symbol)!.bottomY : 0) * this.glyphScale + ); + case NoteYPosition.StemDown: + return ( + this.y - + (smufl.stemDown.has(this.symbol) + ? smufl.stemDown.get(this.symbol)!.topY + : -smufl.glyphHeights.get(this.symbol)! / 2) * + this.glyphScale + ); + } + return this.y; + } } /** diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts index e74cba30d..dd39c0f5c 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts @@ -114,24 +114,7 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { } public override getRestY(requestedPosition: NoteYPosition): number { - const g = this.restGlyph; - if (g) { - switch (requestedPosition) { - case NoteYPosition.TopWithStem: - return g.getBoundingBoxTop() - this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); - case NoteYPosition.Top: - return g.getBoundingBoxTop(); - case NoteYPosition.Center: - case NoteYPosition.StemUp: - case NoteYPosition.StemDown: - return g.getBoundingBoxTop() + g.height / 2; - case NoteYPosition.Bottom: - return g.getBoundingBoxBottom(); - case NoteYPosition.BottomWithStem: - return g.getBoundingBoxBottom() + this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); - } - } - return 0; + return this.getRestGlyphY(this.restGlyph, requestedPosition); } public applyRestCollisionOffset() { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts index 05c336ab0..ea472d74a 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts @@ -7,7 +7,6 @@ import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import type { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import { ScoreChordNoteHeadInfo, @@ -25,7 +24,7 @@ import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; * @internal */ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { - private _noteGlyphLookup: Map = new Map(); + private _noteGlyphLookup: Map = new Map(); private _notes: Note[] = []; private _deadSlapped: DeadSlappedBeatGlyph | null = null; private _tremoloPicking: TremoloPickingGlyph | null = null; @@ -103,18 +102,13 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { return this.minStepsNote ? this._internalGetNoteY(this.minStepsNote.glyph, requestedPosition) : 0; } - private _internalGetNoteY(n: MusicFontGlyph, requestedPosition: NoteYPosition): number { - let pos = this.y + n.y; - + private _internalGetNoteY(n: NoteHeadGlyphBase, requestedPosition: NoteYPosition): number { const sr = this.renderer as ScoreBarRenderer; - const scale = this.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; + const scale = this.scale; switch (requestedPosition) { - case NoteYPosition.TopWithStem: - // stem start - pos -= - (sr.smuflMetrics.stemUp.has(n.symbol) ? sr.smuflMetrics.stemUp.get(n.symbol)!.bottomY : 0) * scale; - - // stem size according to duration + case NoteYPosition.TopWithStem: { + // stem start and size according to duration + let pos = this.y + n.getNoteHeadY(sr.smuflMetrics, NoteYPosition.StemUp); pos -= sr.smuflMetrics.getStemLength(this.beat.duration, sr.hasFlag(this.beat)) * scale; pos -= this._stemLengthExtension; @@ -122,22 +116,10 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { topCenterY -= this._stemLengthExtension; return Math.min(topCenterY, pos); - - case NoteYPosition.Top: - pos -= n.height / 2; - break; - case NoteYPosition.Center: - break; - case NoteYPosition.Bottom: - pos += n.height / 2; - break; - case NoteYPosition.BottomWithStem: - pos -= - (this.renderer.smuflMetrics.stemDown.has(n.symbol) - ? this.renderer.smuflMetrics.stemDown.get(n.symbol)!.topY - : -this.renderer.smuflMetrics.glyphHeights.get(n.symbol)! / 2) * scale; - - // stem size according to duration + } + case NoteYPosition.BottomWithStem: { + // stem start and size according to duration + let pos = this.y + n.getNoteHeadY(sr.smuflMetrics, NoteYPosition.StemDown); pos += sr.smuflMetrics.getStemLength(this.beat.duration, sr.hasFlag(this.beat)) * scale; pos += this._stemLengthExtension; @@ -145,20 +127,9 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { bottomCenterY += this._stemLengthExtension; return Math.max(bottomCenterY, pos); - - case NoteYPosition.StemUp: - pos -= - (sr.smuflMetrics.stemUp.has(n.symbol) ? sr.smuflMetrics.stemUp.get(n.symbol)!.bottomY : 0) * scale; - break; - case NoteYPosition.StemDown: - pos -= - (sr.smuflMetrics.stemDown.has(n.symbol) - ? sr.smuflMetrics.stemDown.get(n.symbol)!.topY - : -sr.smuflMetrics.glyphHeights.get(n.symbol)! / 2) * scale; - break; + } } - - return pos; + return this.y + n.getNoteHeadY(sr.smuflMetrics, requestedPosition); } public addMainNoteGlyph(noteGlyph: NoteHeadGlyphBase, note: Note, noteLine: number): void { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts index 5ae9a3717..2f01179d5 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts @@ -484,9 +484,6 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { side: ScoreChordNoteHeadGroupSide, leftOfStem: boolean ) { - const scale = this.scale; - const smufl = this.renderer.smuflMetrics; - for (const stepInfos of side.notes.values()) { // NOTE: for now we do not displace "third" voices even further but they overlap for (const info of stepInfos) { @@ -499,17 +496,9 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { } // shift left/right according to stem position or glyph size else if (leftOfStem) { - // stem-up is the offset on the right side of the notehead - if (smufl.stemUp.has(info.glyph.symbol)) { - info.glyph.x -= smufl.stemUp.get(info.glyph.symbol)!.x * scale; - } else { - info.glyph.x -= smufl.glyphWidths.get(info.glyph.symbol)! * scale; - } + info.glyph.x -= info.glyph.getStemX(this.renderer.smuflMetrics, BeamDirection.Up); } else { - // stem-down is the offset on the left side of the notehead - if (smufl.stemDown.has(info.glyph.symbol)) { - info.glyph.x += smufl.stemDown.get(info.glyph.symbol)!.x * scale; - } + info.glyph.x += info.glyph.getStemX(this.renderer.smuflMetrics, BeamDirection.Down); } // update side @@ -573,26 +562,10 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { } private _updateGroupStemXPosition(info: ScoreNoteGlyphInfo, noteGroup: ScoreChordNoteHeadGroup) { - const smufl = this.renderer.smuflMetrics; - const scale = this.scale; - let stemX: number; - - if (noteGroup.direction === BeamDirection.Up || noteGroup.displacedNotes) { - if (smufl.stemUp.has(info.glyph.symbol)) { - const stemInfo = smufl.stemUp.get(info.glyph.symbol)!; - stemX = stemInfo.x * scale; - } else { - stemX = smufl.glyphWidths.get(info.glyph.symbol)! * scale; - } - } else { - if (smufl.stemDown.has(info.glyph.symbol)) { - const stemInfo = smufl.stemDown.get(info.glyph.symbol)!; - stemX = stemInfo.x * scale; - } else { - stemX = 0; - } - } - + let stemX = info.glyph.getStemX( + this.renderer.smuflMetrics, + noteGroup.direction === BeamDirection.Up || noteGroup.displacedNotes ? BeamDirection.Up : BeamDirection.Down + ); stemX += this.noteStartX; if (stemX > noteGroup.stemX) { diff --git a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts index 60175b124..3fe65e377 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts @@ -1,8 +1,5 @@ -import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { 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 { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; @@ -82,24 +79,7 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { } public override getRestY(requestedPosition: NoteYPosition): number { - const g = this.restGlyph; - if (g) { - switch (requestedPosition) { - case NoteYPosition.TopWithStem: - return g.getBoundingBoxTop() - this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); - case NoteYPosition.Top: - return g.getBoundingBoxTop(); - case NoteYPosition.Center: - case NoteYPosition.StemUp: - case NoteYPosition.StemDown: - return g.getBoundingBoxTop() + g.height / 2; - case NoteYPosition.Bottom: - return g.getBoundingBoxBottom(); - case NoteYPosition.BottomWithStem: - return g.getBoundingBoxBottom() + this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); - } - } - return 0; + return this.getRestGlyphY(this.restGlyph, requestedPosition); } public override getNoteY(_note: Note, requestedPosition: NoteYPosition): number { @@ -107,73 +87,42 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { } public _internalGetNoteY(requestedPosition: NoteYPosition): number { - let g: Glyph | null = null; - let symbol: MusicFontSymbol = MusicFontSymbol.None; - let hasStem = false; - if (this.noteHeads) { - g = this.noteHeads; - symbol = SlashNoteHeadGlyph.getSymbol(this.container.beat.duration); - hasStem = true; - } else if (this.deadSlapped) { - g = this.deadSlapped; - } - - if (g) { - let pos = this.y + g.y; + const noteHeads = this.noteHeads; + if (noteHeads) { const sr = this.renderer as SlashBarRenderer; const beat = this.container.beat; - const scale = beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; - switch (requestedPosition) { case NoteYPosition.TopWithStem: - if (hasStem) { - // stem start - pos -= - (sr.smuflMetrics.stemUp.has(symbol) ? sr.smuflMetrics.stemUp.get(symbol)!.bottomY : 0) * - scale; + // stem start and size according to duration + return ( + this.y + + noteHeads.getNoteHeadY(sr.smuflMetrics, NoteYPosition.StemUp) - + sr.smuflMetrics.getStemLength(beat.duration, sr.hasFlag(beat)) * noteHeads.glyphScale - + this._stemLengthExtension + ); + case NoteYPosition.BottomWithStem: + return ( + this.y + + noteHeads.getNoteHeadY(sr.smuflMetrics, NoteYPosition.StemDown) + + sr.smuflMetrics.getStemLength(beat.duration, sr.hasFlag(beat)) * noteHeads.glyphScale + + this._stemLengthExtension + ); + } + return this.y + noteHeads.getNoteHeadY(sr.smuflMetrics, requestedPosition); + } - // stem size according to duration - pos -= sr.smuflMetrics.getStemLength(beat.duration, sr.hasFlag(beat)) * scale; - pos -= this._stemLengthExtension; - } else { - pos -= g.height / 2; - } - return pos; + const g = this.deadSlapped; + if (g) { + // no stems + const pos = this.y + g.y; + switch (requestedPosition) { + case NoteYPosition.TopWithStem: case NoteYPosition.Top: - pos -= g.height / 2; - break; - case NoteYPosition.Center: - break; - case NoteYPosition.Bottom: - pos += g.height / 2; - break; + return pos - g.height / 2; case NoteYPosition.BottomWithStem: - if (hasStem) { - pos -= - (sr.smuflMetrics.stemDown.has(symbol) - ? sr.smuflMetrics.stemDown.get(symbol)!.topY - : -sr.smuflMetrics.glyphHeights.get(symbol)! / 2) * scale; - - // stem size according to duration - pos += sr.smuflMetrics.getStemLength(beat.duration, sr.hasFlag(beat)) * scale; - pos += this._stemLengthExtension; - } else { - pos += g.height / 2; - } - return pos; - - case NoteYPosition.StemUp: - pos -= this.renderer.smuflMetrics.stemUp.has(symbol) - ? this.renderer.smuflMetrics.stemUp.get(symbol)!.bottomY - : 0; - break; - case NoteYPosition.StemDown: - pos -= this.renderer.smuflMetrics.stemDown.has(symbol) - ? this.renderer.smuflMetrics.stemDown.get(symbol)!.topY - : 0; - break; + case NoteYPosition.Bottom: + return pos + g.height / 2; } - return pos; } return 0; diff --git a/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts index e2eb54c0f..cd35d7b94 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts @@ -7,7 +7,6 @@ import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** @@ -69,15 +68,7 @@ export class SlashNoteHeadGlyph extends NoteHeadGlyphBase { lr.registerBeatEffectOverflowsForBeat(this.beat!, minEffectY, maxEffectY); } - const direction = lr.getBeatDirection(this.beat!); - const symbol = this.symbol; - if (direction === BeamDirection.Up) { - const stemInfoUp = lr.smuflMetrics.stemUp.has(symbol) ? lr.smuflMetrics.stemUp.get(symbol)!.x : 0; - this.stemX = stemInfoUp; - } else { - const stemInfoDown = lr.smuflMetrics.stemDown.has(symbol) ? lr.smuflMetrics.stemDown.get(symbol)!.x : 0; - this.stemX = stemInfoDown; - } + this.stemX = this.getStemX(lr.smuflMetrics, lr.getBeatDirection(this.beat!)); } public static getSymbol(duration: Duration): MusicFontSymbol { diff --git a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts index e989f634d..1684c84dd 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts @@ -13,6 +13,7 @@ import { TabNoteChordGlyph } from '@coderline/alphatab/rendering/glyphs/TabNoteC import { TabRestGlyph } from '@coderline/alphatab/rendering/glyphs/TabRestGlyph'; import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; +import { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; /** @@ -46,35 +47,64 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { } public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { + if (TabBeatGlyph._isStemEnd(requestedPosition)) { + return this._getStemEndY(requestedPosition); + } + if (this.slash) { + return this.slash.getNoteHeadY(this.renderer.smuflMetrics, requestedPosition); + } return this.noteNumbers ? this.noteNumbers.getNoteY(note, requestedPosition) : 0; } - public override getRestY(requestedPosition: NoteYPosition): number { - const g = this.restGlyph; - if (g) { - switch (requestedPosition) { - case NoteYPosition.TopWithStem: - return g.getBoundingBoxTop() - this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); - case NoteYPosition.Top: - return g.getBoundingBoxTop(); - case NoteYPosition.Center: - case NoteYPosition.StemUp: - case NoteYPosition.StemDown: - return g.getBoundingBoxTop() + g.height / 2; - case NoteYPosition.Bottom: - return g.getBoundingBoxTop(); - case NoteYPosition.BottomWithStem: - return g.getBoundingBoxBottom() + this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); - } + private static _isStemEnd(requestedPosition: NoteYPosition): boolean { + return requestedPosition === NoteYPosition.TopWithStem || requestedPosition === NoteYPosition.BottomWithStem; + } + + /** + * Stems end in the rhythm area above or below the staff, independent of the notes. + */ + private _getStemEndY(requestedPosition: NoteYPosition): number { + const rhythmHeight = this.renderer.settings.notation.rhythmHeight; + const tremoloHeight = this._calculateTremoloHeightForStem(); + return requestedPosition === NoteYPosition.TopWithStem + ? -rhythmHeight - tremoloHeight + : this.renderer.height + rhythmHeight + tremoloHeight; + } + + private _calculateTremoloHeightForStem(): number { + const beat = this.container.beat; + if (!beat.isTremolo) { + return 0; + } + if (beat.duration <= Duration.Quarter) { + return 0; } - return 0; + const symbol = TremoloPickingGlyph._getSymbol(beat.tremoloPicking!); + const smufl = this.renderer.smuflMetrics; + return smufl.glyphHeights.has(symbol) ? smufl.glyphHeights.get(symbol)! : 0; + } + + public override getRestY(requestedPosition: NoteYPosition): number { + return this.getRestGlyphY(this.restGlyph, requestedPosition); } public override getLowestNoteY(requestedPosition: NoteYPosition): number { + if (TabBeatGlyph._isStemEnd(requestedPosition)) { + return this._getStemEndY(requestedPosition); + } + if (this.slash) { + return this.slash.getNoteHeadY(this.renderer.smuflMetrics, requestedPosition); + } return this.noteNumbers ? this.noteNumbers.getLowestNoteY(requestedPosition) : 0; } public override getHighestNoteY(requestedPosition: NoteYPosition): number { + if (TabBeatGlyph._isStemEnd(requestedPosition)) { + return this._getStemEndY(requestedPosition); + } + if (this.slash) { + return this.slash.getNoteHeadY(this.renderer.smuflMetrics, requestedPosition); + } return this.noteNumbers ? this.noteNumbers.getHighestNoteY(requestedPosition) : 0; } @@ -96,7 +126,7 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { let beatEffects: Map; - if (this.container.beat.slashed && !this.container.beat.notes.some(x => x.isTieDestination as boolean)) { + if (this.container.beat.slashed) { const line = Math.floor((this.renderer.bar.staff.tuning.length - 1) / 2); const slashY = tabRenderer.getLineY(line); const slashNoteHead = new SlashNoteHeadGlyph(0, slashY, this.container.beat); @@ -178,10 +208,22 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { this.onTimeX = this.slash!.x + this.slash!.width / 2; } this.middleX = this.onTimeX; - this.stemX = this.middleX; + // slashes attach the stem like regular note heads (SMuFL stem anchor) + this.stemX = this.slash ? this.slash.x + this.slash.stemX : this.middleX; + + if (this.slash) { + // reserve the slash area so stems are not painted through it + this.renderer.collisionHelper.reserveBeatSlot( + this.container.beat, + this.slash.getNoteHeadY(this.renderer.smuflMetrics, NoteYPosition.Top), + this.slash.getNoteHeadY(this.renderer.smuflMetrics, NoteYPosition.Bottom) + ); + } + // effects like tremolos sit on the (potentially hidden) stem + const effectX = BeamingHelper.beatHasStem(this.container.beat) ? this.stemX : this.onTimeX; for (const g of centeredEffectGlyphs) { - g.x = this.onTimeX; + g.x = effectX; } } diff --git a/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts index 43288a895..6211e6472 100644 --- a/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts @@ -1,5 +1,4 @@ import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { Duration } from '@coderline/alphatab/model/Duration'; import type { Note } from '@coderline/alphatab/model/Note'; import { type ICanvas, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; @@ -7,7 +6,6 @@ import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarR import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import type { NoteNumberGlyph } from '@coderline/alphatab/rendering/glyphs/NoteNumberGlyph'; -import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; @@ -85,15 +83,6 @@ export class TabNoteChordGlyph extends Glyph { case NoteYPosition.StemDown: pos = this.y + n.getBoundingBoxBottom(); break; - case NoteYPosition.TopWithStem: - pos = -this.renderer.settings.notation.rhythmHeight; - pos -= this.calculateTremoloHeightForStem(); - break; - - case NoteYPosition.BottomWithStem: - pos = this.renderer.height + this.renderer.settings.notation.rhythmHeight; - pos += this.calculateTremoloHeightForStem(); - break; } return pos; @@ -101,19 +90,6 @@ export class TabNoteChordGlyph extends Glyph { return 0; } - public calculateTremoloHeightForStem() { - const beat = this.beat; - if (!beat.isTremolo) { - return 0; - } - if (beat.duration <= Duration.Quarter) { - return 0; - } - const symbol = TremoloPickingGlyph._getSymbol(beat.tremoloPicking!); - const smufl = this.renderer.smuflMetrics; - return smufl.glyphHeights.has(symbol) ? smufl.glyphHeights.get(symbol)! : 0; - } - public override doLayout(): void { let w: number = 0; diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-slash.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-slash.png index 2e1960543..dbdde2865 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-slash.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-slash.png differ diff --git a/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-slashed.png b/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-slashed.png new file mode 100644 index 000000000..8b1c84879 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-slashed.png differ diff --git a/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-with-beams-slashed.png b/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-with-beams-slashed.png new file mode 100644 index 000000000..9a4b23cfc Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-with-beams-slashed.png differ diff --git a/packages/alphatab/test/visualTests/features/GuitarTabs.test.ts b/packages/alphatab/test/visualTests/features/GuitarTabs.test.ts index 1617b2904..1498d0b58 100644 --- a/packages/alphatab/test/visualTests/features/GuitarTabs.test.ts +++ b/packages/alphatab/test/visualTests/features/GuitarTabs.test.ts @@ -1,8 +1,8 @@ -import { describe, it } from 'vitest'; -import { StaveProfile } from '@coderline/alphatab/StaveProfile'; import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { Settings } from '@coderline/alphatab/Settings'; -import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; +import { StaveProfile } from '@coderline/alphatab/StaveProfile'; +import { VisualTestHelper, VisualTestOptions, VisualTestRun } from 'test/visualTests/VisualTestHelper'; +import { describe, it } from 'vitest'; describe('GuitarTabsTests', () => { it('rhythm', async () => { @@ -19,6 +19,30 @@ describe('GuitarTabsTests', () => { await VisualTestHelper.runVisualTest('guitar-tabs/rhythm-with-beams.gp', settings); }); + it('rhythm-slashed', async () => { + const settings: Settings = new Settings(); + settings.notation.rhythmMode = TabRhythmMode.ShowWithBars; + await VisualTestHelper.runVisualTestFull( + await VisualTestOptions.file( + 'effects-and-annotations/beat-slash.gp', + [new VisualTestRun(-1, 'test-data/visual-tests/guitar-tabs/rhythm-slashed.png')], + settings + ) + ); + }); + + it('rhythm-with-beams-slashed', async () => { + const settings: Settings = new Settings(); + settings.notation.rhythmMode = TabRhythmMode.ShowWithBeams; + await VisualTestHelper.runVisualTestFull( + await VisualTestOptions.file( + 'effects-and-annotations/beat-slash.gp', + [new VisualTestRun(-1, 'test-data/visual-tests/guitar-tabs/rhythm-with-beams-slashed.png')], + settings + ) + ); + }); + it('string-variations', async () => { const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Tab;