diff --git a/packages/alphatab/.env b/packages/alphatab/.env index 72295dc04..4bca2fb57 100644 --- a/packages/alphatab/.env +++ b/packages/alphatab/.env @@ -1,2 +1,3 @@ FORCE_COLOR=1 NODE_OPTIONS=--expose-gc +UPDATE_SNAPSHOT=true \ No newline at end of file diff --git a/packages/alphatab/src/EngravingSettings.ts b/packages/alphatab/src/EngravingSettings.ts index 1bf263854..7ffe0d130 100644 --- a/packages/alphatab/src/EngravingSettings.ts +++ b/packages/alphatab/src/EngravingSettings.ts @@ -2,8 +2,7 @@ import { EngravingSettingsCloner } from '@coderline/alphatab/generated/Engraving import { JsonHelper } from '@coderline/alphatab/io/JsonHelper'; import { Logger } from '@coderline/alphatab/Logger'; import { Duration } from '@coderline/alphatab/model/Duration'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { MusicFontSymbol, MusicFontSymbolLookup } from '@coderline/alphatab/model/MusicFontSymbol'; import type { SmuflMetadata } from '@coderline/alphatab/SmuflMetadata'; /** @@ -53,6 +52,12 @@ export class EngravingStemInfo { export class EngravingSettings { private static _bravuraDefaults?: EngravingSettings; + // NOTE: configurable in future? + /** + * @internal + */ + public static readonly GraceScale: number = 0.75; + /** * A {@link EngravingSettings} copy filled with the settings of the Bravura font used by default in alphaTab. */ @@ -431,7 +436,7 @@ export class EngravingSettings { MusicFontSymbol.NoteheadNull ]); - for (const symbol of ModelUtils.getAllMusicFontSymbols()) { + for (const symbol of MusicFontSymbolLookup.getAllMusicFontSymbols()) { if (!handledSymbols.has(symbol)) { if (!ignoredSymbols.has(symbol)) { Logger.warning( @@ -498,6 +503,7 @@ export class EngravingSettings { this.tripletFeelBracketPadding = 0.2 * this.oneStaffSpace; this.accidentalPadding = 0.1 * this.oneStaffSpace; this.preBeatGlyphSpacing = 0.5 * this.oneStaffSpace; + this.multiVoiceDisplacedNoteHeadSpacing = 0.2 * this.oneStaffSpace; this.tuningGlyphStringRowPadding = 0.2 * this.oneStaffSpace; } @@ -554,21 +560,20 @@ export class EngravingSettings { public lineRangedGlyphDashSize = 0; /** - * The padding between effects and glyphs placed before the note heads, e.g. accidentals or brushes + * The padding between effects and glyphs placed before the note heads, e.g. accidentals or brushes */ public preNoteEffectPadding = 0; /** - * The padding between effects and glyphs placed after the note heads, e.g. slides or bends + * The padding between effects and glyphs placed after the note heads, e.g. slides or bends */ public postNoteEffectPadding = 0; /** - * The padding between effects and glyphs placed above/blow the note heads e.g. staccato + * The padding between effects and glyphs placed above/blow the note heads e.g. staccato */ public onNoteEffectPadding = 0; - /** * The padding between the circles around string numbers. */ @@ -600,7 +605,7 @@ export class EngravingSettings { public tieHeight = 0; /** - * The padding between the border and text of beat timers. + * The padding between the border and text of beat timers. */ public beatTimerPadding = 0; @@ -630,7 +635,7 @@ export class EngravingSettings { public tabWhammyTextPadding = 0; /** - * The height applied per half-note whammy. + * The height applied per half-note whammy. */ public tabWhammyPerHalfHeight = 0; @@ -658,15 +663,15 @@ export class EngravingSettings { * The size of the dashes on bends (e.g. on holds) */ public tabBendDashSize = 0; - + /** * The additional padding between the staff and the point * where bend values are calculated from. */ public tabBendStaffPadding = 0; - + /** - * The height applied per quarter-note. + * The height applied per quarter-note. */ public tabBendPerValueHeight = 0; @@ -714,7 +719,7 @@ export class EngravingSettings { public chordDiagramLineWidth = 0; /** - * The padding between the bracket lines and numbers of tuplets + * The padding between the bracket lines and numbers of tuplets */ public tripletFeelBracketPadding = 0; @@ -753,6 +758,12 @@ export class EngravingSettings { */ public directionsScale = 0.6; + /** + * The spacing between displaced displaced note heads + * in case of multi-voice note head overlaps. + */ + public multiVoiceDisplacedNoteHeadSpacing = 0; + // Idea: maybe we can encode and pack this large metadata into a more compact format (e.g. BSON or a custom binary blob?) // This metadata below is updated automatically from the bravura_metadata.json via npm script diff --git a/packages/alphatab/src/Environment.ts b/packages/alphatab/src/Environment.ts index 330dfcf3b..8a2f3c360 100644 --- a/packages/alphatab/src/Environment.ts +++ b/packages/alphatab/src/Environment.ts @@ -831,4 +831,17 @@ export class Environment { public static quoteJsonString(text: string) { return JSON.stringify(text); } + + /** + * @internal + * @target web + * @partial + */ + public static sortDescending(array: number[]) { + // java is a joke: + // no primitive sorting of arrays with custom comparer in 2025 + // so we need to declare this specific helper function and implement it in Kotlin ourselves. + array.sort((a, b) => b - a); + } + } diff --git a/packages/alphatab/src/generated/EngravingSettingsCloner.ts b/packages/alphatab/src/generated/EngravingSettingsCloner.ts index b33cbd41c..823167329 100644 --- a/packages/alphatab/src/generated/EngravingSettingsCloner.ts +++ b/packages/alphatab/src/generated/EngravingSettingsCloner.ts @@ -92,6 +92,7 @@ export class EngravingSettingsCloner { clone.tuningGlyphStringColumnScale = original.tuningGlyphStringColumnScale; clone.tuningGlyphStringRowPadding = original.tuningGlyphStringRowPadding; clone.directionsScale = original.directionsScale; + clone.multiVoiceDisplacedNoteHeadSpacing = original.multiVoiceDisplacedNoteHeadSpacing; return clone; } } diff --git a/packages/alphatab/src/generated/EngravingSettingsJson.ts b/packages/alphatab/src/generated/EngravingSettingsJson.ts index 60276da8d..adf10d15c 100644 --- a/packages/alphatab/src/generated/EngravingSettingsJson.ts +++ b/packages/alphatab/src/generated/EngravingSettingsJson.ts @@ -395,4 +395,9 @@ export interface EngravingSettingsJson { * The relative scale of any directions glyphs drawn like coda or segno. */ directionsScale?: number; + /** + * The spacing between displaced displaced note heads + * in case of multi-voice note head overlaps. + */ + multiVoiceDisplacedNoteHeadSpacing?: number; } diff --git a/packages/alphatab/src/generated/EngravingSettingsSerializer.ts b/packages/alphatab/src/generated/EngravingSettingsSerializer.ts index 009b1b81a..47e797902 100644 --- a/packages/alphatab/src/generated/EngravingSettingsSerializer.ts +++ b/packages/alphatab/src/generated/EngravingSettingsSerializer.ts @@ -154,6 +154,7 @@ export class EngravingSettingsSerializer { o.set("tuningglyphstringcolumnscale", obj.tuningGlyphStringColumnScale); o.set("tuningglyphstringrowpadding", obj.tuningGlyphStringRowPadding); o.set("directionsscale", obj.directionsScale); + o.set("multivoicedisplacednoteheadspacing", obj.multiVoiceDisplacedNoteHeadSpacing); return o; } public static setProperty(obj: EngravingSettings, property: string, v: unknown): boolean { @@ -432,6 +433,9 @@ export class EngravingSettingsSerializer { case "directionsscale": obj.directionsScale = v! as number; return true; + case "multivoicedisplacednoteheadspacing": + obj.multiVoiceDisplacedNoteHeadSpacing = v! as number; + return true; } return false; } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 62a22f588..13ebb462b 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -278,7 +278,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler const types = lookup.get(tag); if (!types) { - if (args) { + if (args && args.arguments.length > 0) { importer.addSemanticDiagnostic({ code: AlphaTexDiagnosticCode.AT300, message: `Expected no arguments, but found some.`, diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts index 19143eb58..31bfcb9b2 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts @@ -47,6 +47,19 @@ export class AlphaTex1MetaDataReader implements IAlphaTexMetaDataReader { AlphaTexNodeType.Number ]); + public hasMetaDataArguments(metaData: AlphaTexMetaDataTagNode): boolean { + const tag = metaData.tag.text.toLowerCase(); + for (const lookup of AlphaTex1LanguageDefinitions.metaDataSignatures) { + if (lookup.has(tag)) { + const types = lookup.get(tag); + return types !== null; + } + } + + // unknown meta -> assume args exist + return true; + } + public readMetaDataArguments( parser: AlphaTexParser, metaData: AlphaTexMetaDataTagNode @@ -83,7 +96,10 @@ export class AlphaTex1MetaDataReader implements IAlphaTexMetaDataReader { if (!AlphaTex1LanguageDefinitions.metaDataProperties.has(tag)) { return undefined; } - const props = AlphaTex1LanguageDefinitions.metaDataProperties.get(tag)!; + const props = AlphaTex1LanguageDefinitions.metaDataProperties.get(tag); + if (!props) { + return this._readPropertyArguments(parser, [], property); + } return this._readPropertyArguments(parser, [props], property); } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts index aa206522d..5d11afbf1 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts @@ -67,7 +67,7 @@ export class AlphaTexParser { /** * The parsing mode. */ - public mode:AlphaTexParseMode = AlphaTexParseMode.ForModelImport; + public mode: AlphaTexParseMode = AlphaTexParseMode.ForModelImport; public get lexerDiagnostics(): AlphaTexDiagnosticBag { return this.lexer.lexerDiagnostics; @@ -509,7 +509,6 @@ export class AlphaTexParser { private static readonly _allowValuesAfterProperties = new Set(['chord']); - private _metaData(metaDataList: AlphaTexMetaDataNode[]) { const tag = this.lexer.peekToken(); if (!tag || tag.nodeType !== AlphaTexNodeType.Tag) { @@ -558,17 +557,19 @@ export class AlphaTexParser { } } } else { - metaData.arguments = this.argumentList(); - if (!metaData.arguments) { - metaData.arguments = this._metaDataReader.readMetaDataArguments(this, metaData.tag); - if (metaData.arguments && metaData.arguments.arguments.length > 1) { - this.addParserDiagnostic({ - code: AlphaTexDiagnosticCode.AT301, - message: `Metadata arguments should be wrapped into parenthesis.`, - severity: AlphaTexDiagnosticsSeverity.Warning, - start: metaData.arguments?.start ?? metaData.start, - end: metaData.arguments?.end ?? metaData.end - }); + if (this._metaDataReader.hasMetaDataArguments(metaData.tag)) { + metaData.arguments = this.argumentList(); + if (!metaData.arguments) { + metaData.arguments = this._metaDataReader.readMetaDataArguments(this, metaData.tag); + if (metaData.arguments && metaData.arguments.arguments.length > 1) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT301, + message: `Metadata arguments should be wrapped into parenthesis.`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: metaData.arguments?.start ?? metaData.start, + end: metaData.arguments?.end ?? metaData.end + }); + } } } diff --git a/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts b/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts index dc86aa63b..9916ce13a 100644 --- a/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts +++ b/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts @@ -1,7 +1,7 @@ import type { + AlphaTexArgumentList, AlphaTexMetaDataTagNode, - AlphaTexPropertyNode, - AlphaTexArgumentList + AlphaTexPropertyNode } from '@coderline/alphatab/importer/alphaTex/AlphaTexAst'; import type { AlphaTexParser } from '@coderline/alphatab/importer/alphaTex/AlphaTexParser'; @@ -9,6 +9,7 @@ import type { AlphaTexParser } from '@coderline/alphatab/importer/alphaTex/Alpha * @internal */ export interface IAlphaTexMetaDataReader { + hasMetaDataArguments(metaData: AlphaTexMetaDataTagNode): boolean; readMetaDataArguments(parser: AlphaTexParser, metaData: AlphaTexMetaDataTagNode): AlphaTexArgumentList | undefined; readMetaDataPropertyArguments( diff --git a/packages/alphatab/src/model/Beat.ts b/packages/alphatab/src/model/Beat.ts index 57e18ac09..097fb5a8e 100644 --- a/packages/alphatab/src/model/Beat.ts +++ b/packages/alphatab/src/model/Beat.ts @@ -1173,7 +1173,6 @@ export class Beat { return this.noteStringLookup.has(noteString); } - // TODO: can be likely eliminated public getNoteWithRealValue(noteRealValue: number): Note | null { if (this.noteValueLookup.has(noteRealValue)) { return this.noteValueLookup.get(noteRealValue)!; diff --git a/packages/alphatab/src/model/ModelUtils.ts b/packages/alphatab/src/model/ModelUtils.ts index 12d9c78f2..0e0302b32 100644 --- a/packages/alphatab/src/model/ModelUtils.ts +++ b/packages/alphatab/src/model/ModelUtils.ts @@ -1,17 +1,16 @@ +import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import { Automation, AutomationType } from '@coderline/alphatab/model/Automation'; +import { Bar } from '@coderline/alphatab/model/Bar'; import { Beat } from '@coderline/alphatab/model/Beat'; import type { Duration } from '@coderline/alphatab/model/Duration'; -import { HeaderFooterStyle, type Score, ScoreStyle, type ScoreSubElement } from '@coderline/alphatab/model/Score'; -import type { Settings } from '@coderline/alphatab/Settings'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; +import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; import { MasterBar } from '@coderline/alphatab/model/MasterBar'; +import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; +import { HeaderFooterStyle, type Score, ScoreStyle, type ScoreSubElement } from '@coderline/alphatab/model/Score'; import type { Track } from '@coderline/alphatab/model/Track'; -import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; -import { Bar } from '@coderline/alphatab/model/Bar'; import { Voice } from '@coderline/alphatab/model/Voice'; -import { Automation, AutomationType } from '@coderline/alphatab/model/Automation'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import type { Settings } from '@coderline/alphatab/Settings'; +import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; /** * @internal @@ -671,22 +670,6 @@ export class ModelUtils { masterBar.previousMasterBar!.nextMasterBar = null; } } - - private static _allMusicFontSymbols: MusicFontSymbol[] = []; - - /** - * Gets a list of all music font symbols used in alphaTab. - */ - public static getAllMusicFontSymbols(): MusicFontSymbol[] { - if (ModelUtils._allMusicFontSymbols.length === 0) { - ModelUtils._allMusicFontSymbols = Object.values(MusicFontSymbol) - .filter((k: any) => typeof k === 'number') - .map(v => v as number as MusicFontSymbol) as MusicFontSymbol[]; - } - - return ModelUtils._allMusicFontSymbols; - } - /** * Lists the display transpositions for some known midi instruments. * It is a common practice to transpose the standard notation for instruments like guitars. diff --git a/packages/alphatab/src/model/MusicFontSymbol.ts b/packages/alphatab/src/model/MusicFontSymbol.ts index 34789e15e..8e52b560e 100644 --- a/packages/alphatab/src/model/MusicFontSymbol.ts +++ b/packages/alphatab/src/model/MusicFontSymbol.ts @@ -346,3 +346,38 @@ export enum MusicFontSymbol { FingeringALower = 0xed1b, FingeringCLower = 0xed1c } + +/** + * @internal + */ +export class MusicFontSymbolLookup { + private static _allMusicFontSymbols: MusicFontSymbol[] = []; + private static readonly _blackNoteHeadGlyphs = new Set(); + + private static _initialize() { + const all = MusicFontSymbolLookup._allMusicFontSymbols; + if (all.length === 0) { + for (const v of Object.values(MusicFontSymbol).filter((k: any) => typeof k === 'number')) { + const symbol = v as number as MusicFontSymbol; + all.push(symbol); + const name = MusicFontSymbol[symbol].toLowerCase(); + if (name.endsWith('black')) { + MusicFontSymbolLookup._blackNoteHeadGlyphs.add(symbol); + } + } + } + } + + /** + * Gets a list of all music font symbols used in alphaTab. + */ + public static getAllMusicFontSymbols(): MusicFontSymbol[] { + MusicFontSymbolLookup._initialize(); + return MusicFontSymbolLookup._allMusicFontSymbols; + } + + public static isBlackNoteHead(glph: MusicFontSymbol): boolean { + MusicFontSymbolLookup._initialize(); + return MusicFontSymbolLookup._blackNoteHeadGlyphs.has(glph); + } +} diff --git a/packages/alphatab/src/model/Score.ts b/packages/alphatab/src/model/Score.ts index 625587d1b..833179f49 100644 --- a/packages/alphatab/src/model/Score.ts +++ b/packages/alphatab/src/model/Score.ts @@ -432,6 +432,13 @@ export class Score { for (let i: number = 0, j: number = this.tracks.length; i < j; i++) { this.tracks[i].finish(settings, sharedDataBag); } + + // fixup masterbar starts to handle anacrusis lengths + for (const mb of this.masterBars) { + if (mb.index > 0) { + mb.start = mb.previousMasterBar!.start + mb.previousMasterBar!.calculateDuration(); + } + } } /** diff --git a/packages/alphatab/src/rendering/BarRendererBase.ts b/packages/alphatab/src/rendering/BarRendererBase.ts index 2af7403bb..26f01d02d 100644 --- a/packages/alphatab/src/rendering/BarRendererBase.ts +++ b/packages/alphatab/src/rendering/BarRendererBase.ts @@ -5,15 +5,17 @@ import type { Note } from '@coderline/alphatab/model/Note'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { type Voice, VoiceSubElement } from '@coderline/alphatab/model/Voice'; import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { EffectBandContainer } from '@coderline/alphatab/rendering/EffectBandContainer'; -import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; -import type { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import type { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { + BeatContainerGlyph, + type BeatContainerGlyphBase +} from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { LeftToRightLayoutingGlyphGroup } from '@coderline/alphatab/rendering/glyphs/LeftToRightLayoutingGlyphGroup'; +import { MultiVoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/MultiVoiceContainerGlyph'; 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'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; @@ -26,7 +28,6 @@ import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingH import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import type { Settings } from '@coderline/alphatab/Settings'; /** @@ -88,9 +89,9 @@ export enum NoteXPosition { * @internal */ export class BarRendererBase { - private _preBeatGlyphs: LeftToRightLayoutingGlyphGroup = new LeftToRightLayoutingGlyphGroup(); - private _voiceContainers: Map = new Map(); - private _postBeatGlyphs: LeftToRightLayoutingGlyphGroup = new LeftToRightLayoutingGlyphGroup(); + private _preBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); + protected readonly voiceContainer = new MultiVoiceContainerGlyph(); + private readonly _postBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); private _ties: ITieGlyph[] = []; @@ -166,7 +167,7 @@ export class BarRendererBase { public canWrap: boolean = true; public get showMultiBarRest(): boolean { - return false; + return true; } public constructor(renderer: ScoreRenderer, bar: Bar) { @@ -218,9 +219,8 @@ export class BarRendererBase { public scaleToWidth(width: number): void { // preBeat and postBeat glyphs do not get resized const containerWidth: number = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width; - for (const container of this._voiceContainers.values()) { - container.scaleToWidth(containerWidth); - } + this.voiceContainer.scaleToWidth(containerWidth); + for (const v of this.helpers.beamHelpers) { for (const h of v) { h.alignWithBeats(); @@ -282,14 +282,9 @@ export class BarRendererBase { if (info.preBeatSize < preSize) { info.preBeatSize = preSize; } - let postBeatStart = 0; - for (const container of this._voiceContainers.values()) { - container.registerLayoutingInfo(info); - const x: number = container.x + container.width; - if (postBeatStart < x) { - postBeatStart = x; - } - } + const container = this.voiceContainer; + container.registerLayoutingInfo(info); + const postSize: number = this._postBeatGlyphs.width; if (info.postBeatSize < postSize) { info.postBeatSize = postSize; @@ -322,18 +317,14 @@ export class BarRendererBase { // if we need additional space in the preBeat group we simply // add a new spacer this._preBeatGlyphs.width = this.layoutingInfo.preBeatSize; + // on beat glyphs we apply the glyph spacing - let voiceEnd: number = this._preBeatGlyphs.x + this._preBeatGlyphs.width; - for (const c of this._voiceContainers.values()) { - c.x = this._preBeatGlyphs.x + this._preBeatGlyphs.width; - c.applyLayoutingInfo(this.layoutingInfo); - const newEnd: number = c.x + c.width; - if (voiceEnd < newEnd) { - voiceEnd = newEnd; - } - } + const container = this.voiceContainer; + container.x = this._preBeatGlyphs.x + this._preBeatGlyphs.width; + container.applyLayoutingInfo(this.layoutingInfo); + // on the post glyphs we add the spacing before all other glyphs - this._postBeatGlyphs.x = Math.floor(voiceEnd); + this._postBeatGlyphs.x = Math.floor(container.x + container.width); this._postBeatGlyphs.width = this.layoutingInfo.postBeatSize; this.width = Math.ceil(this._postBeatGlyphs.x + this._postBeatGlyphs.width); this.computedWidth = this.width; @@ -450,36 +441,18 @@ export class BarRendererBase { } this.helpers.initialize(); this._ties = []; - this._preBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); this._preBeatGlyphs.renderer = this; - this._voiceContainers.clear(); - this._postBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); + this.voiceContainer.renderer = this; this._postBeatGlyphs.renderer = this; this.topEffects.doLayout(); this.bottomEffects.doLayout(); - for (let i: number = 0; i < this.bar.voices.length; i++) { - const voice: Voice = this.bar.voices[i]; - if (this.hasVoiceContainer(voice)) { - const c: VoiceContainerGlyph = new VoiceContainerGlyph(0, 0, voice); - c.renderer = this; - this._voiceContainers.set(this.bar.voices[i].index, c); - } - } if (this.bar.simileMark === SimileMark.SecondOfDouble) { this.canWrap = false; } this.createPreBeatGlyphs(); - - // multibar rest - if (this.additionalMultiRestBars) { - const container = new MultiBarRestBeatContainerGlyph(this.getVoiceContainer(this.bar.voices[0])!); - this.addBeatGlyph(container); - } else { - this.createBeatGlyphs(); - } - + this.createBeatGlyphs(); this.createPostBeatGlyphs(); this._registerLayoutingInfo(); @@ -532,58 +505,34 @@ export class BarRendererBase { } } - for (const v of this._voiceContainers.values()) { - for (const b of v.beatGlyphs) { - const topY = b.getBoundingBoxTop(); - if (topY < 0) { - this.registerOverflowTop(topY * -1); - } + const v = this.voiceContainer; + const contentMinY = v.getBoundingBoxTop(); + if (contentMinY < 0) { + this.registerOverflowTop(contentMinY * -1); + } - const bottomY = b.getBoundingBoxBottom(); - if (bottomY > rendererBottom) { - this.registerOverflowBottom(bottomY - rendererBottom); - } - } + const contentMaxY = v.getBoundingBoxBottom(); + if (contentMaxY > rendererBottom) { + this.registerOverflowBottom(contentMaxY - rendererBottom); } const beatEffectsMinY = this.beatEffectsMinY; - if (!Number.isNaN(beatEffectsMinY)) { - const beatEffectTopOverflow = -beatEffectsMinY; - if (beatEffectTopOverflow > 0) { - this.registerOverflowTop(beatEffectTopOverflow); - } + if (!Number.isNaN(beatEffectsMinY) && beatEffectsMinY < 0) { + this.registerOverflowTop(beatEffectsMinY * -1); } const beatEffectsMaxY = this.beatEffectsMaxY; - if (!Number.isNaN(beatEffectsMaxY)) { - const beatEffectBottomOverflow = beatEffectsMaxY - rendererBottom; - if (beatEffectBottomOverflow > 0) { - this.registerOverflowBottom(beatEffectBottomOverflow); - } + if (!Number.isNaN(beatEffectsMaxY) && beatEffectsMaxY > rendererBottom) { + this.registerOverflowBottom(beatEffectsMaxY - rendererBottom); } } - protected hasVoiceContainer(voice: Voice): boolean { - if (this.additionalMultiRestBars || voice.index === 0) { - return true; - } - return !voice.isEmpty; - } - protected updateSizes(): void { this.staff!.registerStaffTop(0); - const voiceContainers: Map = this._voiceContainers; - const beatGlyphsStart: number = this.beatGlyphsStart; - let postBeatStart: number = beatGlyphsStart; - for (const c of voiceContainers.values()) { - c.x = beatGlyphsStart; - c.doLayout(); - const x: number = c.x + c.width; - if (postBeatStart < x) { - postBeatStart = x; - } - } - this._postBeatGlyphs.x = Math.floor(postBeatStart); + + this.voiceContainer.x = this._preBeatGlyphs.x + this._preBeatGlyphs.width; + this._postBeatGlyphs.x = Math.floor(this.voiceContainer.x + this.voiceContainer.width); + this.width = Math.ceil(this._postBeatGlyphs.x + this._postBeatGlyphs.width); const topHeightChanged = this.topEffects.updateEffectBandHeights(); @@ -603,31 +552,13 @@ export class BarRendererBase { this._preBeatGlyphs.addGlyph(g); } - protected addBeatGlyph(g: BeatContainerGlyph): void { + protected addBeatGlyph(g: BeatContainerGlyphBase): void { g.renderer = this; - g.preNotes.renderer = this; - g.onNotes.renderer = this; - this.getVoiceContainer(g.beat.voice)!.addGlyph(g); + this.voiceContainer.addGlyph(g); } - protected getVoiceContainer(voice: Voice): VoiceContainerGlyph | undefined { - return this._voiceContainers.has(voice.index) ? this._voiceContainers.get(voice.index) : undefined; - } - - public getBeatContainer(beat: Beat): BeatContainerGlyph | undefined { - const beatGlyphs = this.getVoiceContainer(beat.voice)?.beatGlyphs; - if (beatGlyphs && beat.index < beatGlyphs.length) { - return beatGlyphs[beat.index]; - } - return undefined; - } - - public getPreNotesGlyphForBeat(beat: Beat): BeatGlyphBase | undefined { - return this.getBeatContainer(beat)?.preNotes; - } - - public getOnNotesGlyphForBeat(beat: Beat): BeatOnNoteGlyphBase | undefined { - return this.getBeatContainer(beat)?.onNotes; + public getBeatContainer(beat: Beat): BeatContainerGlyphBase | undefined { + return this.voiceContainer.getBeatContainer(beat); } public paint(cx: number, cy: number, canvas: ICanvas): void { @@ -648,11 +579,7 @@ export class BarRendererBase { canvas.color = this.resources.mainGlyphColor; this._preBeatGlyphs.paint(cx + this.x, cy + this.y, canvas); - - for (const c of this._voiceContainers.values()) { - c.paint(cx + this.x, cy + this.y, canvas); - } - + this.voiceContainer.paint(cx + this.x, cy + this.y, canvas); canvas.color = this.resources.mainGlyphColor; this._postBeatGlyphs.paint(cx + this.x, cy + this.y, canvas); @@ -697,15 +624,7 @@ export class BarRendererBase { barBounds.realBounds.h = this.height; masterBarBounds.addBar(barBounds); - for (const [index, c] of this._voiceContainers) { - const isEmptyBar: boolean = this.bar.isEmpty && index === 0; - if (!c.voice.isEmpty || isEmptyBar) { - for (let i: number = 0, j: number = c.beatGlyphs.length; i < j; i++) { - const bc: BeatContainerGlyph = c.beatGlyphs[i]; - bc.buildBoundingsLookup(barBounds, cx + this.x + c.x, cy + this.y + c.y, isEmptyBar); - } - } - } + this.voiceContainer.buildBoundingsLookup(barBounds, cx + this.x, cy + this.y); } protected addPostBeatGlyph(g: Glyph): void { @@ -717,12 +636,17 @@ export class BarRendererBase { } protected createBeatGlyphs(): void { - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - this.createVoiceGlyphs(voice); + if (this.additionalMultiRestBars) { + const container = new MultiBarRestBeatContainerGlyph(); + this.addBeatGlyph(container); + } else { + for (const index of this.bar.filledVoices) { + this.createVoiceGlyphs(this.bar.voices[index]); } } + this.voiceContainer.doLayout(); + if (this.topEffects.isLinkedToPreviousRenderer || this.bottomEffects.isLinkedToPreviousRenderer) { this.isLinkedToPrevious = true; } @@ -738,7 +662,7 @@ export class BarRendererBase { } public get beatGlyphsStart(): number { - return this._preBeatGlyphs.x + this._preBeatGlyphs.width; + return this.voiceContainer.x; } public get postBeatGlyphsStart(): number { @@ -750,11 +674,7 @@ export class BarRendererBase { requestedPosition: BeatXPosition = BeatXPosition.PreNotes, useSharedSizes: boolean = false ): number { - const container = this.getBeatContainer(beat); - if (container) { - return container.voiceContainer.x + container.x + container.getBeatX(requestedPosition, useSharedSizes); - } - return 0; + return this.beatGlyphsStart + this.voiceContainer.getBeatX(beat, requestedPosition, useSharedSizes); } public getRatioPositionX(ticks: number): number { @@ -767,24 +687,15 @@ export class BarRendererBase { } public getNoteX(note: Note, requestedPosition: NoteXPosition): number { - const container = this.getBeatContainer(note.beat); - if (container) { - return ( - container.voiceContainer.x + - container.x + - container.onNotes.x + - container.onNotes.getNoteX(note, requestedPosition) - ); - } - return 0; + return this.beatGlyphsStart + this.voiceContainer.getNoteX(note, requestedPosition); } public getNoteY(note: Note, requestedPosition: NoteYPosition): number { - const beat = this.getOnNotesGlyphForBeat(note.beat); - if (beat) { - return beat.getNoteY(note, requestedPosition); - } - return Number.NaN; + return this.voiceContainer.y + +this.voiceContainer.getNoteY(note, requestedPosition); + } + + public getRestY(beat: Beat, requestedPosition: NoteYPosition): number { + return this.voiceContainer.y + +this.voiceContainer.getRestY(beat, requestedPosition); } public reLayout(): void { diff --git a/packages/alphatab/src/rendering/EffectBand.ts b/packages/alphatab/src/rendering/EffectBand.ts index c0d5e8058..e408d8268 100644 --- a/packages/alphatab/src/rendering/EffectBand.ts +++ b/packages/alphatab/src/rendering/EffectBand.ts @@ -6,7 +6,6 @@ import type { EffectBandContainer } from '@coderline/alphatab/rendering/EffectBa import type { EffectBandSlot } from '@coderline/alphatab/rendering/EffectBandSlot'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; -import type { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; @@ -201,7 +200,7 @@ export class EffectBand extends Glyph { private _alignGlyph(sizing: EffectBarGlyphSizing, beat: Beat): void { const g: EffectGlyph = this._effectGlyphs[beat.voice.index].get(beat.index)!; - const container: BeatContainerGlyph = this.renderer.getBeatContainer(beat)!; + const container = this.renderer.getBeatContainer(beat)!; switch (sizing) { case EffectBarGlyphSizing.SinglePreBeat: @@ -215,7 +214,7 @@ export class EffectBand extends Glyph { case EffectBarGlyphSizing.SingleOnBeatToEnd: case EffectBarGlyphSizing.GroupedOnBeatToEnd: g.x = this.renderer.beatGlyphsStart + container.x + container.onTimeX; - if (container.beat.isLastOfVoice) { + if (container.isLastOfVoice) { g.width = this.renderer.width - g.x; } else { // shift to the start using the biggest post-beat size of the respective beat diff --git a/packages/alphatab/src/rendering/LineBarRenderer.ts b/packages/alphatab/src/rendering/LineBarRenderer.ts index c3e6141fb..1c525af66 100644 --- a/packages/alphatab/src/rendering/LineBarRenderer.ts +++ b/packages/alphatab/src/rendering/LineBarRenderer.ts @@ -1,3 +1,4 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import type { BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatBeamingMode, type BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; @@ -13,7 +14,6 @@ import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; import { FlagGlyph } from '@coderline/alphatab/rendering/glyphs/FlagGlyph'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import { RepeatCountGlyph } from '@coderline/alphatab/rendering/glyphs/RepeatCountGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; @@ -189,10 +189,10 @@ export abstract class LineBarRenderer extends BarRendererBase { beatElement: BeatSubElement, bracketsAsArcs: boolean = false ): void { - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const container = this.getVoiceContainer(voice)!; - for (const tupletGroup of container.tupletGroups) { + for (const v of this.voiceContainer.voiceDrawOrder!) { + if (this.voiceContainer.tupletGroups.has(v)) { + const voice = this.voiceContainer.tupletGroups.get(v)!; + for (const tupletGroup of voice) { this._paintTupletHelper(cx, cy, canvas, tupletGroup, beatElement, bracketsAsArcs); } } @@ -425,8 +425,8 @@ export abstract class LineBarRenderer extends BarRendererBase { flagsElement: BeatSubElement, beamsElement: BeatSubElement ): void { - for (const v of this.helpers.beamHelpers) { - for (const h of v) { + for (const v of this.voiceContainer.voiceDrawOrder!) { + for (const h of this.helpers.beamHelpers[v]) { this.paintBeamHelper(cx, cy, canvas, h, flagsElement, beamsElement); } } @@ -440,7 +440,7 @@ export abstract class LineBarRenderer extends BarRendererBase { if (beat.isRest) { return false; } - + const helper = this.helpers.getBeamingHelperForBeat(beat); if (helper) { return helper.hasFlag(this.drawBeamHelperAsFlags(helper), beat); @@ -449,6 +449,19 @@ export abstract class LineBarRenderer extends BarRendererBase { return BeamingHelper.beatHasFlag(beat); } + public hasStem(beat: Beat) { + if (beat.isRest) { + return false; + } + + const helper = this.helpers.getBeamingHelperForBeat(beat); + if (helper) { + return helper.hasStem(this.drawBeamHelperAsFlags(helper), beat); + } + + return BeamingHelper.beatHasStem(beat); + } + protected paintBeamHelper( cx: number, cy: number, @@ -551,7 +564,7 @@ export abstract class LineBarRenderer extends BarRendererBase { cx + this.x + beatLineX + flagWidth / 2, (topY + bottomY - this.smuflMetrics.glyphHeights.get(MusicFontSymbol.GraceNoteSlashStemDown)!) / 2, - NoteHeadGlyph.GraceScale, + EngravingSettings.GraceScale, MusicFontSymbol.GraceNoteSlashStemDown, true ); @@ -561,7 +574,7 @@ export abstract class LineBarRenderer extends BarRendererBase { cx + this.x + beatLineX + flagWidth / 2, (topY + bottomY + this.smuflMetrics.glyphHeights.get(MusicFontSymbol.GraceNoteSlashStemUp)!) / 2, - NoteHeadGlyph.GraceScale, + EngravingSettings.GraceScale, MusicFontSymbol.GraceNoteSlashStemUp, true ); @@ -640,7 +653,7 @@ export abstract class LineBarRenderer extends BarRendererBase { protected paintBar(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper, beamsElement: BeatSubElement): void { const direction: BeamDirection = this.getBeamDirection(h); const isGrace: boolean = h.graceType !== GraceType.None; - const scaleMod: number = isGrace ? NoteHeadGlyph.GraceScale : 1; + const scaleMod: number = isGrace ? EngravingSettings.GraceScale : 1; let barSpacing: number = (this.smuflMetrics.beamSpacing + this.smuflMetrics.beamThickness) * scaleMod; let barSize: number = this.smuflMetrics.beamThickness * scaleMod; if (direction === BeamDirection.Down) { @@ -769,7 +782,8 @@ export abstract class LineBarRenderer extends BarRendererBase { if (h.graceType === GraceType.BeforeBeat) { const beatLineX: number = this.getBeatX(h.beats[0], BeatXPosition.Stem); - const flagWidth = this.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * NoteHeadGlyph.GraceScale; + const flagWidth = + this.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * EngravingSettings.GraceScale; let slashY: number = (cy + this.y + this.calculateBeamY(h, beatLineX)) | 0; slashY += barSize + barSpacing; @@ -778,7 +792,7 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas, cx + this.x + beatLineX + flagWidth / 2, slashY, - NoteHeadGlyph.GraceScale, + EngravingSettings.GraceScale, MusicFontSymbol.GraceNoteSlashStemDown, true ); @@ -787,7 +801,7 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas, cx + this.x + beatLineX + flagWidth / 2, slashY, - NoteHeadGlyph.GraceScale, + EngravingSettings.GraceScale, MusicFontSymbol.GraceNoteSlashStemUp, true ); @@ -901,7 +915,7 @@ export abstract class LineBarRenderer extends BarRendererBase { if (h.drawingInfos.has(direction)) { return; } - const scale = h.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scale = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2; const drawingInfo = new BeamingHelperDrawInfo(); @@ -1023,7 +1037,7 @@ export abstract class LineBarRenderer extends BarRendererBase { let barSpacing = 0; if (h.restBeats.length > 0) { // space needed for the bars, rests need to be below them - const scaleMod: number = h.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scaleMod: number = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; barSpacing = barCount * (this.smuflMetrics.beamSpacing + this.smuflMetrics.beamThickness) * scaleMod; } diff --git a/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts b/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts index c45f1a4eb..36c5b95d9 100644 --- a/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts @@ -1,34 +1,144 @@ -import { Beat } from '@coderline/alphatab/model/Beat'; -import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; -import type { VoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/VoiceContainerGlyph'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import type { GraceGroup } from '@coderline/alphatab/model/GraceGroup'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import type { Note } from '@coderline/alphatab/model/Note'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { BarBounds } from '@coderline/alphatab/rendering/_barrel'; +import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { BeatContainerGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { MultiBarRestGlyph } from '@coderline/alphatab/rendering/glyphs/MultiBarRestGlyph'; +import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; /** * @internal */ -export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyph { - public constructor(voiceContainer: VoiceContainerGlyph) { - super(MultiBarRestBeatContainerGlyph._getOrCreatePlaceholderBeat(voiceContainer), voiceContainer); - this.preNotes = new BeatGlyphBase(); - this.onNotes = new BeatOnNoteGlyphBase(); +export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyphBase { + private _glyph?: MultiBarRestGlyph; + + public constructor() { + super(0, 0); } + public override get absoluteDisplayStart(): number { + return this.renderer.bar.masterBar.start; + } + public override get onTimeX(): number { + return 0; + } + public override get graceType(): GraceType { + return GraceType.None; + } + public override get graceIndex(): number { + return 0; + } + public override get graceGroup(): GraceGroup | null { + return null; + } + public override get voiceIndex(): number { + return 0; + } + public override get isFirstOfTupletGroup(): boolean { + return false; + } + public override get tupletGroup(): TupletGroup | null { + return null; + } + public override get isLastOfVoice(): boolean { + return true; + } + + public override get displayDuration(): number { + return 0; + } + + public override getRestY(requestedPosition: NoteYPosition): number { + const g = this._glyph; + if (g) { + switch (requestedPosition) { + case NoteYPosition.TopWithStem: + case NoteYPosition.Top: + return g.y; + case NoteYPosition.Center: + case NoteYPosition.StemUp: + case NoteYPosition.StemDown: + return g.y + g.height / 2; + case NoteYPosition.Bottom: + case NoteYPosition.BottomWithStem: + return g.y + g.height; + } + } + return 0; + } + + public override getNoteY(_note: Note, requestedPosition: NoteYPosition): number { + return this.getRestY(requestedPosition); + } + + public override getNoteX(_note: Note, requestedPosition: NoteXPosition): number { + const g = this._glyph; + if (g) { + switch (requestedPosition) { + case NoteXPosition.Left: + return g.x; + case NoteXPosition.Center: + return g.x + g.width / 2; + case NoteXPosition.Right: + return g.x + g.width; + } + } + return 0; + } + + public override getBeatX(requestedPosition: BeatXPosition, _useSharedSizes: boolean): number { + const g = this._glyph; + if (g) { + switch (requestedPosition) { + case BeatXPosition.PreNotes: + return g.x; + case BeatXPosition.OnNotes: + case BeatXPosition.MiddleNotes: + case BeatXPosition.Stem: + return g.x + g.width / 2; + case BeatXPosition.PostNotes: + return g.x + g.width; + case BeatXPosition.EndBeat: + return this.width; + } + } + return 0; + } + public override registerLayoutingInfo(layoutings: BarLayoutingInfo): void { + const width = this._glyph?.width ?? 0; + layoutings.addBeatSpring(this, 0, width); + } + + public override applyLayoutingInfo(_info: BarLayoutingInfo): void {} + + public override buildBoundingsLookup(_barBounds: BarBounds, _cx: number, _cy: number): void {} + public override doLayout(): void { if (this.renderer.showMultiBarRest) { - this.onNotes.addNormal(new MultiBarRestGlyph()); + this._glyph = new MultiBarRestGlyph(); + this._glyph.renderer = this.renderer; + this._glyph.doLayout(); + this.width = this._glyph.width; } + } - super.doLayout(); + public override doMultiVoiceLayout(): void { + // nothing to do } - private static _getOrCreatePlaceholderBeat(voiceContainer: VoiceContainerGlyph): Beat { - if (voiceContainer.voice.beats.length > 1) { - return voiceContainer.voice.beats[0]; - } - const placeholder = new Beat(); - placeholder.voice = voiceContainer.voice; - return placeholder; + public override getBoundingBoxTop(): number { + return this._glyph?.getBoundingBoxTop() ?? Number.NaN; + } + + public override getBoundingBoxBottom(): number { + return this._glyph?.getBoundingBoxBottom() ?? Number.NaN; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + this._glyph?.paint(cx + this.x, cy + this.y, canvas); } } diff --git a/packages/alphatab/src/rendering/NumberedBarRenderer.ts b/packages/alphatab/src/rendering/NumberedBarRenderer.ts index 4089b48bd..83c103775 100644 --- a/packages/alphatab/src/rendering/NumberedBarRenderer.ts +++ b/packages/alphatab/src/rendering/NumberedBarRenderer.ts @@ -1,27 +1,24 @@ import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; import type { Voice } from '@coderline/alphatab/model/Voice'; import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import type { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; +import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import { NumberedBeatContainerGlyph } from '@coderline/alphatab/rendering/NumberedBeatContainerGlyph'; -import { NumberedBeatGlyph, NumberedBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedBeatGlyph'; +import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; +import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; +import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; +import { NumberedKeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedKeySignatureGlyph'; import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; -import { NumberedKeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedKeySignatureGlyph'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; -import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; /** * This BarRenderer renders a bar using (Jianpu) Numbered Music Notation @@ -36,11 +33,14 @@ export class NumberedBarRenderer extends LineBarRenderer { public shortestDuration = Duration.QuadrupleWhole; public lowestOctave: number | null = null; public highestOctave: number | null = null; + public octaves = new Map(); + get dotSpacing(): number { return this.smuflMetrics.glyphHeights.get(MusicFontSymbol.AugmentationDot)! * 2; } - public registerOctave(octave: number) { + public registerOctave(beat: Beat, octave: number) { + this.octaves.set(beat, octave); if (this.lowestOctave === null) { this.lowestOctave = octave; this.highestOctave = octave; @@ -105,17 +105,7 @@ export class NumberedBarRenderer extends LineBarRenderer { public override doLayout(): void { super.doLayout(); - let hasTuplets: boolean = false; - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const c = this.getVoiceContainer(voice)!; - if (c.tupletGroups.length > 0) { - hasTuplets = true; - break; - } - } - } - if (hasTuplets) { + if (this.voiceContainer.tupletGroups.size > 0) { this.registerOverflowTop(this.tupletSize); } @@ -203,8 +193,7 @@ export class NumberedBarRenderer extends LineBarRenderer { canvas.fillRect(cx + this.x + barStartX, barStartY, barEndX - barStartX, barSize); } - const onNotes = this.getBeatContainer(beat)!.onNotes; - let dotCount = onNotes instanceof NumberedBeatGlyph ? (onNotes as NumberedBeatGlyph).octaveDots : 0; + let dotCount = this.octaves.has(beat) ? this.octaves.get(beat)! : 0; const dotSpacing = this.dotSpacing; let dotsY = 0; let dotsOffset = 0; @@ -342,12 +331,13 @@ export class NumberedBarRenderer extends LineBarRenderer { } protected override createVoiceGlyphs(v: Voice): void { + if (v.index > 0) { + return; + } + super.createVoiceGlyphs(v); for (const b of v.beats) { - const container: NumberedBeatContainerGlyph = new NumberedBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = v.index === 0 ? new NumberedBeatPreNotesGlyph() : new BeatGlyphBase(); - container.onNotes = v.index === 0 ? new NumberedBeatGlyph() : new BeatOnNoteGlyphBase(); - this.addBeatGlyph(container); + this.addBeatGlyph(new NumberedBeatContainerGlyph(b)); } } diff --git a/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts b/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts index 90681d405..0566a47e6 100644 --- a/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts @@ -1,6 +1,8 @@ +import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; import { NumberedTieGlyph } from '@coderline/alphatab/rendering//glyphs/NumberedTieGlyph'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { NumberedBeatGlyph, NumberedBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedBeatGlyph'; import { NumberedSlurGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedSlurGlyph'; import type { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; @@ -11,6 +13,12 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { private _slurs: Map = new Map(); private _effectSlurs: NumberedSlurGlyph[] = []; + public constructor(beat: Beat) { + super(beat); + this.preNotes = new NumberedBeatPreNotesGlyph(); + this.onNotes = new NumberedBeatGlyph(); + } + public override doLayout(): void { this._slurs.clear(); this._effectSlurs = []; @@ -32,7 +40,11 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { const tie = new NumberedTieGlyph(`numbered.tie.${n.tieOrigin!.beat.id}`, n.tieOrigin!, n, true); this.addTie(tie); } - if (n.isLeftHandTapped && !n.isHammerPullDestination && !this._slurs.has(`numbered.tie.leftHandTap.${n.beat.id}`)) { + 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); @@ -72,13 +84,7 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { } } if (!expanded) { - const effectSlur = new NumberedSlurGlyph( - `numbered.slur.effect`, - 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); diff --git a/packages/alphatab/src/rendering/ScoreBarRenderer.ts b/packages/alphatab/src/rendering/ScoreBarRenderer.ts index 76f5527f1..cde341070 100644 --- a/packages/alphatab/src/rendering/ScoreBarRenderer.ts +++ b/packages/alphatab/src/rendering/ScoreBarRenderer.ts @@ -1,3 +1,4 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; @@ -15,8 +16,6 @@ import { ClefGlyph } from '@coderline/alphatab/rendering/glyphs/ClefGlyph'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { KeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/KeySignatureGlyph'; import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { ScoreBeatGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatGlyph'; -import { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; @@ -60,10 +59,6 @@ export class ScoreBarRenderer extends LineBarRenderer { return BarSubElement.StandardNotationStaffLine; } - public override get showMultiBarRest(): boolean { - return true; - } - public override get lineSpacing(): number { return this.smuflMetrics.oneStaffSpace; } @@ -125,7 +120,7 @@ export class ScoreBarRenderer extends LineBarRenderer { if (beat.slashed) { let slashY = this._getSlashFlagY(); const symbol = SlashNoteHeadGlyph.getSymbol(beat.duration); - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scale = beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; if (direction === BeamDirection.Down) { slashY -= this.smuflMetrics.stemDown.has(symbol) @@ -145,7 +140,7 @@ export class ScoreBarRenderer extends LineBarRenderer { const minNote = this.accidentalHelper.getMinStepsNote(beat); if (minNote) { - return this.getBeatContainer(beat)!.onNotes.getNoteY( + return this.getNoteY( minNote, direction === BeamDirection.Up ? NoteYPosition.TopWithStem : NoteYPosition.StemDown ); @@ -154,7 +149,7 @@ export class ScoreBarRenderer extends LineBarRenderer { let y = this.getScoreY(this.accidentalHelper.getMinSteps(beat)); if (direction === BeamDirection.Up && !beat.isRest) { - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scale = beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; y -= this.smuflMetrics.standardStemLength * scale; } @@ -165,7 +160,7 @@ export class ScoreBarRenderer extends LineBarRenderer { if (beat.slashed) { let slashY = this._getSlashFlagY(); const symbol = SlashNoteHeadGlyph.getSymbol(beat.duration); - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scale = beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; if (direction === BeamDirection.Down) { slashY -= this.smuflMetrics.stemDown.has(symbol) @@ -183,7 +178,7 @@ export class ScoreBarRenderer extends LineBarRenderer { const maxNote = this.accidentalHelper.getMaxStepsNote(beat); if (maxNote) { - return this.getBeatContainer(beat)!.onNotes.getNoteY( + return this.getNoteY( maxNote, direction === BeamDirection.Up ? NoteYPosition.StemUp : NoteYPosition.BottomWithStem ); @@ -191,7 +186,7 @@ export class ScoreBarRenderer extends LineBarRenderer { let y = this.getScoreY(this.accidentalHelper.getMaxSteps(beat)); if (direction === BeamDirection.Down) { - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scale = beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; y += this.smuflMetrics.standardStemLength * scale; } return y; @@ -236,7 +231,7 @@ export class ScoreBarRenderer extends LineBarRenderer { // estimate on the position const steps = AccidentalHelper.computeStepsWithoutAccidentals(this.bar, note); y = this.getScoreY(steps); - const scale = note.beat.graceType === GraceType.None ? 1 : NoteHeadGlyph.GraceScale; + const scale = note.beat.graceType === GraceType.None ? 1 : EngravingSettings.GraceScale; const stemHeight = this.smuflMetrics.standardStemLength * scale; const noteHeadHeight = this.smuflMetrics.glyphHeights.get(NoteHeadGlyph.getSymbol(note.beat.duration))! * scale; @@ -302,14 +297,14 @@ export class ScoreBarRenderer extends LineBarRenderer { if (direction === BeamDirection.Up) { const maxNote = this.accidentalHelper.getMaxStepsNote(beat); if (maxNote) { - return this.getBeatContainer(beat)!.onNotes.getNoteY(maxNote, NoteYPosition.StemUp); + return this.getNoteY(maxNote, NoteYPosition.StemUp); } return this.getScoreY(this.accidentalHelper.getMaxSteps(beat)); } const minNote = this.accidentalHelper.getMinStepsNote(beat); if (minNote) { - return this.getBeatContainer(beat)!.onNotes.getNoteY(minNote, NoteYPosition.StemDown); + return this.getNoteY(minNote, NoteYPosition.StemDown); } return this.getScoreY(this.accidentalHelper.getMinSteps(beat)); } @@ -479,10 +474,7 @@ export class ScoreBarRenderer extends LineBarRenderer { protected override createVoiceGlyphs(v: Voice): void { super.createVoiceGlyphs(v); for (const b of v.beats) { - const container: ScoreBeatContainerGlyph = new ScoreBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = new ScoreBeatPreNotesGlyph(); - container.onNotes = new ScoreBeatGlyph(); - this.addBeatGlyph(container); + this.addBeatGlyph(new ScoreBeatContainerGlyph(b)); } } diff --git a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts index e24b5cdaa..c982f1c9c 100644 --- a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts @@ -1,3 +1,4 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import type { Beat } from '@coderline/alphatab/model/Beat'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; @@ -8,7 +9,8 @@ import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { FlagGlyph } from '@coderline/alphatab/rendering/glyphs/FlagGlyph'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { ScoreBeatGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatGlyph'; +import { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreBendGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBendGlyph'; import { ScoreLegatoGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreLegatoGlyph'; import { ScoreSlideLineGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreSlideLineGlyph'; @@ -24,6 +26,33 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { private _effectSlur: ScoreSlurGlyph | null = null; private _effectEndSlur: ScoreSlurGlyph | null = null; + public constructor(beat: Beat) { + super(beat); + this.preNotes = new ScoreBeatPreNotesGlyph(); + this.onNotes = new ScoreBeatGlyph(); + } + + public get prebendNoteHeadOffset() { + return (this.preNotes as ScoreBeatPreNotesGlyph).prebendNoteHeadOffset; + } + + public get accidentalsWidth() { + const preNotes = this.preNotes as ScoreBeatPreNotesGlyph; + if (preNotes && preNotes.accidentals) { + return preNotes.accidentals.width; + } + return 0; + } + + public override doMultiVoiceLayout(): void { + this.preNotes.x = 0; + (this.preNotes as ScoreBeatPreNotesGlyph).doMultiVoiceLayout(); + this.onNotes.x = this.preNotes.x + this.preNotes.width; + (this.onNotes as ScoreBeatGlyph).doMultiVoiceLayout(); + + this._bend?.doMultiVoiceLayout(); + } + public override doLayout(): void { this._effectSlur = null; this._effectEndSlur = null; @@ -34,14 +63,14 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { const isGrace = beat.graceType !== GraceType.None; if (sr.hasFlag(beat)) { const direction = this.renderer.getBeatDirection(beat); - const scale = isGrace ? NoteHeadGlyph.GraceScale : 1; + const scale = isGrace ? EngravingSettings.GraceScale : 1; const symbol = FlagGlyph.getSymbol(beat.duration, direction, isGrace); const flagWidth = this.renderer.smuflMetrics.glyphWidths.get(symbol)! * scale; this._flagStretch = flagWidth; } else if (isGrace) { // always use flag size as spacing on grace notes const graceSpacing = - this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * NoteHeadGlyph.GraceScale; + this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * EngravingSettings.GraceScale; this._flagStretch = graceSpacing; } @@ -128,7 +157,7 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { } if (n.hasBend) { if (!this._bend) { - const bend = new ScoreBendGlyph(n.beat); + const bend = new ScoreBendGlyph(this); this._bend = bend; bend.renderer = this.renderer; this.addTie(bend); diff --git a/packages/alphatab/src/rendering/SlashBarRenderer.ts b/packages/alphatab/src/rendering/SlashBarRenderer.ts index 77a88a9cd..94dce013d 100644 --- a/packages/alphatab/src/rendering/SlashBarRenderer.ts +++ b/packages/alphatab/src/rendering/SlashBarRenderer.ts @@ -1,3 +1,4 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import { GraceType } from '@coderline/alphatab/model/GraceType'; @@ -7,15 +8,11 @@ import type { Voice } from '@coderline/alphatab/model/Voice'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { LineBarRenderer } from '@coderline/alphatab/rendering//LineBarRenderer'; import type { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import { SlashBeatContainerGlyph } from '@coderline/alphatab/rendering/SlashBeatContainerGlyph'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; -import { SlashBeatGlyph } from '@coderline/alphatab/rendering/glyphs/SlashBeatGlyph'; import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; +import { SlashBeatContainerGlyph } from '@coderline/alphatab/rendering/SlashBeatContainerGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; @@ -83,17 +80,7 @@ export class SlashBarRenderer extends LineBarRenderer { public override doLayout(): void { super.doLayout(); - let hasTuplets: boolean = false; - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const c = this.getVoiceContainer(voice)!; - if (c.tupletGroups.length > 0) { - hasTuplets = true; - break; - } - } - } - if (hasTuplets) { + if (this.voiceContainer.tupletGroups.size > 0) { this.registerOverflowTop(this.tupletSize); } } @@ -105,7 +92,7 @@ export class SlashBarRenderer extends LineBarRenderer { protected override getFlagTopY(beat: Beat, _direction: BeamDirection): number { let slashY = this.getLineY(0); const symbol = SlashNoteHeadGlyph.getSymbol(beat.duration); - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scale = beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; slashY -= this.smuflMetrics.stemUp.has(symbol) ? this.smuflMetrics.stemUp.get(symbol)!.bottomY * scale : 0; if (!beat.isRest) { @@ -118,7 +105,7 @@ export class SlashBarRenderer extends LineBarRenderer { protected override getFlagBottomY(beat: Beat, _direction: BeamDirection): number { let slashY = this.getLineY(0); const symbol = SlashNoteHeadGlyph.getSymbol(beat.duration); - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scale = beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; slashY -= this.smuflMetrics.stemUp.has(symbol) ? this.smuflMetrics.stemUp.get(symbol)!.bottomY * scale : 0; @@ -192,12 +179,13 @@ export class SlashBarRenderer extends LineBarRenderer { } protected override createVoiceGlyphs(v: Voice): void { + if (v.index > 0) { + return; + } + super.createVoiceGlyphs(v); for (const b of v.beats) { - const container: SlashBeatContainerGlyph = new SlashBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = new BeatGlyphBase(); - container.onNotes = v.index === 0 ? new SlashBeatGlyph() : new BeatOnNoteGlyphBase(); - this.addBeatGlyph(container); + this.addBeatGlyph(new SlashBeatContainerGlyph(b)); } } diff --git a/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts b/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts index f6bb49fd7..19168513e 100644 --- a/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts @@ -1,9 +1,12 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import type { Beat } from '@coderline/alphatab/model/Beat'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; import { FlagGlyph } from '@coderline/alphatab/rendering/glyphs/FlagGlyph'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { SlashBeatGlyph } from '@coderline/alphatab/rendering/glyphs/SlashBeatGlyph'; import { SlashTieGlyph } from '@coderline/alphatab/rendering/glyphs/SlashTieGlyph'; import type { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRenderer'; @@ -13,6 +16,12 @@ import type { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRen export class SlashBeatContainerGlyph extends BeatContainerGlyph { private _tiedNoteTie: SlashTieGlyph | null = null; + public constructor(beat:Beat){ + super(beat); + this.preNotes = new BeatGlyphBase(); + this.onNotes = new SlashBeatGlyph(); + } + public override doLayout(): void { // make space for flag const sr = this.renderer as SlashBarRenderer; @@ -20,14 +29,14 @@ export class SlashBeatContainerGlyph extends BeatContainerGlyph { const isGrace = beat.graceType !== GraceType.None; if (sr.hasFlag(beat)) { const direction = this.renderer.getBeatDirection(beat); - const scale = isGrace ? NoteHeadGlyph.GraceScale : 1; + const scale = isGrace ? EngravingSettings.GraceScale : 1; const symbol = FlagGlyph.getSymbol(beat.duration, direction, isGrace); const flagWidth = this.renderer.smuflMetrics.glyphWidths.get(symbol)! * scale; this._flagStretch = flagWidth; } else if (isGrace) { // always use flag size as spacing on grace notes const graceSpacing = - this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * NoteHeadGlyph.GraceScale; + this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * EngravingSettings.GraceScale; this._flagStretch = graceSpacing; } diff --git a/packages/alphatab/src/rendering/TabBarRenderer.ts b/packages/alphatab/src/rendering/TabBarRenderer.ts index da7585cff..fcc737a60 100644 --- a/packages/alphatab/src/rendering/TabBarRenderer.ts +++ b/packages/alphatab/src/rendering/TabBarRenderer.ts @@ -10,14 +10,11 @@ import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import { TabBeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatContainerGlyph'; -import { TabBeatGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatGlyph'; -import { TabBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatPreNotesGlyph'; +import type { TabBeatGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatGlyph'; import { TabClefGlyph } from '@coderline/alphatab/rendering/glyphs/TabClefGlyph'; import type { TabNoteChordGlyph } from '@coderline/alphatab/rendering/glyphs/TabNoteChordGlyph'; import { TabTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/TabTimeSignatureGlyph'; -import type { VoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/VoiceContainerGlyph'; import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import { MultiBarRestBeatContainerGlyph } from '@coderline/alphatab/rendering/MultiBarRestBeatContainerGlyph'; import { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import type { ReservedLayoutAreaSlot } from '@coderline/alphatab/rendering/utils/BarCollisionHelper'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; @@ -87,24 +84,25 @@ export class TabBarRenderer extends LineBarRenderer { public maxString = Number.NaN; protected override collectSpaces(spaces: Float32Array[][]): void { + if (this.additionalMultiRestBars) { + return; + } + const padding: number = this.smuflMetrics.staffLineThickness; const tuning = this.bar.staff.tuning; - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const vc: VoiceContainerGlyph = this.getVoiceContainer(voice)!; - for (const bg of vc.beatGlyphs) { - const notes: TabBeatGlyph = bg.onNotes as TabBeatGlyph; - const noteNumbers: TabNoteChordGlyph | null = notes.noteNumbers; - if (noteNumbers) { - for (const [str, noteNumber] of noteNumbers.notesPerString) { - if (!noteNumber.isEmpty) { - spaces[tuning.length - str].push( - new Float32Array([ - vc.x + bg.x + notes.x + noteNumbers!.x - padding, - noteNumbers!.width + padding * 2 - ]) - ); - } + for (const voice of this.voiceContainer.beatGlyphs.values()) { + for (const bg of voice) { + const notes: TabBeatGlyph = (bg as TabBeatContainerGlyph).onNotes as TabBeatGlyph; + const noteNumbers: TabNoteChordGlyph | null = notes.noteNumbers; + if (noteNumbers) { + for (const [str, noteNumber] of noteNumbers.notesPerString) { + if (!noteNumber.isEmpty) { + spaces[tuning.length - str].push( + new Float32Array([ + this.beatGlyphsStart + bg.x + notes.x + noteNumbers!.x - padding, + noteNumbers!.width + padding * 2 + ]) + ); } } } @@ -162,16 +160,7 @@ export class TabBarRenderer extends LineBarRenderer { } if (this.rhythmMode !== TabRhythmMode.Hidden) { - this._hasTuplets = false; - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const c: VoiceContainerGlyph = this.getVoiceContainer(voice)!; - if (c.tupletGroups.length > 0) { - this._hasTuplets = true; - break; - } - } - } + this._hasTuplets = this.voiceContainer.tupletGroups.size > 0; if (this._hasTuplets) { this.registerOverflowBottom(this.settings.notation.rhythmHeight + this.tupletSize); } @@ -222,17 +211,9 @@ export class TabBarRenderer extends LineBarRenderer { protected override createVoiceGlyphs(v: Voice): void { super.createVoiceGlyphs(v); - // multibar rest - if (this.additionalMultiRestBars) { - const container = new MultiBarRestBeatContainerGlyph(this.getVoiceContainer(v)!); - this.addBeatGlyph(container); - } else { - for (const b of v.beats) { - const container: TabBeatContainerGlyph = new TabBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = new TabBeatPreNotesGlyph(); - container.onNotes = new TabBeatGlyph(); - this.addBeatGlyph(container); - } + + for (const b of v.beats) { + this.addBeatGlyph(new TabBeatContainerGlyph(b)); } } @@ -277,14 +258,11 @@ export class TabBarRenderer extends LineBarRenderer { } protected override getFlagTopY(beat: Beat, _direction: BeamDirection): number { - const startGlyph: TabBeatGlyph = this.getOnNotesGlyphForBeat(beat) as TabBeatGlyph; - if (!startGlyph.noteNumbers || beat.duration === Duration.Half) { + const container = this.getBeatContainer(beat); + if (!container || !beat.minStringNote || beat.duration === Duration.Half) { return this.height - this.settings.notation.rhythmHeight - this.tupletSize; } - return ( - startGlyph.noteNumbers.getNoteY(startGlyph.noteNumbers.minStringNote!, NoteYPosition.Bottom) + - this.smuflMetrics.staffLineThickness - ); + return container.getNoteY(beat.minStringNote, NoteYPosition.Bottom) + this.smuflMetrics.staffLineThickness; } protected override getFlagBottomY(_beat: Beat, _direction: BeamDirection): number { diff --git a/packages/alphatab/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts b/packages/alphatab/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts index b86438462..7e6c219bd 100644 --- a/packages/alphatab/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts @@ -1,13 +1,13 @@ -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; /** * @internal */ export class ArticStaccatoAboveGlyph extends MusicFontGlyph { public constructor(x: number, y: number) { - super(x, y, NoteHeadGlyph.GraceScale, MusicFontSymbol.ArticStaccatoAbove); + super(x, y, EngravingSettings.GraceScale, MusicFontSymbol.ArticStaccatoAbove); this.center = true; } diff --git a/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts b/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts index 66ece1570..fbee6aae0 100644 --- a/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts @@ -312,8 +312,8 @@ export class BarLineGlyph extends LeftToRightLayoutingGlyphGroup { top -= lineYOffset; bottom += lineRenderer.height; } else { - top += lineRenderer.getLineY(0); - bottom += lineRenderer.getLineY(lineRenderer.drawnLineCount - 1); + top += lineRenderer.getLineY(0) - lineYOffset / 2; + bottom += lineRenderer.getLineY(lineRenderer.drawnLineCount - 1) + lineYOffset / 2; } const h: number = bottom - top; diff --git a/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts index 925b5d9a7..f505ff11e 100644 --- a/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts @@ -1,13 +1,16 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; +import type { GraceGroup } from '@coderline/alphatab/model/GraceGroup'; +import type { GraceType } from '@coderline/alphatab/model/GraceType'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { Note } from '@coderline/alphatab/model/Note'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import type { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; import type { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import type { ITieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; -import type { VoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/VoiceContainerGlyph'; import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; import type { BarBounds } from '@coderline/alphatab/rendering/utils/BarBounds'; import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; @@ -17,23 +20,96 @@ import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; /** * @internal */ -export class BeatContainerGlyph extends Glyph { +export abstract class BeatContainerGlyphBase extends Glyph { + public abstract get absoluteDisplayStart(): number; + public abstract get displayDuration(): number; + public abstract get onTimeX(): number; + public abstract get graceType(): GraceType; + public abstract get graceIndex(): number; + public abstract get graceGroup(): GraceGroup | null; + public abstract get voiceIndex(): number; + public abstract get isFirstOfTupletGroup(): boolean; + public abstract get tupletGroup(): TupletGroup | null; + public abstract get isLastOfVoice(): boolean; + public abstract getNoteY(note: Note, requestedPosition: NoteYPosition): number; + public abstract doMultiVoiceLayout(): void; + public abstract getRestY(requestedPosition: NoteYPosition): number; + public abstract getNoteX(note: Note, requestedPosition: NoteXPosition): number; + public abstract getBeatX(requestedPosition: BeatXPosition, useSharedSizes: boolean): number; + public abstract registerLayoutingInfo(layoutings: BarLayoutingInfo): void; + public abstract applyLayoutingInfo(info: BarLayoutingInfo): void; + public abstract buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number): void; + public scaleToWidth(beatWidth: number) { + this.width = beatWidth; + } +} + +/** + * @internal + */ +export class BeatContainerGlyph extends BeatContainerGlyphBase { private _ties: ITieGlyph[] = []; - public voiceContainer: VoiceContainerGlyph; public beat: Beat; public preNotes!: BeatGlyphBase; public onNotes!: BeatOnNoteGlyphBase; public minWidth: number = 0; + public override get isLastOfVoice(): boolean { + return this.beat.isLastOfVoice; + } + + public override get displayDuration(): number { + return this.beat.displayDuration; + } + + public override get graceIndex(): number { + return this.beat.graceIndex; + } + + public override get graceType(): GraceType { + return this.beat.graceType; + } + + public override get absoluteDisplayStart(): number { + return this.beat.absoluteDisplayStart; + } + + public override get graceGroup(): GraceGroup | null { + return this.beat.graceGroup; + } + + public override get voiceIndex(): number { + return this.beat.voice.index; + } + + public override get isFirstOfTupletGroup(): boolean { + return this.beat.hasTuplet && this.beat.tupletGroup!.beats[0].id === this.beat.id; + } + + public override get tupletGroup(): TupletGroup | null { + return this.beat.tupletGroup; + } + public get onTimeX(): number { return this.onNotes.x + this.onNotes.onTimeX; } - public constructor(beat: Beat, voiceContainer: VoiceContainerGlyph) { + public constructor(beat: Beat) { super(0, 0); this.beat = beat; this._ties = []; - this.voiceContainer = voiceContainer; + } + + public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { + return this.onNotes.y + this.onNotes.getNoteY(note, requestedPosition); + } + + public override getRestY(requestedPosition: NoteYPosition): number { + return this.onNotes.y + this.onNotes.getRestY(requestedPosition); + } + + public override getNoteX(note: Note, requestedPosition: NoteXPosition): number { + return this.onNotes.x + this.onNotes.getNoteX(note, requestedPosition); } public addTie(tie: ITieGlyph) { @@ -69,12 +145,12 @@ export class BeatContainerGlyph extends Glyph { postBeatStretch += tg.width; } - layoutings.addBeatSpring(this.beat, preBeatStretch, postBeatStretch); + layoutings.addBeatSpring(this, preBeatStretch, postBeatStretch); // store sizes for usages in effects // we might have empty content in the individual bar renderers, but need to know // the "shared" maximum widths - layoutings.setBeatSizes(this.beat, { + layoutings.setBeatSizes(this, { preBeatSize: this.preNotes.width, onBeatSize: this.onNotes.width }); @@ -100,6 +176,10 @@ export class BeatContainerGlyph extends Glyph { this.updateWidth(); } + public override doMultiVoiceLayout(): void { + // do nothing by default, overridden when needed + } + protected updateWidth(): void { this.minWidth = this.preNotes.width + this.onNotes.width; let tieWidth: number = 0; @@ -159,8 +239,13 @@ export class BeatContainerGlyph extends Glyph { // canvas.color = new Color(200, 0, 0, 100); // canvas.strokeRect(cx + this.x + this.preNotes.x, cy + this.y + 10, this.preNotes.width, 10); - // canvas.color = new Color(0, 200, 0, 100); - // canvas.strokeRect(cx + this.x + this.onNotes.x, cy + this.y + 10, this.onNotes.width, 10); + // canvas.color = new Color(0, 200, 0, 100); + // canvas.strokeRect( + // cx + this.x + this.onNotes.x, + // cy + this.y + this.beat.voice.index * 1, + // this.onNotes.width, + // 10 + // ); // canvas.color = new Color(0, 200, 200, 100); // canvas.strokeRect(cx + this.x + this.onNotes.x + this.onNotes.centerX, cy, 1, this.renderer.height); @@ -177,8 +262,8 @@ export class BeatContainerGlyph extends Glyph { this.onNotes.paint(cx + this.x, cy + this.y, canvas); // reason: we have possibly multiple staves involved and need to calculate the correct positions. - const staffX: number = cx - this.voiceContainer.x - this.renderer.x; - const staffY: number = cy - this.voiceContainer.y - this.renderer.y; + const staffX: number = cx - this.renderer.beatGlyphsStart - this.renderer.x; + const staffY: number = cy - this.renderer.y; for (let i: number = 0, j: number = this._ties.length; i < j; i++) { const t = this._ties[i] as unknown as Glyph; t.renderer = this.renderer; @@ -187,7 +272,7 @@ export class BeatContainerGlyph extends Glyph { canvas.endGroup(); } - public buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number, _isEmptyBar: boolean) { + public buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number) { const beatBoundings: BeatBounds = new BeatBounds(); beatBoundings.beat = this.beat; diff --git a/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts b/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts index 15ef6e6be..e478e4e66 100644 --- a/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts +++ b/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts @@ -6,27 +6,15 @@ import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds' /** * @internal */ -export class BeatOnNoteGlyphBase extends BeatGlyphBase { +export abstract class BeatOnNoteGlyphBase extends BeatGlyphBase { public onTimeX: number = 0; public middleX: number = 0; public stemX: number = 0; - public buildBoundingsLookup(_beatBounds: BeatBounds, _cx: number, _cy: number) { - // implemented in subclasses - } - - public getNoteX(_note: Note, _requestedPosition: NoteXPosition): number { - return 0; - } - public getNoteY(_note: Note, _requestedPosition: NoteYPosition): number { - return 0; - } - - public getHighestNoteY(): number { - return 0; - } - - public getLowestNoteY(): number { - return 0; - } + public abstract buildBoundingsLookup(_beatBounds: BeatBounds, _cx: number, _cy: number): void; + public abstract getNoteX(_note: Note, _requestedPosition: NoteXPosition): number; + public abstract getNoteY(_note: Note, _requestedPosition: NoteYPosition): number; + public abstract getRestY(_requestedPosition: NoteYPosition): number; + public abstract getHighestNoteY(): number; + public abstract getLowestNoteY(): number; } diff --git a/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts b/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts index 20532b1c2..fecb77d7d 100644 --- a/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts @@ -1,6 +1,7 @@ -import type { Color } from '@coderline/alphatab/model/Color'; +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; import type { Beat } from '@coderline/alphatab/model/Beat'; +import type { Color } from '@coderline/alphatab/model/Color'; import { Duration } from '@coderline/alphatab/model/Duration'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; @@ -8,7 +9,10 @@ import { AccidentalGroupGlyph } from '@coderline/alphatab/rendering/glyphs/Accid import { GhostNoteContainerGlyph } from '@coderline/alphatab/rendering/glyphs/GhostNoteContainerGlyph'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { ScoreNoteChordGlyphBase } from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyphBase'; +import { + ScoreChordNoteHeadInfo, + ScoreNoteChordGlyphBase +} from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyphBase'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; @@ -16,7 +20,6 @@ import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection * @internal */ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { - private _beat: Beat; private _showParenthesis: boolean = false; private _noteValueLookup: Map = new Map(); @@ -24,20 +27,28 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { private _preNoteParenthesis: GhostNoteContainerGlyph | null = null; private _postNoteParenthesis: GhostNoteContainerGlyph | null = null; public isEmpty: boolean = true; + private _groupId: string; public override get scale(): number { - return NoteHeadGlyph.GraceScale; + return EngravingSettings.GraceScale; + } + + public override get hasFlag(): boolean { + return false; + } + + public override get hasStem(): boolean { + return false; } public get direction(): BeamDirection { return BeamDirection.Up; } - public noteHeadOffset: number = 0; - - public constructor(beat: Beat, showParenthesis: boolean = false) { + public constructor(groupId: string, beat: Beat, showParenthesis: boolean = false) { super(); this._beat = beat; + this._groupId = groupId; this._showParenthesis = showParenthesis; if (showParenthesis) { this._preNoteParenthesis = new GhostNoteContainerGlyph(true); @@ -45,6 +56,18 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { } } + protected override getScoreChordNoteHeadInfo(): ScoreChordNoteHeadInfo { + // TODO: do we need to share this spacing across all staves&tracks? + const staff = this._beat.voice.bar.staff; + const key = `score.noteheads.${this._groupId}.${staff.track.index}.${staff.index}.${this._beat.absoluteDisplayStart}`; + let existing = this.renderer.staff!.getSharedLayoutData(key, undefined); + if (!existing) { + existing = new ScoreChordNoteHeadInfo(this.direction); + this.renderer.staff!.setSharedLayoutData(key, existing); + } + return new ScoreChordNoteHeadInfo(this.direction); + } + public containsNoteValue(noteValue: number): boolean { return this._noteValueLookup.has(noteValue); } @@ -74,7 +97,7 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { this._postNoteParenthesis!.addParenthesisOnSteps(steps, true); } if (accidental !== AccidentalType.None) { - const g = new AccidentalGlyph(0, noteHeadGlyph.y, accidental, NoteHeadGlyph.GraceScale); + const g = new AccidentalGlyph(0, noteHeadGlyph.y, accidental, EngravingSettings.GraceScale); g.renderer = this.renderer; this._accidentals.renderer = this.renderer; this._accidentals.addGlyph(g); @@ -101,7 +124,6 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { } this.noteStartX = x; super.doLayout(); - this.noteHeadOffset = this.noteStartX + (this.width - this.noteStartX) / 2; if (this._showParenthesis) { this._postNoteParenthesis!.x = this.width + this.renderer.smuflMetrics.bendNoteHeadElementPadding; this._postNoteParenthesis!.renderer = this.renderer; diff --git a/packages/alphatab/src/rendering/glyphs/DeadNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/DeadNoteHeadGlyph.ts index d32083a2b..29e64028c 100644 --- a/packages/alphatab/src/rendering/glyphs/DeadNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/DeadNoteHeadGlyph.ts @@ -1,12 +1,11 @@ -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ -export class DeadNoteHeadGlyph extends MusicFontGlyph { +export class DeadNoteHeadGlyph extends NoteHeadGlyphBase { public constructor(x: number, y: number, isGrace: boolean) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, MusicFontSymbol.NoteheadXOrnate); + super(x, y, isGrace, MusicFontSymbol.NoteheadXOrnate); } } diff --git a/packages/alphatab/src/rendering/glyphs/DiamondNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/DiamondNoteHeadGlyph.ts index aa1658c4e..18cc45075 100644 --- a/packages/alphatab/src/rendering/glyphs/DiamondNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/DiamondNoteHeadGlyph.ts @@ -1,14 +1,13 @@ import { Duration } from '@coderline/alphatab/model/Duration'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ -export class DiamondNoteHeadGlyph extends MusicFontGlyph { +export class DiamondNoteHeadGlyph extends NoteHeadGlyphBase { public constructor(x: number, y: number, duration: Duration, isGrace: boolean) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, DiamondNoteHeadGlyph._getSymbol(duration)); + super(x, y, isGrace, DiamondNoteHeadGlyph._getSymbol(duration)); } private static _getSymbol(duration: Duration): MusicFontSymbol { diff --git a/packages/alphatab/src/rendering/glyphs/FlagGlyph.ts b/packages/alphatab/src/rendering/glyphs/FlagGlyph.ts index 19818548d..6782ea508 100644 --- a/packages/alphatab/src/rendering/glyphs/FlagGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/FlagGlyph.ts @@ -1,15 +1,15 @@ +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 { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ export class FlagGlyph extends MusicFontGlyph { public constructor(x: number, y: number, duration: Duration, direction: BeamDirection, isGrace: boolean) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, FlagGlyph.getSymbol(duration, direction, isGrace)); + super(x, y, isGrace ? EngravingSettings.GraceScale : 1, FlagGlyph.getSymbol(duration, direction, isGrace)); } public static getSymbol(duration: Duration, direction: BeamDirection, isGrace: boolean): MusicFontSymbol { diff --git a/packages/alphatab/src/rendering/glyphs/GuitarGolpeGlyph.ts b/packages/alphatab/src/rendering/glyphs/GuitarGolpeGlyph.ts index 613a2c90d..cc872e133 100644 --- a/packages/alphatab/src/rendering/glyphs/GuitarGolpeGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/GuitarGolpeGlyph.ts @@ -1,5 +1,5 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; /** @@ -7,7 +7,7 @@ import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGl */ export class GuitarGolpeGlyph extends MusicFontGlyph { public constructor(x: number, y: number, center: boolean = false) { - super(x, y, NoteHeadGlyph.GraceScale, MusicFontSymbol.GuitarGolpe); + super(x, y, EngravingSettings.GraceScale, MusicFontSymbol.GuitarGolpe); this.center = center; } diff --git a/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts new file mode 100644 index 000000000..0640eb327 --- /dev/null +++ b/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts @@ -0,0 +1,294 @@ +import { Environment } from '@coderline/alphatab/Environment'; +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import type { Note } from '@coderline/alphatab/model/Note'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; +import { VoiceSubElement } from '@coderline/alphatab/model/Voice'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { BarBounds } from '@coderline/alphatab/rendering/_barrel'; +import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import type { BeatContainerGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; + +/** + * This glyph acts as container for handling + * multiple voice rendering + * @internal + */ +export class MultiVoiceContainerGlyph extends Glyph { + public static readonly KeySizeBeat: string = 'Beat'; + + public voiceDrawOrder?: number[]; + + public beatGlyphs = new Map(); + public tupletGroups = new Map(); + + public constructor() { + super(0, 0); + } + + public override getBoundingBoxTop(): number { + let y = Number.NaN; + for (const v of this.beatGlyphs.values()) { + for (const b of v) { + y = ModelUtils.minBoundingBox(y, b.getBoundingBoxTop()); + } + } + return y; + } + + public override getBoundingBoxBottom(): number { + let y = Number.NaN; + for (const v of this.beatGlyphs.values()) { + for (const b of v) { + y = ModelUtils.maxBoundingBox(y, b.getBoundingBoxBottom()); + } + } + return y; + } + + public scaleToWidth(width: number): void { + const force: number = this.renderer.layoutingInfo.spaceToForce(width); + this._scaleToForce(force); + } + + private _scaleToForce(force: number): void { + this.width = this.renderer.layoutingInfo.calculateVoiceWidth(force); + const positions: Map = this.renderer.layoutingInfo.buildOnTimePositions(force); + for (const beatGlyphs of this.beatGlyphs.values()) { + for (let i: number = 0, j: number = beatGlyphs.length; i < j; i++) { + const currentBeatGlyph = beatGlyphs[i]; + + switch (currentBeatGlyph.graceType) { + case GraceType.None: + currentBeatGlyph.x = + positions.get(currentBeatGlyph.absoluteDisplayStart)! - currentBeatGlyph.onTimeX; + break; + default: + const graceDisplayStart = currentBeatGlyph.graceGroup!.beats[0].absoluteDisplayStart; + const graceGroupId = currentBeatGlyph.graceGroup!.id; + // placement for proper grace notes which have a following note + if (currentBeatGlyph.graceGroup!.isComplete && positions.has(graceDisplayStart)) { + currentBeatGlyph.x = positions.get(graceDisplayStart)! - currentBeatGlyph.onTimeX; + + const graceSprings = this.renderer.layoutingInfo.allGraceRods.get(graceGroupId)!; + + // get the pre beat stretch of this voice/staff, not the + // shared space. This way we use the potentially empty space (see discussions/1092). + const afterGraceBeat = + currentBeatGlyph.graceGroup!.beats[currentBeatGlyph.graceGroup!.beats.length - 1] + .nextBeat; + const preBeatStretch = afterGraceBeat + ? this.renderer.layoutingInfo.getPreBeatSize(afterGraceBeat) + : 0; + + // move right in front to the note + currentBeatGlyph.x -= preBeatStretch; + // respect the post beat width of the grace note + currentBeatGlyph.x -= graceSprings[currentBeatGlyph.graceIndex].postSpringWidth; + // shift to right position of the particular grace note + + currentBeatGlyph.x += graceSprings[currentBeatGlyph.graceIndex].graceBeatWidth; + // move the whole group again forward for cases where another track has e.g. 3 beats and here we have only 2. + // so we shift the whole group of this voice to stick to the end of the group. + const lastGraceSpring = graceSprings[currentBeatGlyph.graceGroup!.beats.length - 1]; + currentBeatGlyph.x -= lastGraceSpring.graceBeatWidth; + } else { + // placement for improper grace beats where no beat in the same bar follows + const graceSpring = this.renderer.layoutingInfo.incompleteGraceRods.get(graceGroupId)!; + const relativeOffset = + graceSpring[currentBeatGlyph.graceIndex].postSpringWidth - + graceSpring[currentBeatGlyph.graceIndex].preSpringWidth; + + if (i > 0) { + if (currentBeatGlyph.graceIndex === 0) { + // we place the grace beat directly after the previous one + // otherwise this causes flickers on resizing + currentBeatGlyph.x = beatGlyphs[i - 1].x + beatGlyphs[i - 1].width; + } else { + // for the multiple grace glyphs we take the width of the grace rod + // this width setting is aligned with the positioning logic below + currentBeatGlyph.x = + beatGlyphs[i - 1].x + + graceSpring[currentBeatGlyph.graceIndex - 1].postSpringWidth - + graceSpring[currentBeatGlyph.graceIndex - 1].preSpringWidth - + relativeOffset; + } + } else { + currentBeatGlyph.x = -relativeOffset; + } + } + break; + } + + // size always previous glyph after we know the position + // of the next glyph + if (i > 0) { + const beatWidth: number = currentBeatGlyph.x - beatGlyphs[i - 1].x; + beatGlyphs[i - 1].scaleToWidth(beatWidth); + } + // for the last glyph size based on the full width + if (i === j - 1) { + const beatWidth: number = this.width - beatGlyphs[beatGlyphs.length - 1].x; + currentBeatGlyph.scaleToWidth(beatWidth); + } + } + } + } + + public registerLayoutingInfo(info: BarLayoutingInfo): void { + for (const beatGlyphs of this.beatGlyphs.values()) { + for (const b of beatGlyphs) { + b.registerLayoutingInfo(info); + } + } + } + + public applyLayoutingInfo(info: BarLayoutingInfo): void { + for (const beatGlyphs of this.beatGlyphs.values()) { + for (const b of beatGlyphs) { + b.applyLayoutingInfo(info); + } + this._scaleToForce(Math.max(this.renderer.settings.display.stretchForce, info.minStretchForce)); + } + } + + public addGlyph(bg: BeatContainerGlyphBase): void { + let beatGlyphs: BeatContainerGlyphBase[]; + if (this.beatGlyphs.has(bg.voiceIndex)) { + beatGlyphs = this.beatGlyphs.get(bg.voiceIndex)!; + } else { + beatGlyphs = []; + this.beatGlyphs.set(bg.voiceIndex, beatGlyphs); + } + + bg.x = + beatGlyphs.length === 0 ? 0 : beatGlyphs[beatGlyphs.length - 1].x + beatGlyphs[beatGlyphs.length - 1].width; + bg.renderer = this.renderer; + beatGlyphs.push(bg); + const newWidth = bg.x + bg.width; + if (newWidth > this.width) { + this.width = newWidth; + } + if (bg.isFirstOfTupletGroup) { + let tupletGroups: TupletGroup[]; + if (this.tupletGroups.has(bg.voiceIndex)) { + tupletGroups = this.tupletGroups.get(bg.voiceIndex)!; + } else { + tupletGroups = []; + this.tupletGroups.set(bg.voiceIndex, tupletGroups); + } + + tupletGroups.push(bg.tupletGroup!); + } + } + public getBeatX( + beat: Beat, + requestedPosition: BeatXPosition = BeatXPosition.PreNotes, + useSharedSizes: boolean = false + ): number { + const container = this.getBeatContainer(beat); + if (container) { + return container.x + container.getBeatX(requestedPosition, useSharedSizes); + } + return 0; + } + public getNoteX(note: Note, requestedPosition: NoteXPosition): number { + const container = this.getBeatContainer(note.beat); + if (container) { + return container.x + container.getNoteX(note, requestedPosition); + } + return 0; + } + + public getNoteY(note: Note, requestedPosition: NoteYPosition): number { + const beat = this.getBeatContainer(note.beat); + if (beat) { + return beat.y + beat.getNoteY(note, requestedPosition); + } + return Number.NaN; + } + + public getRestY(beat: Beat, requestedPosition: NoteYPosition): number { + const container = this.getBeatContainer(beat); + if (container) { + return container.y + container.getRestY(requestedPosition); + } + return Number.NaN; + } + + public getBeatContainer(beat: Beat): BeatContainerGlyphBase | undefined { + if (!this.beatGlyphs.has(beat.voice.index)) { + return undefined; + } + const beats = this.beatGlyphs.get(beat.voice.index)!; + return beat.index < beats.length ? beats[beat.index] : undefined; + } + + public buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number): void { + for (const [index, c] of this.beatGlyphs) { + const voice = this.renderer.bar.voices[index]; + if (index === 0 || !voice.isEmpty) { + for (const bc of c) { + bc.buildBoundingsLookup(barBounds, cx + this.x, cy + this.y); + } + } + } + } + + public override doLayout(): void { + for (const v of this.beatGlyphs.values()) { + let x = 0; + for (const b of v) { + b.x = x; + b.doLayout(); + x += b.width; + } + + if (x > this.width) { + this.width = x; + } + } + + if (this.renderer.bar.isMultiVoice) { + this._doMultiVoiceLayout(); + } + + // draw order is reversed so that the main voice overlaps secondary ones + this.voiceDrawOrder = Array.from(this.beatGlyphs.keys()); + Environment.sortDescending(this.voiceDrawOrder); + } + + private _doMultiVoiceLayout() { + for (const v of this.beatGlyphs.values()) { + let x = 0; + for (const b of v) { + b.x = x; + b.doMultiVoiceLayout(); + x += b.width; + } + + if (x > this.width) { + this.width = x; + } + } + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + // canvas.color = Color.random(); + // canvas.strokeRect(cx + this.x, cy + this.y, this.width, this.renderer.height); + for (const v of this.voiceDrawOrder!) { + const beatGlyphs = this.beatGlyphs.get(v)!; + const voice = this.renderer.bar.voices[v]; + using _ = ElementStyleHelper.voice(canvas, VoiceSubElement.Glyphs, voice, true); + + for (let i: number = 0, j: number = beatGlyphs.length; i < j; i++) { + beatGlyphs[i].paint(cx + this.x, cy + this.y, canvas); + } + } + } +} diff --git a/packages/alphatab/src/rendering/glyphs/MusicFontGlyph.ts b/packages/alphatab/src/rendering/glyphs/MusicFontGlyph.ts index 444664f5f..75b3e7909 100644 --- a/packages/alphatab/src/rendering/glyphs/MusicFontGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/MusicFontGlyph.ts @@ -7,7 +7,7 @@ import type { Color } from '@coderline/alphatab/model/Color'; * @internal */ export class MusicFontGlyph extends EffectGlyph { - protected glyphScale: number = 0; + public glyphScale: number = 0; public symbol: MusicFontSymbol; public center: boolean = false; public colorOverride?: Color; diff --git a/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts index 3d170c398..53735156c 100644 --- a/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts @@ -2,18 +2,15 @@ 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'; /** * @internal */ -export class NoteHeadGlyph extends MusicFontGlyph { - // TODO: SmuFL - public static readonly GraceScale: number = 0.75; - +export class NoteHeadGlyphBase extends MusicFontGlyph { public centerOnStem = false; - - public constructor(x: number, y: number, duration: Duration, isGrace: boolean) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, NoteHeadGlyph.getSymbol(duration)); + public constructor(x: number, y: number, isGrace: boolean, symbol: MusicFontSymbol) { + super(x, y, isGrace ? EngravingSettings.GraceScale : 1, symbol); } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -22,6 +19,15 @@ export class NoteHeadGlyph extends MusicFontGlyph { } super.paint(cx, cy, canvas); } +} + +/** + * @internal + */ +export class NoteHeadGlyph extends NoteHeadGlyphBase { + public constructor(x: number, y: number, duration: Duration, isGrace: boolean) { + super(x, y, isGrace, NoteHeadGlyph.getSymbol(duration)); + } public static getSymbol(duration: Duration): MusicFontSymbol { switch (duration) { diff --git a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts index dbede743e..9c4793b02 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts @@ -1,29 +1,29 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; -import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; -import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; -import { NumberedNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedNoteHeadGlyph'; -import type { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; -import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { AccidentalGroupGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGroupGlyph'; import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import { AccidentalGroupGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGroupGlyph'; import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; -import { NumberedDashGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedDashGlyph'; -import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; +import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; +import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { NumberedDashGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedDashGlyph'; +import { NumberedNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedNoteHeadGlyph'; +import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import type { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; +import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper'; +import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; +import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; +import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; /** * @internal @@ -85,8 +85,8 @@ export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { sr.getLineY(0), accidentalToSet, note.beat.graceType !== GraceType.None - ? NoteHeadGlyph.GraceScale * NoteHeadGlyph.GraceScale - : NoteHeadGlyph.GraceScale + ? EngravingSettings.GraceScale * EngravingSettings.GraceScale + : EngravingSettings.GraceScale ); g.colorOverride = color; g.renderer = this.renderer; @@ -163,6 +163,10 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { return this._internalGetNoteY(requestedPosition); } + public override getRestY(requestedPosition: NoteYPosition): number { + return this._internalGetNoteY(requestedPosition); + } + private _internalGetNoteY(requestedPosition: NoteYPosition): number { let g: Glyph | null = null; if (this.noteHeads) { @@ -249,7 +253,7 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { dots *= -1; } this.octaveDots = dots; - sr.registerOctave(dots); + sr.registerOctave(this.container.beat, dots); const stepList = ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks) diff --git a/packages/alphatab/src/rendering/glyphs/PercussionNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/PercussionNoteHeadGlyph.ts index f62d0a51c..e0c8f82f3 100644 --- a/packages/alphatab/src/rendering/glyphs/PercussionNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/PercussionNoteHeadGlyph.ts @@ -1,14 +1,16 @@ -import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import type { Duration } from '@coderline/alphatab/model/Duration'; -import { TechniqueSymbolPlacement, type InstrumentArticulation } from '@coderline/alphatab/model/InstrumentArticulation'; +import { + type InstrumentArticulation, + TechniqueSymbolPlacement +} from '@coderline/alphatab/model/InstrumentArticulation'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ -export class PercussionNoteHeadGlyph extends MusicFontGlyph { +export class PercussionNoteHeadGlyph extends NoteHeadGlyphBase { private _isGrace: boolean; private _articulation: InstrumentArticulation; @@ -19,7 +21,7 @@ export class PercussionNoteHeadGlyph extends MusicFontGlyph { duration: Duration, isGrace: boolean ) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, articulation.getSymbol(duration)); + super(x, y, isGrace, articulation.getSymbol(duration)); this._isGrace = isGrace; this._articulation = articulation; } @@ -31,13 +33,21 @@ export class PercussionNoteHeadGlyph extends MusicFontGlyph { } const offset: number = this._isGrace ? 1 : 0; - CanvasHelper.fillMusicFontSymbolSafe(canvas,cx + this.x, cy + this.y + offset, this.glyphScale, this.symbol, false); + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + cx + this.x, + cy + this.y + offset, + this.glyphScale, + this.symbol, + false + ); if ( this._articulation.techniqueSymbol !== MusicFontSymbol.None && this._articulation.techniqueSymbolPlacement === TechniqueSymbolPlacement.Inside ) { - CanvasHelper.fillMusicFontSymbolSafe(canvas, + CanvasHelper.fillMusicFontSymbolSafe( + canvas, cx + this.x, cy + this.y + offset, this.glyphScale, diff --git a/packages/alphatab/src/rendering/glyphs/PickStrokeGlyph.ts b/packages/alphatab/src/rendering/glyphs/PickStrokeGlyph.ts index f24c56026..e28295e00 100644 --- a/packages/alphatab/src/rendering/glyphs/PickStrokeGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/PickStrokeGlyph.ts @@ -1,14 +1,14 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ export class PickStrokeGlyph extends MusicFontGlyph { public constructor(x: number, y: number, pickStroke: PickStroke) { - super(x, y, NoteHeadGlyph.GraceScale, PickStrokeGlyph._getSymbol(pickStroke)); + super(x, y, EngravingSettings.GraceScale, PickStrokeGlyph._getSymbol(pickStroke)); this.center = true; } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts index d850f977a..41c24b688 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts @@ -1,41 +1,41 @@ +import { Logger } from '@coderline/alphatab/Logger'; import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; +import { TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; +import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; +import { PickStroke } from '@coderline/alphatab/model/PickStroke'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; +import { type NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { AccentuationGlyph } from '@coderline/alphatab/rendering/glyphs/AccentuationGlyph'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { ArticStaccatoAboveGlyph } from '@coderline/alphatab/rendering/glyphs/ArticStaccatoAboveGlyph'; import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; +import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import { DeadNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/DeadNoteHeadGlyph'; import { DiamondNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/DiamondNoteHeadGlyph'; +import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { GhostNoteContainerGlyph } from '@coderline/alphatab/rendering/glyphs/GhostNoteContainerGlyph'; import { GlyphGroup } from '@coderline/alphatab/rendering/glyphs/GlyphGroup'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { GuitarGolpeGlyph } from '@coderline/alphatab/rendering/glyphs/GuitarGolpeGlyph'; +import { NoteHeadGlyph, type NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { PercussionNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/PercussionNoteHeadGlyph'; +import { PickStrokeGlyph } from '@coderline/alphatab/rendering/glyphs/PickStrokeGlyph'; +import { PictEdgeOfCymbalGlyph } from '@coderline/alphatab/rendering/glyphs/PictEdgeOfCymbalGlyph'; import { ScoreNoteChordGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyph'; import { ScoreRestGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreRestGlyph'; import { ScoreWhammyBarGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreWhammyBarGlyph'; +import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; +import { StringNumberContainerGlyph } from '@coderline/alphatab/rendering/glyphs/StringNumberContainerGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; -import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; -import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; -import { PercussionNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/PercussionNoteHeadGlyph'; -import { Logger } from '@coderline/alphatab/Logger'; -import { ArticStaccatoAboveGlyph } from '@coderline/alphatab/rendering/glyphs/ArticStaccatoAboveGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { PictEdgeOfCymbalGlyph } from '@coderline/alphatab/rendering/glyphs/PictEdgeOfCymbalGlyph'; -import { PickStrokeGlyph } from '@coderline/alphatab/rendering/glyphs/PickStrokeGlyph'; -import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { GuitarGolpeGlyph } from '@coderline/alphatab/rendering/glyphs/GuitarGolpeGlyph'; +import type { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; import { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { StringNumberContainerGlyph } from '@coderline/alphatab/rendering/glyphs/StringNumberContainerGlyph'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; +import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation'; -import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; -import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; /** * @internal @@ -102,11 +102,30 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { return this.noteHeads ? this.noteHeads.getNoteY(note, requestedPosition) : 0; } + public override getRestY(requestedPosition: NoteYPosition): number { + const g = this.restGlyph; + if (g) { + switch (requestedPosition) { + case NoteYPosition.TopWithStem: + case NoteYPosition.Top: + return g.getBoundingBoxTop(); + case NoteYPosition.Center: + case NoteYPosition.StemUp: + case NoteYPosition.StemDown: + return g.getBoundingBoxTop() + g.height / 2; + case NoteYPosition.Bottom: + case NoteYPosition.BottomWithStem: + return g.getBoundingBoxBottom(); + } + } + return 0; + } + public applyRestCollisionOffset() { if (!this.restGlyph) { return; } - if (this.renderer.bar.isMultiVoice && Number.isNaN(this._collisionOffset)) { + if (Number.isNaN(this._collisionOffset)) { this._collisionOffset = this.renderer.collisionHelper.applyRestCollisionOffset( this.container.beat, this.restGlyph.y, @@ -131,104 +150,31 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { } } - public override doLayout(): void { - // create glyphs - const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - if (!this.container.beat.isEmpty) { - if (!this.container.beat.isRest) { - // - // Note heads - // - const noteHeads = new ScoreNoteChordGlyph(); - this.noteHeads = noteHeads; - noteHeads.beat = this.container.beat; - const ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(false); - ghost.renderer = this.renderer; - - for (const note of this.container.beat.notes) { - if (note.isVisible && (!note.beat.slashed || note.index === 0)) { - this._createNoteGlyph(note); - ghost.addParenthesis(note); - } - } - - this.addNormal(noteHeads); - if (!ghost.isEmpty) { - this.addEffect(ghost); - } - - // - // Whammy Bar - if (this.container.beat.hasWhammyBar) { - const whammy: ScoreWhammyBarGlyph = new ScoreWhammyBarGlyph(this.container.beat); - this._whammy = whammy; - whammy.renderer = this.renderer; - whammy.doLayout(); - this.container.addTie(whammy); - } - // - // Note dots - // - if (this.container.beat.dots > 0) { - for (let i: number = 0; i < this.container.beat.dots; i++) { - const group: GlyphGroup = new GlyphGroup(0, 0); - group.renderer = this.renderer; - for (const note of this.container.beat.notes) { - const g = this._createBeatDot(sr.getNoteSteps(note), group); - g.colorOverride = ElementStyleHelper.noteColor( - sr.resources, - NoteSubElement.StandardNotationEffects, - note - ); - } - this.addEffect(group); - } - } - } else { - let steps = Math.ceil((this.renderer.bar.staff.standardNotationLineCount - 1) / 2) * 2; - - // this positioning is quite strange, for most staff line counts - // the whole/rest are aligned as half below the whole rest. - // but for staff line count 1 and 3 they are aligned centered on the same line. - if ( - this.container.beat.duration === Duration.Whole && - this.renderer.bar.staff.standardNotationLineCount !== 1 && - this.renderer.bar.staff.standardNotationLineCount !== 3 - ) { - steps -= 2; - } - - const restGlyph = new ScoreRestGlyph(0, sr.getScoreY(steps), this.container.beat.duration); - this.restGlyph = restGlyph; - restGlyph.beat = this.container.beat; - this.addNormal(restGlyph); - - if (this.renderer.bar.isMultiVoice) { - if (this.container.beat.voice.index === 0) { - const restSizes = BeamingHelper.computeLineHeightsForRest(this.container.beat.duration); - const restTop = restGlyph.y - sr.getScoreHeight(restSizes[0]); - const restBottom = restGlyph.y + sr.getScoreHeight(restSizes[1]); - this.renderer.collisionHelper.reserveBeatSlot(this.container.beat, restTop, restBottom); - } else { - this.renderer.collisionHelper.registerRest(this.container.beat); - } - } - - // - // Note dots - // - if (this.container.beat.dots > 0) { - for (let i: number = 0; i < this.container.beat.dots; i++) { - const group: GlyphGroup = new GlyphGroup(0, 0); - group.renderer = this.renderer; - this._createBeatDot(steps, group); - this.addEffect(group); - } - } + public doMultiVoiceLayout(): void { + this.applyRestCollisionOffset(); + this.noteHeads?.doMultiVoiceLayout(); + this._whammy?.doMultiVoiceLayout(); + + let w: number = 0; + if (this.glyphs) { + for (const g of this.glyphs) { + g.x = w; + w += g.width; } } + this.width = w; + this.computedWidth = w; + + this._updatePositions(); + } + + public override doLayout(): void { + this._createGlyphs(); super.doLayout(); - this.applyRestCollisionOffset(); + this._updatePositions(); + } + + private _updatePositions() { if (this.container.beat.isEmpty) { this.onTimeX = this.width / 2; this.middleX = this.onTimeX; @@ -240,10 +186,115 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { } else if (this.noteHeads) { this.onTimeX = this.noteHeads!.x + this.noteHeads!.onTimeX; this.middleX = this.noteHeads!.x + this.noteHeads!.width / 2; - const direction = this.renderer.getBeatDirection(this.container.beat); - this.stemX = - this.noteHeads!.x + - (direction === BeamDirection.Up ? this.noteHeads!.upLineX : this.noteHeads!.downLineX); + this.stemX = this.noteHeads!.x + this.noteHeads!.stemX; + } + } + + private _createGlyphs() { + if (this.container.beat.isEmpty) { + return; + } + + if (!this.container.beat.isRest) { + this._createNoteGlyphs(); + } else { + this._createRestGlyphs(); + } + } + + private _createNoteGlyphs() { + const sr = this.renderer as ScoreBarRenderer; + + // + // Note heads + const noteHeads = new ScoreNoteChordGlyph(); + this.noteHeads = noteHeads; + noteHeads.beat = this.container.beat; + const ghost = new GhostNoteContainerGlyph(false); + ghost.renderer = this.renderer; + + for (const note of this.container.beat.notes) { + if (note.isVisible && (!note.beat.slashed || note.index === 0)) { + this._createNoteGlyph(note); + ghost.addParenthesis(note); + } + } + + this.addNormal(noteHeads); + if (!ghost.isEmpty) { + this.addEffect(ghost); + } + + // + // Whammy Bar + if (this.container.beat.hasWhammyBar) { + const whammy: ScoreWhammyBarGlyph = new ScoreWhammyBarGlyph(this.container as ScoreBeatContainerGlyph); + this._whammy = whammy; + whammy.renderer = this.renderer; + whammy.doLayout(); + this.container.addTie(whammy); + } + // + // Note dots + if (this.container.beat.dots > 0) { + for (let i: number = 0; i < this.container.beat.dots; i++) { + const group: GlyphGroup = new GlyphGroup(0, 0); + group.renderer = this.renderer; + for (const note of this.container.beat.notes) { + const g = this._createBeatDot(sr.getNoteSteps(note), group); + g.colorOverride = ElementStyleHelper.noteColor( + sr.resources, + NoteSubElement.StandardNotationEffects, + note + ); + } + this.addEffect(group); + } + } + } + + private _createRestGlyphs() { + const sr = this.renderer as ScoreBarRenderer; + + let steps = Math.ceil((this.renderer.bar.staff.standardNotationLineCount - 1) / 2) * 2; + + // this positioning is quite strange, for most staff line counts + // the whole/rest are aligned as half below the whole rest. + // but for staff line count 1 and 3 they are aligned centered on the same line. + if ( + this.container.beat.duration === Duration.Whole && + this.renderer.bar.staff.standardNotationLineCount !== 1 && + this.renderer.bar.staff.standardNotationLineCount !== 3 + ) { + steps -= 2; + } + + const restGlyph = new ScoreRestGlyph(0, sr.getScoreY(steps), this.container.beat.duration); + this.restGlyph = restGlyph; + restGlyph.beat = this.container.beat; + this.addNormal(restGlyph); + + if (this.renderer.bar.isMultiVoice) { + if (this.container.beat.voice.index === 0) { + const restSizes = BeamingHelper.computeLineHeightsForRest(this.container.beat.duration); + const restTop = restGlyph.y - sr.getScoreHeight(restSizes[0]); + const restBottom = restGlyph.y + sr.getScoreHeight(restSizes[1]); + this.renderer.collisionHelper.reserveBeatSlot(this.container.beat, restTop, restBottom); + } else { + this.renderer.collisionHelper.registerRest(this.container.beat); + } + } + + // + // Note dots + // + if (this.container.beat.dots > 0) { + for (let i: number = 0; i < this.container.beat.dots; i++) { + const group: GlyphGroup = new GlyphGroup(0, 0); + group.renderer = this.renderer; + this._createBeatDot(steps, group); + this.addEffect(group); + } } } @@ -254,7 +305,7 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { return g; } - private _createNoteHeadGlyph(n: Note): MusicFontGlyph { + private _createNoteHeadGlyph(n: Note): NoteHeadGlyphBase { const isGrace: boolean = this.container.beat.graceType !== GraceType.None; const style = n.style; diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts index 4cca91f71..79eef5d88 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts @@ -1,23 +1,23 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { BendType } from '@coderline/alphatab/model/BendType'; import { BrushType } from '@coderline/alphatab/model/BrushType'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; +import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { WhammyType } from '@coderline/alphatab/model/WhammyType'; import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; import { AccidentalGroupGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGroupGlyph'; import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; import { BendNoteHeadGroupGlyph } from '@coderline/alphatab/rendering/glyphs/BendNoteHeadGroupGlyph'; +import { FingeringGroupGlyph } from '@coderline/alphatab/rendering/glyphs/FingeringGroupGlyph'; import { GhostNoteContainerGlyph } from '@coderline/alphatab/rendering/glyphs/GhostNoteContainerGlyph'; import { ScoreBrushGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBrushGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { FingeringGroupGlyph } from '@coderline/alphatab/rendering/glyphs/FingeringGroupGlyph'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { SlideInType } from '@coderline/alphatab/model/SlideInType'; /** * @internal @@ -25,7 +25,7 @@ import { SlideInType } from '@coderline/alphatab/model/SlideInType'; export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { private _prebends: BendNoteHeadGroupGlyph | null = null; public get prebendNoteHeadOffset(): number { - return this._prebends ? this._prebends.x + this._prebends.noteHeadOffset : 0; + return this._prebends ? this._prebends.x + this._prebends.onTimeX : 0; } protected override get effectElement() { @@ -34,151 +34,163 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { public accidentals: AccidentalGroupGlyph | null = null; + public doMultiVoiceLayout() { + this._prebends?.doMultiVoiceLayout(); + } + public override doLayout(): void { if (!this.container.beat.isRest) { - const accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); - accidentals.renderer = this.renderer; - - const fingering: FingeringGroupGlyph = new FingeringGroupGlyph(); - fingering.renderer = this.renderer; - - const ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(true); - ghost.renderer = this.renderer; - - const preBends = new BendNoteHeadGroupGlyph(this.container.beat, true); - this._prebends = preBends; - preBends.renderer = this.renderer; - - let hasSimpleSlideIn = false; + this._createGlyphs(); + } + super.doLayout(); + } - for (const note of this.container.beat.notes) { - const color = ElementStyleHelper.noteColor( - this.renderer.resources, - NoteSubElement.StandardNotationEffects, - note - ); - if (note.isVisible) { - if (note.hasBend) { - switch (note.bendType) { - case BendType.PrebendBend: - case BendType.Prebend: - case BendType.PrebendRelease: - preBends.addGlyph( - note.displayValue - ((note.bendPoints![0].value / 2) | 0), - false, - color - ); - break; - } - } else if (note.beat.hasWhammyBar) { - switch (note.beat.whammyBarType) { - case WhammyType.PrediveDive: - case WhammyType.Predive: - this._prebends.addGlyph( - note.displayValue - ((note.beat.whammyBarPoints![0].value / 2) | 0), - false, - color - ); - break; - } + private _createGlyphs() { + const accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); + accidentals.renderer = this.renderer; + + const fingering: FingeringGroupGlyph = new FingeringGroupGlyph(); + fingering.renderer = this.renderer; + + const ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(true); + ghost.renderer = this.renderer; + + let preBends: BendNoteHeadGroupGlyph | null = null; + + + let hasSimpleSlideIn = false; + + for (const note of this.container.beat.notes) { + const color = ElementStyleHelper.noteColor( + this.renderer.resources, + NoteSubElement.StandardNotationEffects, + note + ); + if (note.isVisible) { + if (note.hasBend) { + switch (note.bendType) { + case BendType.PrebendBend: + case BendType.Prebend: + case BendType.PrebendRelease: + if (!preBends) { + preBends = new BendNoteHeadGroupGlyph('prebend', this.container.beat, true); + preBends.renderer = this.renderer; + } + preBends.addGlyph(note.displayValue - ((note.bendPoints![0].value / 2) | 0), false, color); + break; } - this._createAccidentalGlyph(note, accidentals); - ghost.addParenthesis(note); - fingering.addFingers(note); - - switch (note.slideInType) { - case SlideInType.IntoFromBelow: - case SlideInType.IntoFromAbove: - hasSimpleSlideIn = true; + } else if (note.beat.hasWhammyBar) { + switch (note.beat.whammyBarType) { + case WhammyType.PrediveDive: + case WhammyType.Predive: + if (!preBends) { + preBends = new BendNoteHeadGroupGlyph('prebend', this.container.beat, true); + preBends.renderer = this.renderer; + } + preBends.addGlyph( + note.displayValue - ((note.beat.whammyBarPoints![0].value / 2) | 0), + false, + color + ); break; } } - } - - if (hasSimpleSlideIn) { - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.simpleSlideWidth * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); - } - - if (!preBends.isEmpty) { - this.addEffect(preBends); - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); - } - if (this.container.beat.brushType !== BrushType.None) { - this.addEffect(new ScoreBrushGlyph(this.container.beat)); - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); - } - if (!fingering.isEmpty) { - if (!this.isEmpty) { - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); + this._createAccidentalGlyph(note, accidentals); + ghost.addParenthesis(note); + fingering.addFingers(note); + + switch (note.slideInType) { + case SlideInType.IntoFromBelow: + case SlideInType.IntoFromAbove: + hasSimpleSlideIn = true; + break; } + } + } + + if (hasSimpleSlideIn) { + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.simpleSlideWidth * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); + } - this.addEffect(fingering); + this._prebends = preBends; + if (preBends) { + this.addEffect(preBends); + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.preNoteEffectPadding * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); + } + if (this.container.beat.brushType !== BrushType.None) { + this.addEffect(new ScoreBrushGlyph(this.container.beat)); + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.preNoteEffectPadding * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); + } + if (!fingering.isEmpty) { + if (!this.isEmpty) { this.addNormal( new SpacingGlyph( 0, 0, this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) ) ); } - if (!ghost.isEmpty) { - this.addEffect(ghost); - } - if (!accidentals.isEmpty) { - this.accidentals = accidentals; - if (!this.isEmpty) { - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); - } + this.addEffect(fingering); + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.preNoteEffectPadding * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); + } - this.addNormal(accidentals); + if (!ghost.isEmpty) { + this.addEffect(ghost); + } + if (!accidentals.isEmpty) { + this.accidentals = accidentals; + if (!this.isEmpty) { this.addNormal( new SpacingGlyph( 0, 0, this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) ) ); } + + this.addNormal(accidentals); + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.preNoteEffectPadding * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); } - super.doLayout(); } private _createAccidentalGlyph(n: Note, accidentals: AccidentalGroupGlyph): void { @@ -187,7 +199,7 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { let noteSteps: number = sr.getNoteSteps(n); const isGrace: boolean = this.container.beat.graceType !== GraceType.None; const color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationAccidentals, n); - const graceScale = isGrace ? NoteHeadGlyph.GraceScale : 1; + const graceScale = isGrace ? EngravingSettings.GraceScale : 1; if (accidental !== AccidentalType.None) { const g = new AccidentalGlyph(0, sr.getScoreY(noteSteps), accidental, graceScale); g.colorOverride = color; diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts index 2fd5430ee..55a11fa14 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts @@ -1,3 +1,4 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import type { Beat } from '@coderline/alphatab/model/Beat'; import type { BendPoint } from '@coderline/alphatab/model/BendPoint'; import { BendStyle } from '@coderline/alphatab/model/BendStyle'; @@ -9,11 +10,10 @@ import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { BendNoteHeadGroupGlyph } from '@coderline/alphatab/rendering/glyphs/BendNoteHeadGroupGlyph'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import type { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreHelperNotesBaseGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreHelperNotesBaseGlyph'; import { type ITieGlyph, TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; +import type { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; @@ -25,12 +25,14 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly private _notes: Note[] = []; private _endNoteGlyph: BendNoteHeadGroupGlyph | null = null; private _middleNoteGlyph: BendNoteHeadGroupGlyph | null = null; + private _container: ScoreBeatContainerGlyph; public readonly checkForOverflow = false; // handled separately in ScoreBeatContainerGlyph - public constructor(beat: Beat) { + public constructor(container: ScoreBeatContainerGlyph) { super(0, 0); - this._beat = beat; + this._beat = container.beat; + this._container = container; } public override getBoundingBoxTop(): number { @@ -41,6 +43,11 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly return super.getBoundingBoxBottom() + this._calculateMaxSlurHeight(BeamDirection.Down); } + public doMultiVoiceLayout(): void { + this._middleNoteGlyph?.doMultiVoiceLayout(); + this._endNoteGlyph?.doMultiVoiceLayout(); + } + private _calculateMaxSlurHeight(expectedDirection: BeamDirection) { const direction = this.getTieDirection(this._beat, this.renderer as ScoreBarRenderer); if (direction !== expectedDirection) { @@ -57,7 +64,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly } // no helper notes created in addbends for these: - switch(note.bendType){ + switch (note.bendType) { case BendType.Custom: case BendType.Prebend: case BendType.Hold: @@ -133,7 +140,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly { let endGlyphs = this._endNoteGlyph; if (!endGlyphs) { - endGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); + endGlyphs = new BendNoteHeadGroupGlyph('postbend', note.beat, false); endGlyphs.renderer = this.renderer; this._endNoteGlyph = endGlyphs; this.addGlyph(endGlyphs); @@ -151,7 +158,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly if (!note.isTieOrigin) { let endGlyphs = this._endNoteGlyph; if (!endGlyphs) { - endGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); + endGlyphs = new BendNoteHeadGroupGlyph('postbend', note.beat, false); endGlyphs.renderer = this.renderer; this._endNoteGlyph = endGlyphs; this.addGlyph(endGlyphs); @@ -169,7 +176,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly { let middleGlyphs = this._middleNoteGlyph; if (!middleGlyphs) { - middleGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); + middleGlyphs = new BendNoteHeadGroupGlyph('middlebend', note.beat, false); this._middleNoteGlyph = middleGlyphs; middleGlyphs.renderer = this.renderer; this.addGlyph(middleGlyphs); @@ -182,7 +189,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly ); let endGlyphs = this._endNoteGlyph; if (!endGlyphs) { - endGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); + endGlyphs = new BendNoteHeadGroupGlyph('postbend', note.beat, false); endGlyphs.renderer = this.renderer; this._endNoteGlyph = endGlyphs; this.addGlyph(endGlyphs); @@ -208,23 +215,25 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.MiddleNotes); let endBeatX: number = cx + startNoteRenderer.x; if (this._beat.isLastOfVoice) { - endBeatX += startNoteRenderer.postBeatGlyphsStart; + endBeatX += startNoteRenderer.getBeatX(this._beat!, BeatXPosition.EndBeat); } else { endBeatX += startNoteRenderer.getBeatX(this._beat.nextBeat!, BeatXPosition.PreNotes); } + endBeatX -= this.renderer.smuflMetrics.postNoteEffectPadding; if (this._endNoteGlyph) { - endBeatX -= this._endNoteGlyph.upLineX; + const postBeatSize = this._endNoteGlyph.width - this._endNoteGlyph.onTimeX; + endBeatX -= postBeatSize; } const middleX: number = (startX + endBeatX) / 2; if (this._middleNoteGlyph) { - this._middleNoteGlyph.x = middleX - this._middleNoteGlyph.noteHeadOffset; + this._middleNoteGlyph.x = middleX - this._middleNoteGlyph.onTimeX; this._middleNoteGlyph.y = cy + startNoteRenderer.y; this._middleNoteGlyph.paint(0, 0, canvas); } if (this._endNoteGlyph) { - this._endNoteGlyph.x = endBeatX - this._endNoteGlyph.noteHeadOffset; + this._endNoteGlyph.x = endBeatX - this._endNoteGlyph.onTimeX; this._endNoteGlyph.y = cy + startNoteRenderer.y; this._endNoteGlyph.paint(0, 0, canvas); } @@ -246,7 +255,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly direction = BeamDirection.Down; } let startY: number = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(note, NoteYPosition.Top); - let heightOffset: number = noteHeadHeight * NoteHeadGlyph.GraceScale * 0.5; + let heightOffset: number = noteHeadHeight * EngravingSettings.GraceScale * 0.5; if (direction === BeamDirection.Down) { startY += noteHeadHeight; } @@ -290,7 +299,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly endX, endY, direction === BeamDirection.Down, - 1, slurText ); } @@ -321,7 +329,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly endX, endY, direction === BeamDirection.Down, - 1, slurText ); } @@ -332,8 +339,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly case BendType.PrebendRelease: let preX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); - preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) - .prebendNoteHeadOffset; + preX += this._container.prebendNoteHeadOffset; const preY: number = cy + startNoteRenderer.y + @@ -344,7 +350,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly ) ) + heightOffset; - this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down, 1); + this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down); break; } } else { @@ -364,7 +370,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly endBeatX, endY, direction === BeamDirection.Down, - 1, slurText ); break; @@ -378,7 +383,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly middleX, middleY, direction === BeamDirection.Down, - 1, slurText ); endValue = this._getBendNoteValue(note, note.bendPoints![note.bendPoints!.length - 1]); @@ -390,7 +394,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly endBeatX, endY, direction === BeamDirection.Down, - 1, slurText ); break; @@ -405,7 +408,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly endBeatX, endY, direction === BeamDirection.Down, - 1, slurText ); } @@ -415,8 +417,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly case BendType.PrebendRelease: let preX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); - preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) - .prebendNoteHeadOffset; + preX += this._container.prebendNoteHeadOffset; const preY: number = cy + startNoteRenderer.y + @@ -427,7 +428,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly ) ) + heightOffset; - this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down, 1); + this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down); if (this.glyphs) { endValue = this._getBendNoteValue(note, note.bendPoints![note.bendPoints!.length - 1]); endY = (this.glyphs[0] as BendNoteHeadGroupGlyph).getNoteValueY(endValue) + heightOffset; @@ -438,7 +439,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly endBeatX, endY, direction === BeamDirection.Down, - 1, slurText ); } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts index 297a0165e..1baeda37d 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts @@ -16,10 +16,9 @@ export class ScoreHelperNotesBaseGlyph extends GlyphGroup { x2: number, y2: number, down: boolean, - scale: number, slurText?: string ): void { - TieGlyph.drawBendSlur(canvas, x1, y1, x2, y2, down, scale, this.renderer.smuflMetrics.tieHeight, slurText); + TieGlyph.drawBendSlur(canvas, x1, y1, x2, y2, down, this.renderer.smuflMetrics.tieHeight, slurText); } public override doLayout(): void { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts index a542f212d..e086455aa 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts @@ -15,7 +15,7 @@ export class ScoreLegatoGlyph extends TieGlyph { protected startBeatRenderer: BarRendererBase | null = null; protected endBeatRenderer: BarRendererBase | null = null; - public constructor(slurEffectId: string, startBeat: Beat, endBeat: Beat, forEnd:boolean) { + public constructor(slurEffectId: string, startBeat: Beat, endBeat: Beat, forEnd: boolean) { super(slurEffectId, forEnd); this.startBeat = startBeat; this.endBeat = endBeat; @@ -72,15 +72,9 @@ export class ScoreLegatoGlyph extends TieGlyph { if (this.startBeat!.isRest) { switch (this.tieDirection) { case BeamDirection.Up: - return ( - startBeatRenderer.y + - startBeatRenderer.getBeatContainer(this.startBeat)!.onNotes.getBoundingBoxTop() - ); + return startBeatRenderer.y + startBeatRenderer.getRestY(this.startBeat, NoteYPosition.Top); default: - return ( - startBeatRenderer.y + - startBeatRenderer.getBeatContainer(this.startBeat)!.onNotes.getBoundingBoxBottom() - ); + return startBeatRenderer.y + startBeatRenderer.getRestY(this.startBeat, NoteYPosition.Bottom); } } @@ -119,14 +113,9 @@ export class ScoreLegatoGlyph extends TieGlyph { if (this.endBeat.isRest) { switch (this.tieDirection) { case BeamDirection.Up: - return ( - endBeatRenderer.y + endBeatRenderer.getBeatContainer(this.endBeat)!.onNotes.getBoundingBoxTop() - ); + return endBeatRenderer.y + endBeatRenderer.getRestY(this.endBeat, NoteYPosition.Top); default: - return ( - endBeatRenderer.y + - endBeatRenderer.getBeatContainer(this.endBeat)!.onNotes.getBoundingBoxBottom() - ); + return endBeatRenderer.y + endBeatRenderer.getRestY(this.endBeat, NoteYPosition.Bottom); } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts index c937b9805..86d91fa1f 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts @@ -1,3 +1,4 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; @@ -7,8 +8,11 @@ import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarR 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 { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { ScoreNoteChordGlyphBase } from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyphBase'; +import type { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { + ScoreChordNoteHeadInfo, + ScoreNoteChordGlyphBase +} from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyphBase'; import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; @@ -34,8 +38,33 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { return this.renderer.getBeatDirection(this.beat); } + public override get hasFlag(): boolean { + return (this.renderer as ScoreBarRenderer).hasFlag(this.beat); + } + + public override get hasStem(): boolean { + return (this.renderer as ScoreBarRenderer).hasStem(this.beat); + } + public override get scale(): number { - return this.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + return this.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; + } + + protected override getScoreChordNoteHeadInfo(): ScoreChordNoteHeadInfo { + // never share grace beats + if (this.beat.graceType !== GraceType.None) { + return new ScoreChordNoteHeadInfo(this.direction); + } + + // TODO: do we need to share this spacing across all staves&tracks? + const staff = this.beat.voice.bar.staff; + const key = `score.noteheads.${staff.track.index}.${staff.index}.${this.beat.absoluteDisplayStart}`; + let existing = this.renderer.staff!.getSharedLayoutData(key, undefined); + if (!existing) { + existing = new ScoreChordNoteHeadInfo(this.direction); + this.renderer.staff!.setSharedLayoutData(key, existing); + } + return existing; } public getNoteX(note: Note, requestedPosition: NoteXPosition): number { @@ -69,7 +98,7 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { private _internalGetNoteY(n: MusicFontGlyph, requestedPosition: NoteYPosition): number { let pos = this.y + n.y; - const scale = this.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const scale = this.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; switch (requestedPosition) { case NoteYPosition.TopWithStem: // stem start @@ -121,19 +150,19 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { return pos; } - public addMainNoteGlyph(noteGlyph: MusicFontGlyph, note: Note, noteLine: number): void { + public addMainNoteGlyph(noteGlyph: NoteHeadGlyphBase, note: Note, noteLine: number): void { super.add(noteGlyph, noteLine); this._noteGlyphLookup.set(note.id, noteGlyph); this._notes.push(note); } - public addEffectNoteGlyph(noteGlyph: MusicFontGlyph, noteLine: number): void { + public addEffectNoteGlyph(noteGlyph: NoteHeadGlyphBase, noteLine: number): void { super.add(noteGlyph, noteLine); } public override doLayout(): void { super.doLayout(); - const scoreRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const scoreRenderer = this.renderer as ScoreBarRenderer; if (this.beat.deadSlapped) { this._deadSlapped = new DeadSlappedBeatGlyph(); @@ -218,7 +247,7 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { tremoloY = (topY + bottomY) / 2; } - let tremoloX: number = direction === BeamDirection.Up ? this.upLineX : this.downLineX; + let tremoloX: number = this.stemX; const speed: Duration = this.beat.tremoloSpeed!; if (this.beat.duration < Duration.Half) { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts index a5a27cf94..6022914cc 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts @@ -1,23 +1,328 @@ +import type { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { BarSubElement } from '@coderline/alphatab/model/Bar'; +import { MusicFontSymbolLookup } from '@coderline/alphatab/model/MusicFontSymbol'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { ScoreNoteGlyphInfo } from '@coderline/alphatab/rendering/glyphs/ScoreNoteGlyphInfo'; +import type { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; + +// TODO[perf]: the overall note head alignment creates quite a lot of objects which the GC +// will have to cleanup again. we should be optimize this (e.g. via object pooling?, checking for multi-voice and avoid some objects) + +/** + * @internal + * @record + */ +export interface ScoreChordNoteHeadGroupSide { + /** + * A lookup for the notes located at particular steps. + * If we have more than 2 filled voices at the same spot, we might have the additional voices + * placed where the secondary voice already is. + */ + notes: Map; + /** + * The width of this individual side. + */ + width: number; + + /** + * The smallest X-coordinate of all glyphs. Used later to calculate + * the overall shift needed to place notes within the bounds. + */ + minX: number; +} + +/** + * @internal + */ +enum NoteHeadIntersectionKind { + NoIntersection = 0, + EndsTouchingOuter = 1, + EndsTouchingInner = 2, + ExactMatch = 3, + FullIntersection = 4 +} + +/** + * @internal + * @record + */ +export interface ScoreChordNoteHeadGroup { + /** + * All notes on the "correct" side of the stem, + * that's left for upwards stems, and right for downward stems. + */ + correctNotes: ScoreChordNoteHeadGroupSide; + /** + * All displaced notes (the other side of the stem compared to {@link correctNotes}) + */ + displacedNotes?: ScoreChordNoteHeadGroupSide; + + /** + * The direction this group defines. + */ + direction: BeamDirection; + + minStep: number; + maxStep: number; + + /** + * The offset of the stem for this group. + * Offset is relative to the group. + */ + stemX: number; + + /** + * Smallest X-coordinate in this group. + * Offset is relative to the group. + */ + minX: number; + /** + * Largest X-coordinate in this group. + * Offset is relative to the group. + */ + maxX: number; + + /** + * The shift applied for the group to avoid overlaps. + */ + multiVoiceShiftX: number; + + hasFlag: boolean; + hasStem: boolean; +} + +/** + * @internal + */ +export class ScoreChordNoteHeadInfo { + /** + * The direction of the main voice. + */ + public mainVoiceDirection = BeamDirection.Up; + + /** + * All groups respective to their direction. + */ + public readonly groups = new Map(); + public minX = 0; + public maxX = 0; + + private _isFinished = false; + + public constructor(mainVoiceDirection: BeamDirection) { + this.mainVoiceDirection = mainVoiceDirection; + } + + public update() { + let minX = 0; + let maxX = 0; + for (const g of this.groups.values()) { + const gMinX = g.minX + g.multiVoiceShiftX; + const gMaxX = g.maxX + g.multiVoiceShiftX; + if (gMinX < minX) { + minX = gMinX; + } + if (maxX < gMaxX) { + maxX = gMaxX; + } + } + this.minX = minX; + this.maxX = maxX; + } + + finish(smufl:EngravingSettings) { + if (this._isFinished) { + return; + } + this._isFinished = true; + + for (const g of this.groups.values()) { + this._checkForGroupDisplacement(g, smufl); + } + this.update(); + } + + private _checkForGroupDisplacement(noteGroup: ScoreChordNoteHeadGroup, smufl:EngravingSettings) { + // no group displace if we're in the same direction + if (this.mainVoiceDirection === noteGroup.direction) { + return; + } + + const mainGroup = this.groups.get(this.mainVoiceDirection)!; + + // no intersection -> we can align the note heads directly. + const intersection = ScoreChordNoteHeadInfo._checkIntersection(mainGroup, noteGroup); + + const spacing = smufl.multiVoiceDisplacedNoteHeadSpacing; + + switch (intersection) { + case NoteHeadIntersectionKind.NoIntersection: + return; + case NoteHeadIntersectionKind.ExactMatch: + // handling note head + if (!ScoreChordNoteHeadInfo._canShareNoteHead(mainGroup, noteGroup)) { + // align stems back-to-back with additional spacing + if (mainGroup.direction === BeamDirection.Up) { + if(noteGroup.displacedNotes) { + noteGroup.multiVoiceShiftX = noteGroup.stemX + noteGroup.correctNotes.width + spacing; + } else { + noteGroup.multiVoiceShiftX = noteGroup.correctNotes.width + spacing; + } + } else { + mainGroup.multiVoiceShiftX = noteGroup.stemX + spacing; + } + } + break; + case NoteHeadIntersectionKind.EndsTouchingOuter: + if (mainGroup.direction === BeamDirection.Up) { + if (mainGroup.displacedNotes) { + noteGroup.multiVoiceShiftX = mainGroup.stemX; + } else { + const diff = mainGroup.stemX - noteGroup.stemX; + noteGroup.multiVoiceShiftX = diff; + } + } else { + if (mainGroup.displacedNotes) { + noteGroup.multiVoiceShiftX = -noteGroup.stemX; + } else { + const diff = noteGroup.stemX - mainGroup.stemX; + mainGroup.multiVoiceShiftX = diff; + } + } + + break; + case NoteHeadIntersectionKind.EndsTouchingInner: + if (mainGroup.direction === BeamDirection.Up) { + mainGroup.multiVoiceShiftX = mainGroup.stemX; + if (noteGroup.hasFlag) { + mainGroup.multiVoiceShiftX += spacing; + } + } else { + noteGroup.multiVoiceShiftX = noteGroup.stemX; + if (mainGroup.hasFlag) { + noteGroup.multiVoiceShiftX += spacing; + } + } + break; + case NoteHeadIntersectionKind.FullIntersection: + // align note head center to stem + if(!mainGroup.hasStem && !noteGroup.hasStem) { + // we can keep them aligned. + } + else if (mainGroup.direction === BeamDirection.Up) { + mainGroup.multiVoiceShiftX = mainGroup.stemX; + if (noteGroup.hasFlag) { + mainGroup.multiVoiceShiftX += spacing; + } else { + mainGroup.multiVoiceShiftX -= spacing; + } + } else { + noteGroup.multiVoiceShiftX = noteGroup.stemX; + if (mainGroup.hasFlag) { + noteGroup.multiVoiceShiftX += spacing; + } else { + noteGroup.multiVoiceShiftX -= spacing; + } + } + + break; + } + } + + private static _canShareNoteHead(mainGroup: ScoreChordNoteHeadGroup, thisGroup: ScoreChordNoteHeadGroup) { + // TODO: check actual note head + const mainGroupBottom = mainGroup.direction === BeamDirection.Up ? mainGroup.maxStep : mainGroup.minStep; + const thisGroupBottom = thisGroup.direction === BeamDirection.Up ? thisGroup.maxStep : thisGroup.minStep; + + const mainGroupBottomNoteHead = mainGroup.correctNotes.notes.get(mainGroupBottom)!; + if (mainGroupBottomNoteHead.length > 1) { + return false; + } + + const thisGroupBottomNoteHead = thisGroup.correctNotes.notes.get(thisGroupBottom)!; + if (thisGroupBottomNoteHead.length > 1) { + return false; + } + + return ScoreChordNoteHeadInfo._canShareNoteHeadGlyph( + mainGroupBottomNoteHead[0].glyph, + thisGroupBottomNoteHead[0].glyph + ); + } + + private static _canShareNoteHeadGlyph(mainGlyph: NoteHeadGlyphBase, thisGlyph: NoteHeadGlyphBase) { + return ( + mainGlyph.glyphScale === thisGlyph.glyphScale && + mainGlyph.centerOnStem === thisGlyph.centerOnStem && + MusicFontSymbolLookup.isBlackNoteHead(mainGlyph.symbol) && + MusicFontSymbolLookup.isBlackNoteHead(thisGlyph.symbol) + ); + } + + private static _checkIntersection( + mainGroup: ScoreChordNoteHeadGroup, + thisGroup: ScoreChordNoteHeadGroup + ): NoteHeadIntersectionKind { + let bottomGap = 0; + if (mainGroup.direction === BeamDirection.Up) { + const mainGroupBottom = mainGroup.maxStep; + const thisGroupBottom = thisGroup.minStep; + + bottomGap = thisGroupBottom - mainGroupBottom; + } else { + const mainGroupBottom = mainGroup.minStep; + const thisGroupBottom = thisGroup.maxStep; + bottomGap = mainGroupBottom - thisGroupBottom; + } + + if (bottomGap === 0) { + return NoteHeadIntersectionKind.ExactMatch; + } + if (bottomGap === 1) { + return NoteHeadIntersectionKind.EndsTouchingOuter; + } + if (bottomGap === -1) { + return NoteHeadIntersectionKind.EndsTouchingInner; + } + if (bottomGap < 0) { + return NoteHeadIntersectionKind.FullIntersection; + } + return NoteHeadIntersectionKind.NoIntersection; + } +} + +/** + * @internal + * @record + */ +interface ScoreNoteGlyphInfo { + glyph: NoteHeadGlyphBase; + steps: number; +} /** * @internal */ export abstract class ScoreNoteChordGlyphBase extends Glyph { private _infos: ScoreNoteGlyphInfo[] = []; + // TODO[perf]: keeping the whole group only for stemX prevents the GC to collect this + // maybe we can do some better "finalization" of the groups once all voices have been done + private _noteHeadInfo?: ScoreChordNoteHeadInfo; + protected noteGroup?: ScoreChordNoteHeadGroup; public minNote: ScoreNoteGlyphInfo | null = null; public maxNote: ScoreNoteGlyphInfo | null = null; - public upLineX: number = 0; - public downLineX: number = 0; + public get stemX(): number { + if (!this.noteGroup) { + return 0; + } + + return this.noteGroup!.stemX + this.noteGroup!.multiVoiceShiftX; + } + public noteStartX: number = 0; public onTimeX = 0; @@ -27,6 +332,8 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { } public abstract get direction(): BeamDirection; + public abstract get hasFlag(): boolean; + public abstract get hasStem(): boolean; public abstract get scale(): number; public override getBoundingBoxTop(): number { @@ -45,8 +352,8 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { return this.minNote ? (this.renderer as ScoreBarRenderer).getScoreY(this.minNote.steps) : 0; } - protected add(noteGlyph: MusicFontGlyph, noteLine: number): void { - const info: ScoreNoteGlyphInfo = new ScoreNoteGlyphInfo(noteGlyph, noteLine); + protected add(noteGlyph: NoteHeadGlyphBase, noteSteps: number): void { + const info: ScoreNoteGlyphInfo = { glyph: noteGlyph, steps: noteSteps }; this._infos.push(info); if (!this.minNote || this.minNote.steps > info.steps) { this.minNote = info; @@ -56,135 +363,266 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { } } - public override doLayout(): void { - this._infos.sort((a, b) => { - return b.steps - a.steps; - }); - let stemUpX: number = 0; - let stemDownX: number = 0; - let lastDisplaced: boolean = true; - let lastStep: number = 0; - let anyDisplaced = false; + protected abstract getScoreChordNoteHeadInfo(): ScoreChordNoteHeadInfo; + + private _prepareForLayout(info: ScoreChordNoteHeadInfo): ScoreChordNoteHeadGroup { const direction: BeamDirection = this.direction; - // first get stem position on the right side (displacedX) - // to align all note heads accordingly (they might have different widths) - const smufl = this.renderer.smuflMetrics; + // initialize empty info object + if (!info.groups) { + info.mainVoiceDirection = direction; + } + + // sorting helps avoiding weird alignments + // if the stem is upwards we go bottom-up otherwise top-down + // this ensures we start placing notes on the primary side. + if (direction === BeamDirection.Up) { + this._infos.sort((a, b) => { + return b.steps - a.steps; + }); + } else { + this._infos.sort((a, b) => { + return a.steps - b.steps; + }); + } + + // obtain group we belong to + let group: ScoreChordNoteHeadGroup; + const hasFlag = this.hasFlag; + const hasStem = this.hasStem; + if (info.groups!.has(direction)) { + group = info.groups!.get(direction)!; + if (hasFlag) { + group.hasFlag = hasFlag; + } + if (hasStem) { + group.hasStem = hasStem; + } + } else { + group = { + correctNotes: { + notes: new Map(), + width: 0, + minX: Number.NaN + }, + direction, + stemX: 0, + maxX: Number.NaN, + minX: Number.NaN, + minStep: Number.NaN, + maxStep: Number.NaN, + multiVoiceShiftX: 0, + hasFlag, + hasStem + }; + info.groups.set(direction, group); + } + return group; + } + + public override doLayout(): void { + // generally we try to follow the rules defined in "behind the bars" + // "Double-stemmed writing" but not all rules might be implemented + + // The note head alignment has following base logic and rules: + // 1. we have two note groups: + // * stem up + // * stem down + // 2. we have 4 x-positions for note heads + // * stem up non-displaced (left from stem) + // * stem up displaced (right from stem) + // * stem down non-displaced (right from stem) + // * stem down displaced (left from stem) + // 3. by default the non-displaced notes across note groups align vertically + // 4. every note head is registered on its "step" position of the current group + // 5. if the step of the note or +/- 1 step is reserved in the current group, the note head is displaced, otherwise it is non-displaced + // 6. the group of the first voice beat defines the "primary" stem-direction, all other voice beats are "secondary" stem-directions + // 7. if the current voice matches the primary stem direction and we have overlaps: + // * no shifting of the group is needed + // 8. if the current voice does NOT match the primary stem and we have overlaps we might need shifting of the whole group: + // * if the note heads are on the exact same position and have both the same "black" note head (grace, shape etc. are accounted) + // there is no shift and the same "spot" can be used + // * if there is a +/- 1 overlap a shift of the whole group is applied + + // NOTE: + // * This logic is close to what MuseScore does + // * Dorico has own "note groups" for every voice, even if they are in the same stem-direction. + // Then they displace the groups similarly like with different stem-directions (never merging note heads) + + const info = this.getScoreChordNoteHeadInfo(); + const noteGroup = this._prepareForLayout(info); + this.noteGroup = noteGroup; + this._noteHeadInfo = info; + + this._collectNoteDisplacements(noteGroup); + this._alignNoteHeadsGroup(noteGroup); + + info.update(); + + this._updateSizes(); + } + + private _updateSizes() { + const noteGroup = this.noteGroup!; + + // NOTE: no noteGroup.multiVoiceShiftX for onTimeX. we don't shift the time position on displacement + // otherwise the alignment would automatically be corrected and we get no actual "shift" + + // the center of score notes, (used for aligning the beat to the right on-time position) + // is always the center of the "correct note" position. + this.onTimeX = noteGroup.correctNotes.minX + noteGroup.correctNotes.width / 2; + + this.width = this.noteStartX + noteGroup.multiVoiceShiftX + noteGroup.maxX - noteGroup.minX; + } + + public doMultiVoiceLayout() { + this._noteHeadInfo!.finish(this.renderer.smuflMetrics); + this._updateSizes(); + } + + private _alignNoteHeadsGroup(noteGroup: ScoreChordNoteHeadGroup) { + // align all notes so that they align with the stem positions + if (noteGroup.direction === BeamDirection.Up) { + this._alignNoteHeads(noteGroup, noteGroup.correctNotes, true); + if (noteGroup.displacedNotes) { + this._alignNoteHeads(noteGroup, noteGroup.displacedNotes!, false); + } + } else { + this._alignNoteHeads(noteGroup, noteGroup.correctNotes, false); + if (noteGroup.displacedNotes) { + this._alignNoteHeads(noteGroup, noteGroup.displacedNotes!, true); + } + } + } + private _alignNoteHeads( + noteGroup: ScoreChordNoteHeadGroup, + side: ScoreChordNoteHeadGroupSide, + leftOfStem: boolean + ) { const scale = this.scale; - const displaced = new Map(); - for (let i: number = 0, j: number = this._infos.length; i < j; i++) { - const g = this._infos[i].glyph; - g.renderer = this.renderer; - g.doLayout(); - - if (i > 0 && Math.abs(lastStep - this._infos[i].steps) <= 1) { - if (!lastDisplaced) { - anyDisplaced = true; - lastDisplaced = true; - displaced.set(i, true); + 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) { + // align directly + info.glyph.x = noteGroup.stemX; + + // + if (info.glyph.centerOnStem) { + // no offset + } + // 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; + } } else { - lastDisplaced = false; - displaced.set(i, false); + // 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; + } } - } else { - lastDisplaced = false; - displaced.set(i, false); - } - if (smufl.stemUp.has(g.symbol)) { - const stemInfo = smufl.stemUp.get(g.symbol)!; - const topX = stemInfo.x * scale; - if (topX > stemUpX) { - stemUpX = topX; + // update side + side.width = Math.max(side.width, info.glyph.width); + if (Number.isNaN(side.minX) || info.glyph.x < side.minX) { + side.minX = info.glyph.x; } - } else { - const topX = smufl.glyphWidths.get(g.symbol)! * scale; - if (topX > stemUpX) { - stemUpX = topX; - } - } - if (smufl.stemDown.has(g.symbol)) { - const stemInfo = smufl.stemDown.get(g.symbol)!; - const topX = stemInfo.x * scale; - if (topX > stemDownX) { - const diff = topX - stemDownX; - stemDownX = topX; - stemUpX += diff; // shift right accordingly + // update whole group + if (Number.isNaN(noteGroup.minX) || info.glyph.x < noteGroup.minX) { + noteGroup.minX = info.glyph.x; + } + const maxX = info.glyph.x + info.glyph.width; + if (Number.isNaN(noteGroup.maxX) || maxX > noteGroup.maxX) { + noteGroup.maxX = maxX; } } - - lastStep = this._infos[i].steps; } + } - // align all notes so that they align with the stem positions + private static _hasCollision(side: ScoreChordNoteHeadGroupSide, info: ScoreNoteGlyphInfo) { + return side.notes.has(info.steps) || side.notes.has(info.steps + 1) || side.notes.has(info.steps - 1); + } - const stemPosition = anyDisplaced || direction === BeamDirection.Up ? stemUpX : stemDownX; + private _collectNoteDisplacements(noteGroup: ScoreChordNoteHeadGroup) { + for (const info of this._infos) { + info.glyph.renderer = this.renderer; + info.glyph.doLayout(); - let w: number = 0; - let displacedWidth = 0; - let nonDisplacedWidth = 0; - for (let i: number = 0, j: number = this._infos.length; i < j; i++) { - const g = this._infos[i].glyph; - const alignDisplaced: boolean = displaced.get(i)!; + const isGroupCollision = ScoreNoteChordGlyphBase._hasCollision(noteGroup.correctNotes, info); - if (alignDisplaced) { - // displaced: shift note to stem position - g.x = stemPosition; - } else { - // not displaced: align on left side (where down stem would be for notes) - g.x = stemDownX; - if (smufl.stemDown.has(g.symbol)) { - g.x -= smufl.stemDown.get(g.symbol)!.x * scale; + let noteLookup: ScoreChordNoteHeadGroupSide; + if (isGroupCollision) { + if (!noteGroup.displacedNotes) { + noteGroup.displacedNotes = { notes: new Map(), width: 0, minX: 0 }; } + noteLookup = noteGroup.displacedNotes!; + } else { + noteLookup = noteGroup.correctNotes!; } - g.x += this.noteStartX; - const gw = g.x + g.width; - w = Math.max(w, gw); - if (alignDisplaced) { - displacedWidth = Math.max(displacedWidth, gw); + let stepInfos: ScoreNoteGlyphInfo[]; + if (noteLookup.notes.has(info.steps)) { + stepInfos = noteLookup.notes.get(info.steps)!; } else { - nonDisplacedWidth = Math.max(nonDisplacedWidth, gw); + stepInfos = []; + noteLookup.notes.set(info.steps, stepInfos); } + stepInfos.push(info); - // after size calculation, re-align glyph to stem if needed - if (g instanceof NoteHeadGlyph && (g as NoteHeadGlyph).centerOnStem) { - g.x = stemPosition; + if (Number.isNaN(noteGroup.minStep) || info.steps < noteGroup.minStep) { + noteGroup.minStep = info.steps; } - } - if (anyDisplaced) { - this.upLineX = stemPosition; - this.downLineX = stemPosition; - } else { - this.upLineX = stemUpX; - this.downLineX = stemDownX; + if (Number.isNaN(noteGroup.maxStep) || info.steps > noteGroup.maxStep) { + noteGroup.maxStep = info.steps; + } + + this._updateGroupStemXPosition(info, noteGroup); } + } - // the center of score notes, (used for aligning the beat to the right on-time position) - // is always the center of the "correct note" position. - // * If the stem is upwards, the center is the middle of the left hand side note head - // * If the stem is downards, the center is the middle of the right-hand-side note head - if (anyDisplaced) { - if (direction === BeamDirection.Up) { - this.onTimeX = nonDisplacedWidth / 2; + 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 { - const displacedRawWith = displacedWidth - stemPosition; - this.onTimeX = stemPosition + (displacedRawWith / 2); + stemX = smufl.glyphWidths.get(info.glyph.symbol)! * scale; } } else { - // for no displaced notes it is simply the center - this.onTimeX = w / 2; + if (smufl.stemDown.has(info.glyph.symbol)) { + const stemInfo = smufl.stemDown.get(info.glyph.symbol)!; + stemX = stemInfo.x * scale; + } else { + stemX = 0; + } } - this.width = w; + stemX += this.noteStartX; + + if (stemX > noteGroup.stemX) { + noteGroup.stemX = stemX; + } } public override paint(cx: number, cy: number, canvas: ICanvas): void { cx += this.x; cy += this.y; + this._paintLedgerLines(cx, cy, canvas); + const noteGroup = this.noteGroup!; + cx += noteGroup.multiVoiceShiftX; + const infos: ScoreNoteGlyphInfo[] = this._infos; for (const g of infos) { g.glyph.renderer = this.renderer; @@ -203,7 +641,7 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { const scale = this.scale; const lineExtension: number = this.renderer.smuflMetrics.legerLineExtension * scale; - const lineWidth: number = this.width - this.noteStartX + lineExtension * 2; + const lineWidth: number = this.width + lineExtension * 2 - this.noteStartX; const lineSpacing = scoreRenderer.getLineHeight(1); const firstTopLedgerY = scoreRenderer.getLineY(-1); diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteGlyphInfo.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteGlyphInfo.ts deleted file mode 100644 index 2aec3ce08..000000000 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteGlyphInfo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; - -/** - * @internal - */ -export class ScoreNoteGlyphInfo { - public glyph: MusicFontGlyph; - public steps: number = 0; - - public constructor(glyph: MusicFontGlyph, line: number) { - this.glyph = glyph; - this.steps = line; - } -} diff --git a/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts index 55d1ff768..6dffdac1f 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts @@ -9,9 +9,9 @@ import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import type { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { NoteVibratoGlyph } from '@coderline/alphatab/rendering/glyphs/NoteVibratoGlyph'; -import type { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; import type { ITieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; +import type { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; /** * @internal @@ -22,7 +22,7 @@ export class ScoreSlideLineGlyph extends Glyph implements ITieGlyph { private _startNote: Note; private _parent: BeatContainerGlyph; - // the slide line cannot overflow anything and there are ties drawn in here + // the slide line cannot overflow anything and there are ties drawn in here public readonly checkForOverflow = false; public constructor(inType: SlideInType, outType: SlideOutType, startNote: Note, parent: BeatContainerGlyph) { @@ -72,11 +72,8 @@ export class ScoreSlideLineGlyph extends Glyph implements ITieGlyph { } private _getAccidentalsWidth(renderer: ScoreBarRenderer, beat: Beat): number { - const preNotes: ScoreBeatPreNotesGlyph = renderer.getPreNotesGlyphForBeat(beat) as ScoreBeatPreNotesGlyph; - if (preNotes && preNotes.accidentals) { - return preNotes.accidentals.width; - } - return 0; + const container = renderer.getBeatContainer(beat) as ScoreBeatContainerGlyph; + return container.accidentalsWidth; } private _drawSlideOut(cx: number, cy: number, canvas: ICanvas): void { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts index 4d0eba77e..228a1203b 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts @@ -1,3 +1,4 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import type { BendPoint } from '@coderline/alphatab/model/BendPoint'; import { BendStyle } from '@coderline/alphatab/model/BendStyle'; @@ -9,11 +10,10 @@ import type { ICanvas, TextAlign } from '@coderline/alphatab/platform/ICanvas'; import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { BendNoteHeadGroupGlyph } from '@coderline/alphatab/rendering/glyphs/BendNoteHeadGroupGlyph'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import type { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreHelperNotesBaseGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreHelperNotesBaseGlyph'; import { type ITieGlyph, TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; +import type { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; @@ -21,14 +21,16 @@ import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementS * @internal */ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements ITieGlyph { + private _container: ScoreBeatContainerGlyph; private _beat: Beat; private _endGlyph: BendNoteHeadGroupGlyph | null = null; public readonly checkForOverflow = false; - public constructor(beat: Beat) { + public constructor(container: ScoreBeatContainerGlyph) { super(0, 0); - this._beat = beat; + this._container = container; + this._beat = container.beat; } public get hasBoundingBox(): boolean { @@ -53,6 +55,10 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT return super.getBoundingBoxBottom(); } + public doMultiVoiceLayout() { + this._endGlyph?.doMultiVoiceLayout(); + } + public override doLayout(): void { const sr = this.renderer as ScoreBarRenderer; const whammyMode: NotationMode = sr.settings.notation.notationMode; @@ -64,7 +70,11 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT case WhammyType.Dive: case WhammyType.PrediveDive: { - const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); + const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph( + 'postwhammy', + this._beat, + false + ); this._endGlyph = endGlyphs; endGlyphs.renderer = sr; const lastWhammyPoint: BendPoint = @@ -88,7 +98,11 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT // handled separately return; } else { - const middleGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); + const middleGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph( + 'middlewhammy', + this._beat, + false + ); middleGlyphs.renderer = sr; if (sr.settings.notation.notationMode === NotationMode.GuitarPro) { const middleBendPoint: BendPoint = this._beat.whammyBarPoints![1]; @@ -102,7 +116,11 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT } middleGlyphs.doLayout(); this.addGlyph(middleGlyphs); - const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); + const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph( + 'postwhammy', + this._beat, + false + ); endGlyphs.renderer = sr; this._endGlyph = endGlyphs; @@ -165,15 +183,12 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT startY += startNoteRenderer.getNoteY(note, NoteYPosition.Top); } - let endX: number = cx + startNoteRenderer.x; - if (beat.isLastOfVoice) { - endX += startNoteRenderer.postBeatGlyphsStart; - } else { - endX += startNoteRenderer.getBeatX(beat, BeatXPosition.EndBeat); - } + let endX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(beat, BeatXPosition.EndBeat); + endX -= this.renderer.smuflMetrics.postNoteEffectPadding; if (this._endGlyph) { - endX -= this._endGlyph.upLineX; + const postBeatSize = this._endGlyph.width - this._endGlyph.onTimeX; + endX -= postBeatSize; } const slurText: string = beat.whammyStyle === BendStyle.Gradual && i === 0 ? 'grad.' : ''; @@ -193,7 +208,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT } } - let heightOffset: number = noteHeadHeight * NoteHeadGlyph.GraceScale * 0.5; + let heightOffset: number = noteHeadHeight * EngravingSettings.GraceScale * 0.5; if (direction === BeamDirection.Up) { heightOffset = -heightOffset; } @@ -246,7 +261,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT case WhammyType.Dive: if (i === 0) { const g0 = this.glyphs![0] as BendNoteHeadGroupGlyph; - g0.x = endX - g0.noteHeadOffset; + g0.x = endX - g0.onTimeX; const previousY = this.glyphs![0].y; g0.y = cy + startNoteRenderer.y; g0.paint(0, 0, canvas); @@ -263,7 +278,6 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT endX, endY, direction === BeamDirection.Down, - 1, slurText ); } else if (note.isTieOrigin) { @@ -298,7 +312,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT } else { const middleX: number = (startX + endX) / 2; const g0 = this.glyphs![0] as BendNoteHeadGroupGlyph; - g0.x = middleX - g0.noteHeadOffset; + g0.x = middleX - g0.onTimeX; g0.y = cy + startNoteRenderer.y; g0.paint(0, 0, canvas); const middleValue: number = this._getBendNoteValue(note, beat.whammyBarPoints![1]); @@ -310,12 +324,11 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT middleX, middleY, direction === BeamDirection.Down, - 1, slurText ); const g1 = this.glyphs![1] as BendNoteHeadGroupGlyph; - g1.x = endX - g1.noteHeadOffset; + g1.x = endX - g1.onTimeX; g1.y = cy + startNoteRenderer.y; g1.paint(0, 0, canvas); endY = g1.getNoteValueY(endValue) + heightOffset; @@ -326,7 +339,6 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT endX, endY, direction === BeamDirection.Down, - 1, slurText ); } @@ -335,8 +347,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT case WhammyType.Predive: let preX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); - preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) - .prebendNoteHeadOffset; + preX += this._container.prebendNoteHeadOffset; const preY: number = cy + startNoteRenderer.y + @@ -347,19 +358,10 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT ) ) + heightOffset; - this.drawBendSlur( - canvas, - preX, - preY, - startX, - startY, - direction === BeamDirection.Down, - 1, - slurText - ); + this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down, slurText); if (this.glyphs) { const g0 = this.glyphs![0] as BendNoteHeadGroupGlyph; - g0.x = endX - g0.noteHeadOffset; + g0.x = endX - g0.onTimeX; g0.y = cy + startNoteRenderer.y; g0.paint(0, 0, canvas); this.drawBendSlur( @@ -369,7 +371,6 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT endX, endY, direction === BeamDirection.Down, - 1, slurText ); } diff --git a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts index 4e6d7963c..c5f81256b 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts @@ -1,19 +1,18 @@ +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; -import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; +import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; +import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; +import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; +import { SlashRestGlyph } from '@coderline/alphatab/rendering/glyphs/SlashRestGlyph'; import type { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRenderer'; -import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; +import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; -import { SlashRestGlyph } from '@coderline/alphatab/rendering/glyphs/SlashRestGlyph'; -import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; -import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; +import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; /** * @internal @@ -74,6 +73,25 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { return this.noteHeads ? this.noteHeads.y : 0; } + public override getRestY(requestedPosition: NoteYPosition): number { + const g = this.restGlyph; + if (g) { + switch (requestedPosition) { + case NoteYPosition.TopWithStem: + case NoteYPosition.Top: + return g.getBoundingBoxTop(); + case NoteYPosition.Center: + case NoteYPosition.StemUp: + case NoteYPosition.StemDown: + return g.getBoundingBoxTop() + g.height / 2; + case NoteYPosition.Bottom: + case NoteYPosition.BottomWithStem: + return g.getBoundingBoxBottom(); + } + } + return 0; + } + public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { let g: Glyph | null = null; let symbol: MusicFontSymbol = MusicFontSymbol.None; @@ -167,10 +185,7 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { this.stemX = this.onTimeX; } else if (this.noteHeads) { this.onTimeX = this.noteHeads.x + this.noteHeads.width / 2; - const direction = this.renderer.getBeatDirection(this.container.beat); - this.stemX = - this.noteHeads!.x + - (direction === BeamDirection.Up ? this.noteHeads!.upLineX : this.noteHeads!.downLineX); + this.stemX = this.noteHeads!.x + this.noteHeads!.stemX; } else if (this.deadSlapped) { this.onTimeX = this.deadSlapped.x + this.deadSlapped.width / 2; this.stemX = this.onTimeX; diff --git a/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts index c8ff280fa..6349a8841 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts @@ -3,25 +3,24 @@ import { Duration } from '@coderline/alphatab/model/Duration'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { NoteSubElement } from '@coderline/alphatab/model/Note'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * @internal */ -export class SlashNoteHeadGlyph extends MusicFontGlyph { +export class SlashNoteHeadGlyph extends NoteHeadGlyphBase { public beatEffects: Map = new Map(); public noteHeadElement: NoteSubElement = NoteSubElement.SlashNoteHead; public effectElement: BeatSubElement = BeatSubElement.SlashEffects; private _symbol: MusicFontSymbol; - public upLineX: number = 0; - public downLineX: number = 0; + public stemX: number = 0; public constructor(x: number, y: number, duration: Duration, isGrace: boolean, beat: Beat) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, SlashNoteHeadGlyph.getSymbol(duration)); + super(x, y, isGrace, SlashNoteHeadGlyph.getSymbol(duration)); this._symbol = SlashNoteHeadGlyph.getSymbol(duration); this.beat = beat; } @@ -69,16 +68,19 @@ export class SlashNoteHeadGlyph extends MusicFontGlyph { this.renderer.registerBeatEffectOverflows(minEffectY, maxEffectY); } + const direction = this.renderer.getBeatDirection(this.beat!); const symbol = this._symbol; - const stemInfoUp = this.renderer.smuflMetrics.stemUp.has(symbol) - ? this.renderer.smuflMetrics.stemUp.get(symbol)!.x - : 0; - this.upLineX = stemInfoUp; - - const stemInfoDown = this.renderer.smuflMetrics.stemDown.has(symbol) - ? this.renderer.smuflMetrics.stemDown.get(symbol)!.x - : 0; - this.downLineX = stemInfoDown; + if (direction === BeamDirection.Up) { + const stemInfoUp = this.renderer.smuflMetrics.stemUp.has(symbol) + ? this.renderer.smuflMetrics.stemUp.get(symbol)!.x + : 0; + this.stemX = stemInfoUp; + } else { + const stemInfoDown = this.renderer.smuflMetrics.stemDown.has(symbol) + ? this.renderer.smuflMetrics.stemDown.get(symbol)!.x + : 0; + this.stemX = stemInfoDown; + } } public static getSymbol(duration: Duration): MusicFontSymbol { diff --git a/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts index 2af0ea003..437899ea0 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts @@ -1,7 +1,10 @@ +import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { TabBeatGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatGlyph'; +import { TabBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatPreNotesGlyph'; import { TabBendGlyph } from '@coderline/alphatab/rendering/glyphs/TabBendGlyph'; import { TabSlideLineGlyph } from '@coderline/alphatab/rendering/glyphs/TabSlideLineGlyph'; import { TabSlurGlyph } from '@coderline/alphatab/rendering/glyphs/TabSlurGlyph'; @@ -16,6 +19,12 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { private _bend: TabBendGlyph | null = null; private _effectSlurs: TabSlurGlyph[] = []; + public constructor(beat: Beat) { + super(beat); + this.preNotes = new TabBeatPreNotesGlyph(); + this.onNotes = new TabBeatGlyph(); + } + protected override drawBeamHelperAsFlags(helper: BeamingHelper): boolean { return helper.hasFlag((this.renderer as TabBarRenderer).drawBeamHelperAsFlags(helper), this.beat); } @@ -78,7 +87,13 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { } } if (!expanded) { - const effectSlur: TabSlurGlyph = new TabSlurGlyph(`tab.slur.effect.${n.effectSlurOrigin.id}`, 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 d7bfee596..0120bc7b6 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts @@ -8,7 +8,7 @@ import { NoteNumberGlyph } from '@coderline/alphatab/rendering/glyphs/NoteNumber import { TabNoteChordGlyph } from '@coderline/alphatab/rendering/glyphs/TabNoteChordGlyph'; import { TabRestGlyph } from '@coderline/alphatab/rendering/glyphs/TabRestGlyph'; import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; -import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { type NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; @@ -34,6 +34,25 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { 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: + case NoteYPosition.Top: + return g.getBoundingBoxTop(); + case NoteYPosition.Center: + case NoteYPosition.StemUp: + case NoteYPosition.StemDown: + return g.getBoundingBoxTop() + g.height / 2; + case NoteYPosition.Bottom: + case NoteYPosition.BottomWithStem: + return g.getBoundingBoxBottom(); + } + } + return 0; + } + public override getLowestNoteY(): number { return this.noteNumbers ? this.noteNumbers.getLowestNoteY() : 0; } @@ -106,12 +125,7 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { const y: number = tabRenderer.getFlagAndBarPos(); for (let i: number = 0; i < this.container.beat.dots; i++) { - this.addEffect( - new AugmentationDotGlyph( - 0, - y - ) - ); + this.addEffect(new AugmentationDotGlyph(0, y)); } } } else { diff --git a/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts index 6bf9d8acf..67471154d 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts @@ -290,17 +290,19 @@ export class TabBendGlyph extends Glyph implements ITieGlyph { let endX: number = 0; if (!endBeat || (endBeat.isLastOfVoice && !endNoteHasBend)) { endX = cx + endNoteRenderer!.x + endNoteRenderer!.postBeatGlyphsStart; + endX -= this.renderer.smuflMetrics.postNoteEffectPadding; } else if (endNoteHasBend || !endBeat.nextBeat) { endX = cx + endNoteRenderer!.x + endNoteRenderer!.getBeatX(endBeat, BeatXPosition.MiddleNotes); } else if (note.bendType === BendType.Hold) { endX = cx + endNoteRenderer!.x + endNoteRenderer!.getBeatX(endBeat.nextBeat, BeatXPosition.OnNotes); } else { endX = cx + endNoteRenderer!.x + endNoteRenderer!.getBeatX(endBeat.nextBeat, BeatXPosition.PreNotes); + endX -= this.renderer.smuflMetrics.postNoteEffectPadding; } // we need some pixels for the arrow. otherwise we might draw into the next if (!isMultiBeatBend) { - endX -= tabBendArrowSize; + endX -= tabBendArrowSize / 2; } this._paintBendLines(canvas, startX, topY, endX, startNoteRenderer, note, renderPoints); diff --git a/packages/alphatab/src/rendering/glyphs/TabTimeSignatureGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabTimeSignatureGlyph.ts index 10013c6df..27c888365 100644 --- a/packages/alphatab/src/rendering/glyphs/TabTimeSignatureGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabTimeSignatureGlyph.ts @@ -1,7 +1,7 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { BarSubElement } from '@coderline/alphatab/model/Bar'; import { TimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/TimeSignatureGlyph'; import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { BarSubElement } from '@coderline/alphatab/model/Bar'; /** * @internal @@ -19,7 +19,7 @@ export class TabTimeSignatureGlyph extends TimeSignatureGlyph { protected get numberScale(): number { const renderer: TabBarRenderer = this.renderer as TabBarRenderer; if (renderer.bar.staff.tuning.length <= 4) { - return NoteHeadGlyph.GraceScale; + return EngravingSettings.GraceScale; } return 1; } diff --git a/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts index d80c39c9a..bd09b2030 100644 --- a/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts @@ -157,9 +157,18 @@ export class TabWhammyBarGlyph extends EffectGlyph { endX = cx + startNoteRenderer.getBeatX(this._beat, BeatXPosition.PostNotes, true); } else { startX = cx + startNoteRenderer.getBeatX(this._beat, BeatXPosition.MiddleNotes, true); - endX = !endNoteRenderer - ? cx + startNoteRenderer.postBeatGlyphsStart - : cx - startNoteRenderer.x + endNoteRenderer.x + endNoteRenderer.getBeatX(endBeat!, endXPositionType, true); + if (endNoteRenderer) { + endX = + cx - + startNoteRenderer.x + + endNoteRenderer.x + + endNoteRenderer.getBeatX(endBeat!, endXPositionType, true); + } else { + endX = + cx + + startNoteRenderer.getBeatX(this._beat!, BeatXPosition.EndBeat) - + startNoteRenderer.smuflMetrics.postNoteEffectPadding; + } } const oldAlign = canvas.textAlign; @@ -168,7 +177,6 @@ export class TabWhammyBarGlyph extends EffectGlyph { canvas.textBaseline = TextBaseline.Alphabetic; canvas.font = this.renderer.resources.tablatureFont; - if (this._renderPoints.length >= 2) { const dx: number = (endX - startX) / BendPoint.MaxPosition; canvas.beginPath(); diff --git a/packages/alphatab/src/rendering/glyphs/TieGlyph.ts b/packages/alphatab/src/rendering/glyphs/TieGlyph.ts index 84d3af21d..d74767242 100644 --- a/packages/alphatab/src/rendering/glyphs/TieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TieGlyph.ts @@ -22,9 +22,9 @@ export interface ITieGlyph { export abstract class TieGlyph extends Glyph implements ITieGlyph { public tieDirection: BeamDirection = BeamDirection.Up; public readonly slurEffectId: string; - protected isForEnd:boolean; + protected isForEnd: boolean; - public constructor(slurEffectId: string, forEnd:boolean) { + public constructor(slurEffectId: string, forEnd: boolean) { super(0, 0); this.slurEffectId = slurEffectId; this.isForEnd = forEnd; @@ -118,12 +118,21 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { this._boundingBox = undefined; this.y = Math.min(this._startY, this._endY); + let tieBoundingBox: Bounds; if (this.shouldDrawBendSlur()) { - this._tieHeight = 0; // TODO: Bend slur height to be considered? + this._tieHeight = 0; + tieBoundingBox = TieGlyph.calculateBendSlurHeight( + this._startX, + this._startY, + this._endX, + this._endY, + this.tieDirection === BeamDirection.Down, + this.renderer.smuflMetrics.tieHeight + ); } else { this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY); - const tieBoundingBox = TieGlyph.calculateActualTieHeight( + tieBoundingBox = TieGlyph.calculateActualTieHeight( 1, this._startX, this._startY, @@ -133,18 +142,19 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness ); - this._boundingBox = tieBoundingBox; + } - this.height = tieBoundingBox.h; + this._boundingBox = tieBoundingBox; - 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.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; } } } @@ -162,7 +172,6 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { cx + this._endX, cy + this._endY, this.tieDirection === BeamDirection.Down, - 1, this.renderer.smuflMetrics.tieHeight ); } else { @@ -226,7 +235,7 @@ export abstract 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){ + if (cp.length === 0) { return new Bounds(x1, y1, x2 - x1, y2 - y1); } @@ -417,6 +426,39 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { return cp1Y; } + public static calculateBendSlurHeight( + x1: number, + y1: number, + x2: number, + y2: number, + down: boolean, + bendSlurHeight: number + ): Bounds { + let normalVectorX: number = y2 - y1; + let normalVectorY: number = x2 - x1; + const length: number = Math.sqrt(normalVectorX * normalVectorX + normalVectorY * normalVectorY); + if (down) { + normalVectorX *= -1; + } else { + normalVectorY *= -1; + } + // make to unit vector + normalVectorX /= length; + normalVectorY /= length; + // center of connection + const centerY: number = (y2 + y1) / 2; + let offset: number = bendSlurHeight; + if (x2 - x1 < 20) { + offset /= 2; + } + const cp1Y: number = centerY + offset * normalVectorY; + + const minY = Math.min(y1, y2, cp1Y); + const maxY = Math.max(y1, y2, cp1Y); + + return new Bounds(x1, Math.min(y1, y2, cp1Y), x2 - x1, maxY - minY); + } + public static drawBendSlur( canvas: ICanvas, x1: number, @@ -424,7 +466,6 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { x2: number, y2: number, down: boolean, - scale: number, bendSlurHeight: number, slurText?: string ): void { @@ -440,10 +481,9 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { normalVectorX /= length; normalVectorY /= length; // center of connection - // TODO: should be 1/3 const centerX: number = (x2 + x1) / 2; const centerY: number = (y2 + y1) / 2; - let offset: number = bendSlurHeight * scale; + let offset: number = bendSlurHeight; if (x2 - x1 < 20) { offset /= 2; } @@ -472,7 +512,7 @@ export abstract class NoteTieGlyph extends TieGlyph { protected startNoteRenderer: BarRendererBase | null = null; protected endNoteRenderer: BarRendererBase | null = null; - public constructor(slurEffectId: string, startNote: Note, endNote: Note, forEnd:boolean) { + public constructor(slurEffectId: string, startNote: Note, endNote: Note, forEnd: boolean) { super(slurEffectId, forEnd); this.startNote = startNote; this.endNote = endNote; diff --git a/packages/alphatab/src/rendering/glyphs/TimeSignatureGlyph.ts b/packages/alphatab/src/rendering/glyphs/TimeSignatureGlyph.ts index 6c86697a1..eb6a8ed6a 100644 --- a/packages/alphatab/src/rendering/glyphs/TimeSignatureGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TimeSignatureGlyph.ts @@ -50,7 +50,6 @@ export abstract class TimeSignatureGlyph extends GlyphGroup { this.addGlyph(common); super.doLayout(); } else { - // TODO: ensure we align them exactly so they meet in the staff center (use glyphTop and glyphBottom accordingly) const numerator: NumberGlyph = new NumberGlyph( 0, 0, diff --git a/packages/alphatab/src/rendering/glyphs/VoiceContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/VoiceContainerGlyph.ts deleted file mode 100644 index 47e54c819..000000000 --- a/packages/alphatab/src/rendering/glyphs/VoiceContainerGlyph.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; -import { type Voice, VoiceSubElement } from '@coderline/alphatab/model/Voice'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; -import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { GlyphGroup } from '@coderline/alphatab/rendering/glyphs/GlyphGroup'; -import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; - -/** - * This glyph acts as container for handling - * multiple voice rendering - * @internal - */ -export class VoiceContainerGlyph extends GlyphGroup { - public static readonly KeySizeBeat: string = 'Beat'; - - public beatGlyphs: BeatContainerGlyph[]; - public voice: Voice; - public tupletGroups: TupletGroup[]; - - public constructor(x: number, y: number, voice: Voice) { - super(x, y); - this.voice = voice; - this.beatGlyphs = []; - this.tupletGroups = []; - } - - public scaleToWidth(width: number): void { - const force: number = this.renderer.layoutingInfo.spaceToForce(width); - this._scaleToForce(force); - } - - private _scaleToForce(force: number): void { - this.width = this.renderer.layoutingInfo.calculateVoiceWidth(force); - const positions: Map = this.renderer.layoutingInfo.buildOnTimePositions(force); - const beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; - - for (let i: number = 0, j: number = beatGlyphs.length; i < j; i++) { - const currentBeatGlyph: BeatContainerGlyph = beatGlyphs[i]; - - switch (currentBeatGlyph.beat.graceType) { - case GraceType.None: - currentBeatGlyph.x = - positions.get(currentBeatGlyph.beat.absoluteDisplayStart)! - currentBeatGlyph.onTimeX; - break; - default: - const graceDisplayStart = currentBeatGlyph.beat.graceGroup!.beats[0].absoluteDisplayStart; - const graceGroupId = currentBeatGlyph.beat.graceGroup!.id; - // placement for proper grace notes which have a following note - if (currentBeatGlyph.beat.graceGroup!.isComplete && positions.has(graceDisplayStart)) { - currentBeatGlyph.x = positions.get(graceDisplayStart)! - currentBeatGlyph.onTimeX; - - const graceSprings = this.renderer.layoutingInfo.allGraceRods.get(graceGroupId)!; - - // get the pre beat stretch of this voice/staff, not the - // shared space. This way we use the potentially empty space (see discussions/1092). - const afterGraceBeat = - currentBeatGlyph.beat.graceGroup!.beats[currentBeatGlyph.beat.graceGroup!.beats.length - 1] - .nextBeat; - const preBeatStretch = afterGraceBeat - ? this.renderer.layoutingInfo.getPreBeatSize(afterGraceBeat) - : 0; - - // move right in front to the note - currentBeatGlyph.x -= preBeatStretch; - // respect the post beat width of the grace note - currentBeatGlyph.x -= graceSprings[currentBeatGlyph.beat.graceIndex].postSpringWidth; - // shift to right position of the particular grace note - - currentBeatGlyph.x += graceSprings[currentBeatGlyph.beat.graceIndex].graceBeatWidth; - // move the whole group again forward for cases where another track has e.g. 3 beats and here we have only 2. - // so we shift the whole group of this voice to stick to the end of the group. - const lastGraceSpring = graceSprings[currentBeatGlyph.beat.graceGroup!.beats.length - 1]; - currentBeatGlyph.x -= lastGraceSpring.graceBeatWidth; - } else { - // placement for improper grace beats where no beat in the same bar follows - const graceSpring = this.renderer.layoutingInfo.incompleteGraceRods.get(graceGroupId)!; - const relativeOffset = - graceSpring[currentBeatGlyph.beat.graceIndex].postSpringWidth - - graceSpring[currentBeatGlyph.beat.graceIndex].preSpringWidth; - - if (i > 0) { - if (currentBeatGlyph.beat.graceIndex === 0) { - // we place the grace beat directly after the previous one - // otherwise this causes flickers on resizing - currentBeatGlyph.x = beatGlyphs[i - 1].x + beatGlyphs[i - 1].width; - } else { - // for the multiple grace glyphs we take the width of the grace rod - // this width setting is aligned with the positioning logic below - currentBeatGlyph.x = - beatGlyphs[i - 1].x + - graceSpring[currentBeatGlyph.beat.graceIndex - 1].postSpringWidth - - graceSpring[currentBeatGlyph.beat.graceIndex - 1].preSpringWidth - - relativeOffset; - } - } else { - currentBeatGlyph.x = -relativeOffset; - } - } - break; - } - - // size always previous glyph after we know the position - // of the next glyph - if (i > 0) { - const beatWidth: number = currentBeatGlyph.x - beatGlyphs[i - 1].x; - beatGlyphs[i - 1].width = beatWidth; - } - // for the last glyph size based on the full width - if (i === j - 1) { - const beatWidth: number = this.width - beatGlyphs[beatGlyphs.length - 1].x; - currentBeatGlyph.width = beatWidth; - } - } - } - - public registerLayoutingInfo(info: BarLayoutingInfo): void { - const beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; - for (const b of beatGlyphs) { - b.registerLayoutingInfo(info); - } - } - - public applyLayoutingInfo(info: BarLayoutingInfo): void { - const beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; - for (const b of beatGlyphs) { - b.applyLayoutingInfo(info); - } - this._scaleToForce(Math.max(this.renderer.settings.display.stretchForce, info.minStretchForce)); - } - - public override addGlyph(g: Glyph): void { - const bg: BeatContainerGlyph = g as BeatContainerGlyph; - g.x = - this.beatGlyphs.length === 0 - ? 0 - : this.beatGlyphs[this.beatGlyphs.length - 1].x + this.beatGlyphs[this.beatGlyphs.length - 1].width; - g.renderer = this.renderer; - g.doLayout(); - this.beatGlyphs.push(bg); - this.width = g.x + g.width; - if (bg.beat.hasTuplet && bg.beat.tupletGroup!.beats[0].id === bg.beat.id) { - this.tupletGroups.push(bg.beat.tupletGroup!); - } - } - - public override doLayout(): void {} - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - // canvas.color = Color.random(); - // canvas.strokeRect(cx + this.x, cy + this.y, this.width, this.renderer.height); - using _ = ElementStyleHelper.voice(canvas, VoiceSubElement.Glyphs, this.voice, true); - - for (let i: number = 0, j: number = this.beatGlyphs.length; i < j; i++) { - this.beatGlyphs[i].paint(cx + this.x, cy + this.y, canvas); - } - } -} diff --git a/packages/alphatab/src/rendering/layout/PageViewLayout.ts b/packages/alphatab/src/rendering/layout/PageViewLayout.ts index ef2c5c932..769650c52 100644 --- a/packages/alphatab/src/rendering/layout/PageViewLayout.ts +++ b/packages/alphatab/src/rendering/layout/PageViewLayout.ts @@ -235,10 +235,16 @@ export class PageViewLayout extends ScoreLayout { // if the current renderer still has space in the current system add it // also force adding in case the system is empty let renderers: MasterBarsRenderers | null = this._allMasterBarRenderers[currentIndex]; + if (system.width + renderers!.width <= maxWidth || system.masterBarsRenderers.length === 0) { system.addMasterBarRenderers(this.renderer.tracks!, renderers!); - // move to next system + // move to next bar currentIndex++; + + if(this._needsLineBreak(currentIndex)){ + system.isFull = true; + } + } else { // if we cannot wrap on the current bar, we remove the last bar // (this might even remove multiple ones until we reach a bar that can wrap); @@ -248,6 +254,9 @@ export class PageViewLayout extends ScoreLayout { } // in case we do not have space, we create a new system system.isFull = true; + } + + if (system.isFull) { system.isLast = this.lastBarIndex === system.lastBarIndex; this._systems.push(system); this._fitSystem(system); @@ -401,18 +410,7 @@ export class PageViewLayout extends ScoreLayout { this._barsFromPreviousSystem.reverse(); return system; } - // do we need a line break after this bar - let anyTrackNeedsLineBreak = false; - let allTracksNeedLineBreak = true; - for (const track of this.renderer.tracks!) { - if (track.lineBreaks && track.lineBreaks!.has(barIndex + 1)) { - anyTrackNeedsLineBreak = true; - } else { - allTracksNeedLineBreak = false; - } - } - - if (anyTrackNeedsLineBreak && allTracksNeedLineBreak) { + if (this._needsLineBreak(barIndex)) { system.isFull = true; system.isLast = false; return system; @@ -424,6 +422,19 @@ export class PageViewLayout extends ScoreLayout { return system; } + private _needsLineBreak(barIndex: number) { + let anyTrackNeedsLineBreak = false; + let allTracksNeedLineBreak = true; + for (const track of this.renderer.tracks!) { + if (track.lineBreaks && track.lineBreaks!.has(barIndex + 1)) { + anyTrackNeedsLineBreak = true; + } else { + allTracksNeedLineBreak = false; + } + } + return anyTrackNeedsLineBreak && allTracksNeedLineBreak; + } + private get _maxWidth(): number { return this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; } diff --git a/packages/alphatab/src/rendering/staves/BarLayoutingInfo.ts b/packages/alphatab/src/rendering/staves/BarLayoutingInfo.ts index 01dfe9a01..2e3e89a19 100644 --- a/packages/alphatab/src/rendering/staves/BarLayoutingInfo.ts +++ b/packages/alphatab/src/rendering/staves/BarLayoutingInfo.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 type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { BeatContainerGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { Spring } from '@coderline/alphatab/rendering/staves/Spring'; /** @@ -59,7 +60,7 @@ export class BarLayoutingInfo { return undefined; } - public setBeatSizes(beat: Beat, sizes: BarLayoutingInfoBeatSizes) { + public setBeatSizes(beat: BeatContainerGlyphBase, sizes: BarLayoutingInfoBeatSizes) { const key = beat.absoluteDisplayStart; if (this._beatSizes.has(key)) { const current = this._beatSizes.get(key)!; @@ -172,7 +173,7 @@ export class BarLayoutingInfo { return spring; } - public addBeatSpring(beat: Beat, preBeatSize: number, postBeatSize: number): void { + public addBeatSpring(beat: BeatContainerGlyphBase, preBeatSize: number, postBeatSize: number): void { const start: number = beat.absoluteDisplayStart; if (beat.graceType !== GraceType.None) { // For grace beats we just remember the the sizes required for them diff --git a/packages/alphatab/src/rendering/utils/BeamingHelper.ts b/packages/alphatab/src/rendering/utils/BeamingHelper.ts index 1e7e00c8e..abbceb559 100644 --- a/packages/alphatab/src/rendering/utils/BeamingHelper.ts +++ b/packages/alphatab/src/rendering/utils/BeamingHelper.ts @@ -79,12 +79,12 @@ export class BeamingHelper { public hasStem(forceFlagOnSingleBeat: boolean, beat?: Beat): boolean { return ( - (forceFlagOnSingleBeat && this._beatHasStem(beat!)) || - (!forceFlagOnSingleBeat && this.beats.length === 1 && this._beatHasStem(beat!)) + (forceFlagOnSingleBeat && BeamingHelper.beatHasStem(beat!)) || + (!forceFlagOnSingleBeat && BeamingHelper.beatHasStem(beat!)) ); } - private _beatHasStem(beat: Beat): boolean { + public static beatHasStem(beat: Beat): boolean { return beat!.duration > Duration.Whole; } diff --git a/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png b/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png index 13cd5a9cd..5e3201663 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/Binchois.png b/packages/alphatab/test-data/musicxml-samples/Binchois.png index a298e54db..4cba935fe 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Binchois.png and b/packages/alphatab/test-data/musicxml-samples/Binchois.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png b/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png index ca24bd61a..3c3ebac91 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png and b/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png b/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png index aa6cdf919..06f170a30 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png and b/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/22a-Noteheads.png b/packages/alphatab/test-data/musicxml-testsuite/22a-Noteheads.png index 102145b65..75457444e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/22a-Noteheads.png and b/packages/alphatab/test-data/musicxml-testsuite/22a-Noteheads.png differ diff --git a/packages/alphatab/test-data/musicxml4/bends.png b/packages/alphatab/test-data/musicxml4/bends.png index 9633a504e..b9379bc3e 100644 Binary files a/packages/alphatab/test-data/musicxml4/bends.png and b/packages/alphatab/test-data/musicxml4/bends.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/bends.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/bends.png index 6a2dca77c..7ccb20b0f 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/bends.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/bends.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/colors-disabled.png b/packages/alphatab/test-data/visual-tests/general/colors-disabled.png index cc1b23b8f..c0ee5cafa 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/colors-disabled.png and b/packages/alphatab/test-data/visual-tests/general/colors-disabled.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/colors.png b/packages/alphatab/test-data/visual-tests/general/colors.png index 27735f7e4..20c6c9328 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/colors.png and b/packages/alphatab/test-data/visual-tests/general/colors.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-voice.png b/packages/alphatab/test-data/visual-tests/layout/multi-voice.png index 3da935e83..a36238c00 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/multi-voice.png and b/packages/alphatab/test-data/visual-tests/layout/multi-voice.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Chord-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Chord-Automatic-Stem.png new file mode 100644 index 000000000..c3daa883f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Chord-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Single-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Single-Automatic-Stem.png new file mode 100644 index 000000000..943eda76f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Single-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Single-v2_Eighth_Single-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Single-v2_Eighth_Single-Automatic-Stem.png new file mode 100644 index 000000000..18a08e2ed Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Single-v2_Eighth_Single-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Automatic_Stem.png new file mode 100644 index 000000000..6f839dcec Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Reversed_Stem.png new file mode 100644 index 000000000..d2f101100 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Same_Stem.png new file mode 100644 index 000000000..3c6c9a4bb Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Automatic_Stem.png new file mode 100644 index 000000000..39b3cd61a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Reversed_Stem.png new file mode 100644 index 000000000..62c633406 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Same_Stem.png new file mode 100644 index 000000000..316683da2 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Automatic_Stem.png new file mode 100644 index 000000000..ac863c173 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Reversed_Stem.png new file mode 100644 index 000000000..b27c76911 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Same_Stem.png new file mode 100644 index 000000000..93c35c8f7 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Automatic_Stem.png new file mode 100644 index 000000000..dce084a2d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Reversed_Stem.png new file mode 100644 index 000000000..2eae959b1 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Same_Stem.png new file mode 100644 index 000000000..7fe5fa57e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Automatic_Stem.png new file mode 100644 index 000000000..5accd9b74 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Reversed_Stem.png new file mode 100644 index 000000000..6ac3747b2 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Same_Stem.png new file mode 100644 index 000000000..d306a10b2 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Automatic_Stem.png new file mode 100644 index 000000000..feb8e595f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Reversed_Stem.png new file mode 100644 index 000000000..a3de52243 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Same_Stem.png new file mode 100644 index 000000000..09ce2df6d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Automatic_Stem.png new file mode 100644 index 000000000..65379912b Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Reversed_Stem.png new file mode 100644 index 000000000..f8cc5c46e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Same_Stem.png new file mode 100644 index 000000000..de0443907 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Automatic_Stem.png new file mode 100644 index 000000000..dbb24862e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Reversed_Stem.png new file mode 100644 index 000000000..50ad5fce7 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Same_Stem.png new file mode 100644 index 000000000..88c63cbce Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Automatic_Stem.png new file mode 100644 index 000000000..d0517a27d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Reversed_Stem.png new file mode 100644 index 000000000..4b261d4be Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Same_Stem.png new file mode 100644 index 000000000..af698bd70 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Chord-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Chord-Automatic-Stem.png new file mode 100644 index 000000000..8600c6025 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Chord-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Single-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Single-Automatic-Stem.png new file mode 100644 index 000000000..f3caf0f24 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Single-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Single-v2_Eighth_Single-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Single-v2_Eighth_Single-Automatic-Stem.png new file mode 100644 index 000000000..798027608 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Single-v2_Eighth_Single-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Automatic_Stem.png new file mode 100644 index 000000000..de716af34 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Reversed_Stem.png new file mode 100644 index 000000000..98912d6ef Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Same_Stem.png new file mode 100644 index 000000000..785b5e40d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Automatic_Stem.png new file mode 100644 index 000000000..04043f05d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Reversed_Stem.png new file mode 100644 index 000000000..1807ea516 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Same_Stem.png new file mode 100644 index 000000000..d9e2e6a6a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Automatic_Stem.png new file mode 100644 index 000000000..3f578fbee Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Reversed_Stem.png new file mode 100644 index 000000000..9901300f4 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Same_Stem.png new file mode 100644 index 000000000..a1b8bcaa3 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Automatic_Stem.png new file mode 100644 index 000000000..4a0dcc810 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Reversed_Stem.png new file mode 100644 index 000000000..f3e086a08 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Same_Stem.png new file mode 100644 index 000000000..807b5daf1 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Automatic_Stem.png new file mode 100644 index 000000000..1eea4e82f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Reversed_Stem.png new file mode 100644 index 000000000..67c2d0dfa Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Same_Stem.png new file mode 100644 index 000000000..c1bac4d28 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Automatic_Stem.png new file mode 100644 index 000000000..9abfa216c Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Reversed_Stem.png new file mode 100644 index 000000000..73353d097 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Same_Stem.png new file mode 100644 index 000000000..a931e4bbf Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Automatic_Stem.png new file mode 100644 index 000000000..fb2f84e8c Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Reversed_Stem.png new file mode 100644 index 000000000..c937ab6eb Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Same_Stem.png new file mode 100644 index 000000000..c937ab6eb Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Automatic_Stem.png new file mode 100644 index 000000000..59bff58d9 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Reversed_Stem.png new file mode 100644 index 000000000..e3ca8435d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Same_Stem.png new file mode 100644 index 000000000..5b1f46c6e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Automatic_Stem.png new file mode 100644 index 000000000..813da1bc9 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Reversed_Stem.png new file mode 100644 index 000000000..794eb5cad Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Same_Stem.png new file mode 100644 index 000000000..d6759670e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Automatic_Stem.png new file mode 100644 index 000000000..ac11f0068 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Reversed_Stem.png new file mode 100644 index 000000000..7545b9561 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Same_Stem.png new file mode 100644 index 000000000..15ff3315f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Automatic_Stem.png new file mode 100644 index 000000000..8ba0d5d87 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Reversed_Stem.png new file mode 100644 index 000000000..28f1b3108 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Same_Stem.png new file mode 100644 index 000000000..0d8a2bc44 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Automatic_Stem.png new file mode 100644 index 000000000..13f85800c Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Reversed_Stem.png new file mode 100644 index 000000000..3ea76c3ed Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Same_Stem.png new file mode 100644 index 000000000..88e42cb89 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png index bdb7d4946..cc9dea729 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png index 0d827d820..7a207363e 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png index 39f5b7d61..894fbc5ba 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png index 59f99fa28..b3e5acf3b 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png index eb23b3bd5..d8c6e90f5 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png index 1ce448453..9834afcc1 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png index 8f2f51db5..043e05093 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png index f26625710..e4e345000 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png index b7d4d9fbe..b38ecb7a4 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png index a1e909db6..936560d5f 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/mixed-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/mixed-default.png index 59c0029b2..fbb04ae53 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/mixed-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/mixed-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1300.png b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1300.png index c66837999..7e7eeab8a 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1300.png and b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png index ab66baf9d..401ce05bf 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png and b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-800.png b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-800.png index e75679d87..a718c0ab4 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-800.png and b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-800.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png b/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png index 319cd7104..82e4394de 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png and b/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png differ diff --git a/packages/alphatab/test/TestPlatform.ts b/packages/alphatab/test/TestPlatform.ts index faafafd05..f887fb796 100644 --- a/packages/alphatab/test/TestPlatform.ts +++ b/packages/alphatab/test/TestPlatform.ts @@ -46,7 +46,7 @@ export class TestPlatform { * @partial */ public static async listDirectory(path: string): Promise { - return await fs.promises.readdir(path); + return (await fs.promises.readdir(path, { withFileTypes: true })).filter(t => t.isFile()).map(t => t.name); } /** @@ -125,7 +125,7 @@ export class TestPlatform { public static mapAsUnknownIterable(map: unknown): Iterable<[unknown, unknown]> { return (map as Map).entries(); } - + /** * @target web * @partial @@ -142,4 +142,10 @@ export class TestPlatform { const withConstructor = val as object; return (typeof withConstructor.constructor === 'function' && withConstructor.constructor.name) || 'Object'; } + + /** + * @target web + * @partial + */ + public static currentTestName: string = ''; } diff --git a/packages/alphatab/test/global-hooks.ts b/packages/alphatab/test/global-hooks.ts index 45f24d046..1aaddd00f 100644 --- a/packages/alphatab/test/global-hooks.ts +++ b/packages/alphatab/test/global-hooks.ts @@ -1,6 +1,7 @@ /** @target web */ import * as chai from 'chai'; import { afterAll, beforeEachTest, initializeJestSnapshot } from './mocha.jest-snapshot'; +import { TestPlatform } from 'test/TestPlatform'; export const mochaHooks = { async beforeAll() { @@ -10,6 +11,7 @@ export const mochaHooks = { beforeEach: function (done) { beforeEachTest(this.currentTest!); + TestPlatform.currentTestName = this.currentTest!.title; done(); }, diff --git a/packages/alphatab/test/importer/AlphaTexParameter.test.ts b/packages/alphatab/test/importer/AlphaTexParameter.test.ts index f61474854..d20fb79a1 100644 --- a/packages/alphatab/test/importer/AlphaTexParameter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexParameter.test.ts @@ -141,7 +141,7 @@ describe('AlphaTexParameterTests', () => { describe('metadata', () => { describe('empty signature', () => { - it('empty', () => importTest(`\\ac() C4`)); + it('empty', () => importTest(`\\track() C4`)); }); describe('single overload', () => { diff --git a/packages/alphatab/test/importer/AlphaTexParser.test.ts b/packages/alphatab/test/importer/AlphaTexParser.test.ts index 06b06120c..c94d939e1 100644 --- a/packages/alphatab/test/importer/AlphaTexParser.test.ts +++ b/packages/alphatab/test/importer/AlphaTexParser.test.ts @@ -161,6 +161,7 @@ describe('AlphaTexParserTest', () => { describe('ambiguous', () => { it('tempo and stringed note', () => parserTest('\\tempo 120 3.3 3.4')); + it('voice followed by note list', () => parserTest('\\voice (C4 C5)')); }); describe('intermediate', () => { diff --git a/packages/alphatab/test/importer/Gp3Importer.test.ts b/packages/alphatab/test/importer/Gp3Importer.test.ts index b920454a3..61e84c68e 100644 --- a/packages/alphatab/test/importer/Gp3Importer.test.ts +++ b/packages/alphatab/test/importer/Gp3Importer.test.ts @@ -98,7 +98,6 @@ describe('Gp3ImporterTest', () => { }); it('vibrato', async () => { - // TODO: Check why this vibrato is not recognized const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/vibrato.gp3'); const score: Score = reader.readScore(); GpImporterTestHelper.checkVibrato(score, false); diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap index fbeda6a89..58e202661 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap @@ -1,28 +1,31 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`AlphaTexParameterTests handler-validation metadata empty signature empty 1`] = ` -Score (1,1) -> (1,9) { +Score (1,1) -> (1,12) { bars: Array [ - Bar (1,1) -> (1,9) { + Bar (1,1) -> (1,12) { metaData: Array [ - Meta (1,1) -> (1,6) { - tag: Tag "ac" (1,1) -> (1,4) { + Meta (1,1) -> (1,9) { + tag: Tag "track" (1,1) -> (1,7) { prefix: Backslash (1,1) -> (1,2), - tag: Ident "ac" (1,2) -> (1,4), + tag: Ident "track" (1,2) -> (1,7), }, - arguments: Arguments (1,4) -> (1,6) { - openParenthesis: LParen (1,4) -> (1,5), + arguments: Arguments (1,7) -> (1,9) { + openParenthesis: LParen (1,7) -> (1,8), arguments: Array [], - closeParenthesis: RParen (1,5) -> (1,6), + closeParenthesis: RParen (1,8) -> (1,9), + signatureCandidateIndices: Array [ + 0, + ], }, }, ], beats: Array [ - Beat (1,7) -> (1,9) { - notes: NoteList (1,7) -> (1,9) { + Beat (1,10) -> (1,12) { + notes: NoteList (1,10) -> (1,12) { notes: Array [ - Note (1,7) -> (1,9) { - noteValue: Ident "C4" (1,7) -> (1,9), + Note (1,10) -> (1,12) { + noteValue: Ident "C4" (1,10) -> (1,12), }, ], }, diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap index 8544c55d8..101ce7eb2 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap @@ -64,6 +64,43 @@ exports[`AlphaTexParserTest ambiguous tempo, temponame and stringed note: lexer- exports[`AlphaTexParserTest ambiguous tempo, temponame and stringed note: parser-diagnostics 1`] = `Array []`; +exports[`AlphaTexParserTest ambiguous voice followed by note list 1`] = ` +Score (1,1) -> (1,15) { + bars: Array [ + Bar (1,1) -> (1,15) { + metaData: Array [ + Meta (1,1) -> (1,7) { + tag: Tag "voice" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "voice" (1,2) -> (1,7), + }, + }, + ], + beats: Array [ + Beat (1,8) -> (1,15) { + notes: NoteList (1,8) -> (1,15) { + openParenthesis: LParen (1,8) -> (1,9), + notes: Array [ + Note (1,9) -> (1,11) { + noteValue: Ident "C4" (1,9) -> (1,11), + }, + Note (1,12) -> (1,14) { + noteValue: Ident "C5" (1,12) -> (1,14), + }, + ], + closeParenthesis: RParen (1,14) -> (1,15), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest ambiguous voice followed by note list: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest ambiguous voice followed by note list: parser-diagnostics 1`] = `Array []`; + exports[`AlphaTexParserTest comments bar meta singleline 1`] = ` Score (1,1) -> (3,9) { bars: Array [ diff --git a/packages/alphatab/test/visualTests/features/MultiVoice.test.ts b/packages/alphatab/test/visualTests/features/MultiVoice.test.ts new file mode 100644 index 000000000..7be9178cf --- /dev/null +++ b/packages/alphatab/test/visualTests/features/MultiVoice.test.ts @@ -0,0 +1,730 @@ +import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; +import { Settings } from '@coderline/alphatab/Settings'; +import { TestPlatform } from 'test/TestPlatform'; +import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; + +describe('MultiVoiceTests', () => { + describe('displace', async () => { + // TODO: beamed notes test + + async function test(tex: string) { + const settings = new Settings(); + settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.justifyLastSystem = true; + settings.display.layoutMode = LayoutMode.Page; + + const fileName = TestPlatform.currentTestName.replaceAll(':', '_').replaceAll(',', '').replaceAll(' ', '_'); + await VisualTestHelper.runVisualTestTex( + ` + \\track {defaultSystemsLayout 1} + \\staff + \\voiceMode barWise + ${tex} + `, + `test-data/visual-tests/multivoice/${fileName}.png`, + settings, + o => { + o.runs[0].width = 600; + } + ); + } + + // v1 Quarter Single-v2 Quarter Single + it('v1 Quarter Single-v2 Quarter Single-Automatic Stem', async () => + await test( + ` + \\voice + E5*5 + \\voice + C5 D5 E5 F5 G5 + ` + )); + it('v1 Quarter Single-v2 Quarter Single-Reversed Stem', async () => + await test( + ` + \\voice + E5{beam down}*5 + \\voice + C5{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + it('v1 Quarter Single-v2 Quarter Single-Same Stem', async () => + await test( + ` + \\voice + E5{beam up}*5 + \\voice + C5{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + + // v1 Quarter Chord-v2 Quarter Single + it('v1 Quarter Chord-v2 Quarter Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5)*5 + \\voice + C5 D5 E5 F5 G5 + ` + )); + it('v1 Quarter Chord-v2 Quarter Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5){beam down}*5 + \\voice + C5{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Quarter Chord-v2 Quarter Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5){beam up}*5 + \\voice + C5{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Quarter Chord-v2 Quarter Single + it('v1 Quarter Chord-v2 Quarter Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5)*5 + \\voice + (C5 B4) (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 Quarter Chord-v2 Quarter Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5){beam down}*5 + \\voice + (C5 B4){beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 Quarter Chord-v2 Quarter Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5){beam up}*5 + \\voice + (C5 B4){beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + // v1 Quarter Single-v2 Half Single + it('v1 Quarter Single-v2 Half Single-Automatic Stem', async () => + await test( + ` + \\ts (10 4) + \\voice + E5.4 r E5.4 r E5.4 r E5.4 r E5.4 r + \\voice + C5.2 D5 E5 F5 G5 + ` + )); + it('v1 Quarter Single-v2 Half Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.4{beam down} r E5.4{beam down} r E5.4{beam down} r E5.4{beam down} r E5.4{beam down} r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + it('v1 Quarter Single-v2 Half Single-Same Stem', async () => + await test( + ` + \\voice + E5.4 r E5.4 r E5.4 r E5.4 r E5.4 r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + + // v1 Quarter Chord, Half Single + it('v1 Quarter Chord-Half Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r + \\voice + C5.2 D5 E5 F5 G5 + ` + )); + it('v1 Quarter Chord-Half Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Quarter Chord-Half Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Quarter Chord-v2 Half Chord + it('v1 Quarter Chord-v2 Half Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r + \\voice + (C5 B4).2 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 Quarter Chord-v2 Half Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).4{beam down} r (E5 F5).4{beam down} r (E5 F5).4{beam down} r (E5 F5).4{beam down} r (E5 F5).4{beam down} r + \\voice + (C5 B4).2{beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 Quarter Chord-v2 Half Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).4{beam up} r (E5 F5).4{beam up} r (E5 F5).4{beam up} r (E5 F5).4{beam up} r (E5 F5).4 r{beam up} + \\voice + (C5 B4).2{beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + // v1 8th Flag Single-v2 8th Flag Single + it('v1 8th Flag Single-v2 8th Flag Single-Automatic Stem', async () => + await test( + ` + \\ts (5 4) + \\voice + E5.8 r E5 r E5 r E5 r E5 r + \\voice + C5 r D5 r E5 r F5 r G5 r + ` + )); + it('v1 8th Flag Single-v2 8th Flag Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.8{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r + \\voice + C5{beam up} r D5{beam up} r E5{beam up} r F5{beam up} r G5 {beam up} r + ` + )); + it('v1 8th Flag Single-v2 8th Flag Single-Same Stem', async () => + await test( + ` + \\voice + E5.8{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r + \\voice + C5{beam up} r D5{beam up} r E5{beam up} r F5{beam up} r G5 {beam up} r + ` + )); + + // v1 8th Flag Chord, V2 8th Flag Single + it('v1 8th Flag Chord-V2 8th Flag Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + C5 r D5 r E5 r F5 r G5 r + ` + )); + it('v1 8th Flag Chord-V2 8th Flag Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + C5{beam up} r D5{beam up} r E5{beam up} r F5{beam up} r G5{beam up} r + ` + )); + it('v1 8th Flag Chord-V2 8th Flag Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + C5{beam up} r D5{beam up} r E5{beam up} r F5{beam up} r G5{beam up} r + ` + )); + + // v1 8th Flag Chord, V2 8th Flag Chord + it('v1 8th Flag Chord-V2 8th Flag Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + (C5 B4) r (D5 C5) r (E5 D5) r (F5 E5) r (G5 F5) r + ` + )); + it('v1 8th Flag Chord-V2 8th Flag Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + (C5 B4){beam up} r (D5 C5){beam up} r (E5 D5){beam up} r (F5 E5){beam up} r (G5 F5){beam up} r + ` + )); + it('v1 8th Flag Chord-V2 8th Flag Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + (C5 B4){beam up} r (D5 C5){beam up} r (E5 D5){beam up} r (F5 E5){beam up} r (G5 F5){beam up} r + ` + )); + + // v1 8th Flag Single-v2 Quarter Single + it('v1 8th Flag Single-v2 Quarter Single-Automatic Stem', async () => + await test( + ` + \\voice + E5.8 r E5 r E5 r E5 r E5 r + \\voice + C5.4 D5 E5 F5 G5 + ` + )); + it('v1 8th Flag Single-v2 Quarter Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.8{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r + \\voice + C5.4{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + it('v1 8th Flag Single-v2 Quarter Single-Same Stem', async () => + await test( + ` + \\voice + E5.8{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r + \\voice + C5.4{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + + // v1 8th Flag Chord-v2 Quarter Single + it('v1 8th Flag Chord-v2 Quarter Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + C5.4 D5 E5 F5 G5 + ` + )); + it('v1 8th Flag Chord-v2 Quarter Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + C5.4{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 8th Flag Chord-v2 Quarter Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + C5.4{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 8th Flag Chord-v2 Quarter Chord + it('v1 8th Flag Chord-v2 Quarter Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + (C5 B4).4 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 8th Flag Chord-v2 Quarter Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + (C5 B4).4{beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 8th Flag Chord-v2 Quarter Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8 {beam up} r (E5 F5) {beam up} r (E5 F5) {beam up} r (E5 F5) {beam up} r (E5 F5) {beam up} r + \\voice + (C5 B4).4 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + // v1 8th Flag Single-v2 Half Single + it('v1 8th Flag Single-v2 Half Single-Automatic Stem', async () => + await test( + ` + \\ts (5 2) + \\voice + E5.8 r r r E5 r r r E5 r r r E5 r r r E5 r r r + \\voice + C5.2 D5 E5 F5 G5 + ` + )); + it('v1 8th Flag Single-v2 Half Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.8{beam down} r r r E5{beam down} r r r E5{beam down} r r r E5{beam down} r r r E5{beam down} r r r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + it('v1 8th Flag Single-v2 Half Single-Same Stem', async () => + await test( + ` + \\voice + E5.8{beam up} r r r E5{beam up} r r r E5{beam up} r r r E5{beam up} r r r E5{beam up} r r r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + + // v1 8th Flag Chord-v2 Half Single + it('v1 8th Flag Chord-v2 Half Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r r r (E5 F5) r r r (E5 F5) r r r (E5 F5) r r r (E5 F5) r r r + \\voice + C5.2 D5 E5 F5 G5 + ` + )); + it('v1 8th Flag Chord-v2 Half Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 8th Flag Chord-v2 Half Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 8th Flag Chord-v2 Half Chord + it('v1 8th Flag Chord-v2 Half Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r r r (E5 F5) r r r (E5 F5) r r r (E5 F5) r r r (E5 F5) r r r + \\voice + (C5 B4).2 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 8th Flag Chord-v2 Half Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r + \\voice + (C5 B4).2{beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 8th Flag Chord-v2 Half Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r + \\voice + (C5 B4).2 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + // v1 Full Single-v2 Full Single + it('v1 Full Single-v2 Full Single-Automatic Stem', async () => + await test( + ` + \\ts (5 1) + \\voice + E5.1 E5 E5 E5 E5 + \\voice + C5.1 D5 E5 F5 G5 + ` + )); + it('v1 Full Single-v2 Full Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.1{beam down} E5{beam down} E5{beam down} E5{beam down} E5{beam down} + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Full Single-v2 Full Single-Same Stem', async () => + await test( + ` + \\voice + E5.1{beam up} E5{beam up} E5{beam up} E5{beam up} E5{beam up} + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Full Chord-v2 Full Single + it('v1 Full Chord-v2 Full Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).1 (E5 F5) (E5 F5) (E5 F5) (E5 F5) + \\voice + C5.1 D5 E5 F5 G5 + ` + )); + it('v1 Full Chord-v2 Full Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).1{beam down} (E5 F5){beam down} (E5 F5){beam down} (E5 F5){beam down} (E5 F5){beam down} + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Full Chord-v2 Full Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).1{beam up} (E5 F5){beam up} (E5 F5){beam up} (E5 F5){beam up} (E5 F5){beam up} + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Full Chord-v2 Full Chord + it('v1 Full Chord-v2 Full Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).1 (E5 F5) (E5 F5) (E5 F5) (E5 F5) + \\voice + (C5 B4).1 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 Full Chord-v2 Full Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).1{beam down} (E5 F5){beam down} (E5 F5){beam down} (E5 F5){beam down} (E5 F5){beam down} + \\voice + (C5 B4).1 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 Full Chord-v2 Full Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).1{beam up} (E5 F5){beam up} (E5 F5){beam up} (E5 F5){beam up} (E5 F5){beam up} + \\voice + (C5 B4).1 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + //// + + // v1 Half Single-v2 Full Single + it('v1 Half Single-v2 Full Single-Automatic Stem', async () => + await test( + ` + \\ts (5 1) + \\voice + E5.2 r E5 r E5 r E5 r E5 r + \\voice + C5.1 D5 E5 F5 G5 + ` + )); + it('v1 Half Single-v2 Full Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.2{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Half Single-v2 Full Single-Same Stem', async () => + await test( + ` + \\voice + E5.2{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Half Chord-v2 Full Single + it('v1 Half Chord-v2 Full Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).2 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + C5.1 D5 E5 F5 G5 + ` + )); + it('v1 Half Chord-v2 Full Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).2{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Half Chord-v2 Full Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).2{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Half Chord-v2 Full Chord + it('v1 Half Chord-v2 Full Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).2 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + (C5 B4).1 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 Half Chord-v2 Full Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).2{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + (C5 B4).1 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 Half Chord-v2 Full Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).2{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + (C5 B4).1 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + it('v1 Eighth Single-v2 Eighth Single-Automatic-Stem', async () => await test(` + \\voice + E5.8*10 + \\voice + C5*2 D5*2 E5*2 F5*2 G5*2 + `)); + + it('v1 Eighth Chord-v2 Eighth Single-Automatic-Stem', async () => await test(` + \\voice + (E5 F5).8*10 + \\voice + C5*2 D5*2 E5*2 F5*2 G5*2 + `)); + + it('v1 Eighth Chord-v2 Eighth Chord-Automatic-Stem', async () => await test(` + \\voice + (E5 F5).8*10 + \\voice + (C5 B4)*2 (D5 C5)*2 (E5 D5)*2 (F5 E5)*2 (G5 F5)*2 + `)); + + it('v1 16th Single-v2 Eighth Single-Automatic-Stem', async () => await test(` + \\voice + E5.16*20 + \\voice + C5.8*2 D5*2 E5*2 F5*2 G5*2 + `)); + + it('v1 16th Chord-v2 Eighth Single-Automatic-Stem', async () => await test(` + \\voice + (E5 F5).16*20 + \\voice + C5.8*2 D5*2 E5*2 F5*2 G5*2 + `)); + + it('v1 16th Chord-v2 Eighth Chord-Automatic-Stem', async () => await test(` + \\voice + (E5 F5).16*20 + \\voice + (C5 B4).8*2 (D5 C5)*2 (E5 D5)*2 (F5 E5)*2 (G5 F5)*2 + `)); + + // Known issues: (beat counts refer to the beats which "overlap", not the rests or filler beats) + + // Accepted due to force of same stem instead of different directions for voices: + // * Bar 3 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 6 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 9 Beat 3-5: Displace logic breaks + // * Bar 12 beat 3: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 15 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 15 Beat 3: there is an overlap + // * Bar 15 Beat 4: the half note is not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 18 Beat 3: Displace logic breaks + // * Bar 18 Beat 4: Displace logic breaks + // * Bar 18 Beat 5: Displace logic breaks + // * Bar 21 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 24 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 27 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 27 Beat 3: Displace logic breaks + // * Bar 27 Beat 4: the half note is not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 30 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 33 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 33 Beat 4: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 36 Beat 3: Displace logic breaks + // * Bar 36 Beat 4: Displace logic breaks + // * Bar 36 Beat 5: Displace logic breaks + // * Bar 39 Beat 2: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 42 Beat 2: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 42 Beat 4: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 45 Beat 3: Displace logic breaks + // * Bar 45 Beat 4: Displace logic breaks + // * Bar 45 Beat 5: Displace logic breaks + // * Bar 51 Beat 3: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 54 Beat 3: Displace logic breaks + // * Bar 54 Beat 4: Displace logic breaks + // * Bar 54 Beat 5: Displace logic breaks + // * Bar 60 Beat 4: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 63 Beat 3: Displace logic breaks + // * Bar 63 Beat 4: Displace logic breaks + // * Bar 63 Beat 5: Displace logic breaks + }); +}); diff --git a/packages/csharp/src/AlphaTab.Test/TestPlatform.cs b/packages/csharp/src/AlphaTab.Test/TestPlatform.cs index b26da441b..c8ca09f64 100644 --- a/packages/csharp/src/AlphaTab.Test/TestPlatform.cs +++ b/packages/csharp/src/AlphaTab.Test/TestPlatform.cs @@ -51,7 +51,7 @@ public static async Task LoadFileAsJson(string path) Converters = { new ArrayTupleConverterFactory() } }; - private class ArrayTupleConverterFactory :JsonConverterFactory + private class ArrayTupleConverterFactory : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { @@ -80,7 +80,7 @@ public override JsonConverter CreateConverter( typeof(ArrayTupleConverter<,>).MakeGenericType(keyType, valueType), BindingFlags.Instance | BindingFlags.Public, binder: null, - args: new object[]{options}, + args: new object[] { options }, culture: null)!; return converter; @@ -262,4 +262,19 @@ public static string GetConstructorName(object val) _ => val.GetType().Name }; } + + public static string CurrentTestName + { + get + { + var testMethodInfo = TestMethodAccessor.CurrentTest; + if (testMethodInfo == null) + { + return ""; + } + var testName = testMethodInfo.MethodInfo.GetCustomAttribute()! + .DisplayName; + return testName ?? ""; + } + } } diff --git a/packages/csharp/src/AlphaTab/Environment.cs b/packages/csharp/src/AlphaTab/Environment.cs index 0c57b357d..0948893bc 100644 --- a/packages/csharp/src/AlphaTab/Environment.cs +++ b/packages/csharp/src/AlphaTab/Environment.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -57,4 +58,9 @@ public static string QuoteJsonString(string value) { return Json.QuoteJsonString(value); } + + internal static void SortDescending(System.Collections.Generic.IList list) + { + list.Sort((a, b) => b - a); + } } diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt b/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt index e112c5dcb..d3f534bcc 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt @@ -1,5 +1,6 @@ package alphaTab +import alphaTab.collections.DoubleList import alphaTab.platform.Json import alphaTab.platform.android.AndroidCanvas import alphaTab.platform.android.AndroidEnvironment @@ -52,5 +53,10 @@ internal class EnvironmentPartials { @Suppress("NOTHING_TO_INLINE") internal inline fun quoteJsonString(string: String) = Json.quoteJsonString(string) + + @Suppress("NOTHING_TO_INLINE") + internal inline fun sortDescending(list: DoubleList) = list.sortDescending() + + } } diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt b/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt index 98edd9470..adeb843c6 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt @@ -129,6 +129,10 @@ public class DoubleList : IDoubleIterable { _items.sort(0, _size) } + internal fun sortDescending() { + _items.sortDescending(0, _size) + } + public fun shift(): Double { val d = _items[0] if (_items.size > 1) { diff --git a/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt b/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt index 1533343ba..cb8aa34c4 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt @@ -12,6 +12,8 @@ import alphaTab.core.DoubleDoubleArrayTuple import alphaTab.core.IArrayTuple import alphaTab.core.IDoubleDoubleArrayTuple import alphaTab.core.IRecord +import alphaTab.core.TestGlobals +import alphaTab.core.TestName import alphaTab.core.ecmaScript.Record import alphaTab.core.ecmaScript.Uint8Array import alphaTab.core.toInvariantString @@ -21,11 +23,13 @@ import com.beust.klaxon.JsonValue import com.beust.klaxon.Klaxon import com.beust.klaxon.KlaxonException import kotlinx.coroutines.CompletableDeferred +import org.junit.Assert import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream import java.io.OutputStream import java.io.OutputStreamWriter +import java.lang.reflect.Method import java.nio.file.Paths import kotlin.contracts.ExperimentalContracts import kotlin.reflect.KClass @@ -297,5 +301,35 @@ class TestPlatformPartials { null -> "" else -> o.javaClass.name } + + public fun findTestMethod(): Method { + val walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + var testMethod: Method? = null + walker.forEach { frame -> + if (testMethod == null) { + val method = frame.declaringClass.getDeclaredMethod( + frame.methodName, + *frame.methodType.parameterArray() + ) + + if (method.getAnnotation(org.junit.Test::class.java) != null) { + testMethod = method + } + } + } + + if (testMethod == null) { + Assert.fail("No information about current test available, cannot find test snapshot"); + } + + return testMethod!! + } + + internal val currentTestName: String + get() { + val testMethodInfo = findTestMethod() + val testName = testMethodInfo.getAnnotation(TestName::class.java)!!.name + return testName + } } } diff --git a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt index ca022e011..ea93e9529 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt @@ -192,33 +192,11 @@ class Expector(private val actual: T) { } } - private fun findTestMethod(): Method { - val walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) - var testMethod: Method? = null - walker.forEach { frame -> - if (testMethod == null) { - val method = frame.declaringClass.getDeclaredMethod( - frame.methodName, - *frame.methodType.parameterArray() - ) - - if (method.getAnnotation(org.junit.Test::class.java) != null) { - testMethod = method - } - } - } - - if (testMethod == null) { - Assert.fail("No information about current test available, cannot find test snapshot"); - } - - return testMethod!! - } @ExperimentalUnsignedTypes @ExperimentalContracts fun toMatchSnapshot(hint: String = "") { - val testMethodInfo = findTestMethod() + val testMethodInfo = TestPlatformPartials.findTestMethod() val file = testMethodInfo.getAnnotation(SnapshotFile::class.java)?.path if (file.isNullOrEmpty()) { Assert.fail("Missing SnapshotFile annotation with path to .snap file") diff --git a/packages/playground/alphatex-editor.ts b/packages/playground/alphatex-editor.ts index c03da1dc3..1e7c4b791 100644 --- a/packages/playground/alphatex-editor.ts +++ b/packages/playground/alphatex-editor.ts @@ -107,9 +107,26 @@ async function setupEditor(api: alphaTab.AlphaTabApi, element: HTMLElement) { function loadTex(tex: string) { const importer = new alphaTab.importer.AlphaTexImporter(); importer.initFromString(tex, api.settings); + importer.logErrors = true; let score: alphaTab.model.Score; try { score = importer.readScore(); + + const hasSystemsLayout = importer.scoreNode!.bars[0]?.metaData.some( + m => + m.tag.tag.text.toLocaleLowerCase() === 'defaultsystemslayout' || + m.tag.tag.text.toLocaleLowerCase() === 'systemslayout' || + (m.tag.tag.text === 'track' && + m.properties?.properties.some( + p => + p.property.text.toLowerCase() === 'defaultsystemslayout' || + p.property.text.toLowerCase() === 'systemslayout' + )) + ); + api.settings.display.systemsLayoutMode = hasSystemsLayout + ? alphaTab.SystemsLayoutMode.UseModelLayout + : alphaTab.SystemsLayoutMode.Automatic; + api.updateSettings(); } catch { return; } diff --git a/packages/playground/control.css b/packages/playground/control.css index e4bc65d11..ad79bd19e 100644 --- a/packages/playground/control.css +++ b/packages/playground/control.css @@ -22,6 +22,8 @@ @import url('bootstrap/dist/css/bootstrap.min.css'); +@import url('./select-handles.css'); + .at-cursor-bar { /* Defines the color of the bar background when a bar is played */ background: rgba(255, 242, 0, 0.25); @@ -520,5 +522,3 @@ input[type='range']::-moz-range-thumb { cursor: ew-resize !important; } - -@import url('./select-handles.css'); \ No newline at end of file diff --git a/packages/transpiler/src/kotlin/KotlinAstPrinter.ts b/packages/transpiler/src/kotlin/KotlinAstPrinter.ts index 665aa2969..5f77d5643 100644 --- a/packages/transpiler/src/kotlin/KotlinAstPrinter.ts +++ b/packages/transpiler/src/kotlin/KotlinAstPrinter.ts @@ -627,7 +627,8 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeType(d.type); const needsInitializer = - isAutoProperty && d.type.isNullable && d.parent!.nodeType !== cs.SyntaxKind.InterfaceDeclaration; + isAutoProperty && d.type.isNullable && d.parent!.nodeType !== cs.SyntaxKind.InterfaceDeclaration && + !d.isAbstract; let initializerWritten = false; if (d.initializer && !isLateInit) {