From 4d5ca5f597f47ee184d95ccac4076053c49f05ef Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 21 Dec 2025 11:43:47 +0100 Subject: [PATCH 01/10] fix: ensure we grow scaling with fixed systems --- packages/alphatab/src/rendering/layout/PageViewLayout.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/alphatab/src/rendering/layout/PageViewLayout.ts b/packages/alphatab/src/rendering/layout/PageViewLayout.ts index b73a53e28..7ed1fa45a 100644 --- a/packages/alphatab/src/rendering/layout/PageViewLayout.ts +++ b/packages/alphatab/src/rendering/layout/PageViewLayout.ts @@ -212,6 +212,7 @@ export class PageViewLayout extends ScoreLayout { if (barsPerRowActive) { for (let i: number = 0; i < this._systems.length; i++) { const system: StaffSystem = this._systems[i]; + system.width = system.computedWidth; this._fitSystem(system); y += this._paintSystem(system, oldHeight); } From 3e0e58176146c59d1e6f4e2315e897215bc3aaf6 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 21 Dec 2025 13:22:42 +0100 Subject: [PATCH 02/10] refactor: remove internal systems layout mode --- .../alphatab/src/rendering/BarRendererBase.ts | 23 +------ .../layout/HorizontalScreenLayout.ts | 15 +---- .../src/rendering/layout/PageViewLayout.ts | 25 ++----- .../src/rendering/layout/ScoreLayout.ts | 27 +------- .../src/rendering/staves/RenderStaff.ts | 67 +++---------------- 5 files changed, 22 insertions(+), 135 deletions(-) diff --git a/packages/alphatab/src/rendering/BarRendererBase.ts b/packages/alphatab/src/rendering/BarRendererBase.ts index 5726b4756..9c1d9dd1f 100644 --- a/packages/alphatab/src/rendering/BarRendererBase.ts +++ b/packages/alphatab/src/rendering/BarRendererBase.ts @@ -5,6 +5,7 @@ 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 { @@ -15,7 +16,6 @@ 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 { InternalSystemsLayoutMode } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { MultiBarRestBeatContainerGlyph } from '@coderline/alphatab/rendering/MultiBarRestBeatContainerGlyph'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; @@ -27,7 +27,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'; /** @@ -254,14 +253,6 @@ export class BarRendererBase { return this.staff!.system.staves.length > 1 ? this.bar.masterBar.displayScale : this.bar.displayScale; } - /** - * Gets the absolute width in which the bar should be displayed in case the model - * scale should be respected. - */ - public get barDisplayWidth(): number { - return this.staff!.system.staves.length > 1 ? this.bar.masterBar.displayWidth : this.bar.displayWidth; - } - protected wasFirstOfStaff: boolean = false; public get isFirstOfStaff(): boolean { @@ -329,18 +320,6 @@ export class BarRendererBase { this.width = Math.ceil(this._postBeatGlyphs.x + this._postBeatGlyphs.width); this.computedWidth = this.width; - // For cases like in the horizontal layout we need to set the fixed width early - // to have correct partials splitting. the proper alignment to this scale will happen - // later in the workflow. - const fixedBarWidth = this.barDisplayWidth; - if ( - fixedBarWidth > 0 && - this.scoreRenderer.layout!.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithWidths - ) { - this.width = fixedBarWidth; - this.computedWidth = fixedBarWidth; - } - this.topEffects.sizeAndAlignEffectBands(); this.bottomEffects.sizeAndAlignEffectBands(); this._registerStaffOverflow(); diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index aacf95941..769dfe13a 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -1,11 +1,11 @@ +import { Logger } from '@coderline/alphatab/Logger'; import type { MasterBar } from '@coderline/alphatab/model/MasterBar'; import type { Score } from '@coderline/alphatab/model/Score'; import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import { InternalSystemsLayoutMode, ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; +import { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; +import type { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; -import { Logger } from '@coderline/alphatab/Logger'; -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; /** * @internal @@ -44,15 +44,6 @@ export class HorizontalScreenLayout extends ScoreLayout { } protected doLayoutAndRender(): void { - switch (this.renderer.settings.display.systemsLayoutMode) { - case SystemsLayoutMode.Automatic: - this.systemsLayoutMode = InternalSystemsLayoutMode.Automatic; - break; - case SystemsLayoutMode.UseModelLayout: - this.systemsLayoutMode = InternalSystemsLayoutMode.FromModelWithWidths; - break; - } - const score: Score = this.renderer.score!; let startIndex: number = this.renderer.settings.display.startBar; diff --git a/packages/alphatab/src/rendering/layout/PageViewLayout.ts b/packages/alphatab/src/rendering/layout/PageViewLayout.ts index 7ed1fa45a..14a364434 100644 --- a/packages/alphatab/src/rendering/layout/PageViewLayout.ts +++ b/packages/alphatab/src/rendering/layout/PageViewLayout.ts @@ -1,13 +1,13 @@ +import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; +import { Logger } from '@coderline/alphatab/Logger'; +import { ScoreSubElement } from '@coderline/alphatab/model/Score'; import { type ICanvas, TextAlign } from '@coderline/alphatab/platform/ICanvas'; import type { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { InternalSystemsLayoutMode, ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; +import { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import type { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; -import { Logger } from '@coderline/alphatab/Logger'; -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; -import { ScoreSubElement } from '@coderline/alphatab/model/Score'; /** * This layout arranges the bars into a fixed width and dynamic height region. @@ -23,15 +23,6 @@ export class PageViewLayout extends ScoreLayout { } protected doLayoutAndRender(): void { - switch (this.renderer.settings.display.systemsLayoutMode) { - case SystemsLayoutMode.Automatic: - this.systemsLayoutMode = InternalSystemsLayoutMode.Automatic; - break; - case SystemsLayoutMode.UseModelLayout: - this.systemsLayoutMode = InternalSystemsLayoutMode.FromModelWithScale; - break; - } - let y: number = this.pagePadding![1]; this.width = this.renderer.width; this._allMasterBarRenderers = []; @@ -207,7 +198,7 @@ export class PageViewLayout extends ScoreLayout { // if we have a fixed number of bars per row, we only need to refit them. const barsPerRowActive = this.renderer.settings.display.barsPerRow > 0 || - this.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithScale; + this.renderer.settings.display.systemsLayoutMode === SystemsLayoutMode.UseModelLayout; if (barsPerRowActive) { for (let i: number = 0; i < this._systems.length; i++) { @@ -241,10 +232,9 @@ export class PageViewLayout extends ScoreLayout { // move to next bar currentIndex++; - if(this._needsLineBreak(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); @@ -341,8 +331,7 @@ export class PageViewLayout extends ScoreLayout { private _getBarsPerSystem(rowIndex: number) { let barsPerRow: number = this.renderer.settings.display.barsPerRow; - - if (this.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithScale) { + if (this.renderer.settings.display.systemsLayoutMode === SystemsLayoutMode.UseModelLayout) { let defaultSystemsLayout: number; let systemsLayout: number[]; if (this.renderer.tracks!.length > 1) { diff --git a/packages/alphatab/src/rendering/layout/ScoreLayout.ts b/packages/alphatab/src/rendering/layout/ScoreLayout.ts index 8bc3c5cad..aec648903 100644 --- a/packages/alphatab/src/rendering/layout/ScoreLayout.ts +++ b/packages/alphatab/src/rendering/layout/ScoreLayout.ts @@ -37,27 +37,6 @@ class LazyPartial { } } -/** - * Lists the different modes in which the staves and systems are arranged. - * @internal - */ -export enum InternalSystemsLayoutMode { - /** - * Use the automatic alignment system provided by alphaTab (default) - */ - Automatic = 0, - - /** - * Use the relative scaling information stored in the score model. - */ - FromModelWithScale = 1, - - /** - * Use the absolute size information stored in the score model. - */ - FromModelWithWidths = 2 -} - /** * This is the base class for creating new layouting engines for the score renderer. * @internal @@ -86,8 +65,6 @@ export abstract class ScoreLayout { protected chordDiagrams: ChordDiagramContainerGlyph | null = null; protected tuningGlyph: TuningContainerGlyph | null = null; - public systemsLayoutMode: InternalSystemsLayoutMode = InternalSystemsLayoutMode.Automatic; - public constructor(renderer: ScoreRenderer) { this.renderer = renderer; } @@ -108,7 +85,7 @@ export abstract class ScoreLayout { this._lazyPartials.clear(); this.slurRegistry.clear(); this._barRendererLookup.clear(); - + this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile)!; const score: Score = this.renderer.score!; @@ -374,7 +351,7 @@ export abstract class ScoreLayout { public lastBarIndex: number = 0; - protected createEmptyStaffSystem(index:number): StaffSystem { + protected createEmptyStaffSystem(index: number): StaffSystem { const system: StaffSystem = new StaffSystem(this); system.index = index; const allFactories = Environment.defaultRenderers; diff --git a/packages/alphatab/src/rendering/staves/RenderStaff.ts b/packages/alphatab/src/rendering/staves/RenderStaff.ts index 726567d17..441ede018 100644 --- a/packages/alphatab/src/rendering/staves/RenderStaff.ts +++ b/packages/alphatab/src/rendering/staves/RenderStaff.ts @@ -7,7 +7,6 @@ import { type EffectBandInfo, EffectBandMode } from '@coderline/alphatab/rendering/BarRendererFactory'; -import { InternalSystemsLayoutMode } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; import type { StaffTrackGroup } from '@coderline/alphatab/rendering/staves/StaffTrackGroup'; @@ -162,16 +161,6 @@ export class RenderStaff { renderer.layoutingInfo = layoutingInfo; renderer.doLayout(); - // For cases like in the horizontal layout we need to set the fixed width early - // to have correct partials splitting - const barDisplayWidth = renderer.barDisplayWidth; - if ( - barDisplayWidth > 0 && - this.system.layout.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithWidths - ) { - renderer.width = barDisplayWidth; - } - this.barRenderers.push(renderer); this.system.layout.registerBarRenderer(this.staffId, renderer); if (bar.isEmpty || bar.isRestOnly) { @@ -204,53 +193,15 @@ export class RenderStaff { const topOverflow: number = this.topOverflow; let x = 0; - switch (this.system.layout.systemsLayoutMode) { - case InternalSystemsLayoutMode.Automatic: - // Note: here we could do some "intelligent" distribution of - // the space over the bar renderers, for now we evenly apply the space to all bars - const difference: number = width - this.system.computedWidth; - const spacePerBar: number = difference / this.barRenderers.length; - for (const renderer of this.barRenderers) { - renderer.x = x; - renderer.y = this.topPadding + topOverflow; - - const actualBarWidth = renderer.computedWidth + spacePerBar; - renderer.scaleToWidth(actualBarWidth); - x += renderer.width; - } - break; - case InternalSystemsLayoutMode.FromModelWithScale: - // each bar holds a percentual size where the sum of all scales make the width. - // hence we can calculate the width accordingly by calculating how big each column needs to be percentual. - - width -= this.system.accoladeWidth; - const totalScale = this.system.totalBarDisplayScale; - - for (const renderer of this.barRenderers) { - renderer.x = x; - renderer.y = this.topPadding + topOverflow; - - const actualBarWidth = (renderer.barDisplayScale * width) / totalScale; - renderer.scaleToWidth(actualBarWidth); - - x += renderer.width; - } - - break; - case InternalSystemsLayoutMode.FromModelWithWidths: - for (const renderer of this.barRenderers) { - renderer.x = x; - renderer.y = this.topPadding + topOverflow; - const displayWidth = renderer.barDisplayWidth; - if (displayWidth > 0) { - renderer.scaleToWidth(displayWidth); - } else { - renderer.scaleToWidth(renderer.computedWidth); - } - - x += renderer.width; - } - break; + // scale the bars by keeping their respective ratio size + const scaleRatio = width / this.system.computedWidth; + for (const renderer of this.barRenderers) { + renderer.x = x; + renderer.y = this.topPadding + topOverflow; + + const actualBarWidth = renderer.computedWidth * scaleRatio; + renderer.scaleToWidth(actualBarWidth); + x += renderer.width; } } From 72a7d1bbba8d85d16dda23ecc3c55f62d7caeb8d Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 21 Dec 2025 13:23:53 +0100 Subject: [PATCH 03/10] feat: always use fixed bar widths in horizontal layout --- .../layout/HorizontalScreenLayout.ts | 91 +++++++++++-------- .../src/rendering/staves/RenderStaff.ts | 12 +++ .../src/rendering/staves/StaffSystem.ts | 10 ++ 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index 769dfe13a..72c33422b 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -14,6 +14,7 @@ export class HorizontalScreenLayoutPartialInfo { public x: number = 0; public width: number = 0; public masterBars: MasterBar[] = []; + public results: MasterBarsRenderers[] = []; } /** @@ -65,7 +66,6 @@ export class HorizontalScreenLayout extends ScoreLayout { const countPerPartial: number = this.renderer.settings.display.barCountPerPartial; const partials: HorizontalScreenLayoutPartialInfo[] = []; let currentPartial: HorizontalScreenLayoutPartialInfo = new HorizontalScreenLayoutPartialInfo(); - let renderX = 0; while (currentBarIndex <= endBarIndex) { const multiBarRestInfo = this.multiBarRestInfo; const additionalMultiBarsRestBarIndices: number[] | null = @@ -79,53 +79,27 @@ export class HorizontalScreenLayout extends ScoreLayout { additionalMultiBarsRestBarIndices ); - // if we detect that the new renderer is linked to the previous - // renderer, we need to put it into the previous partial - if (currentPartial.masterBars.length === 0 && result.isLinkedToPrevious && partials.length > 0) { - const previousPartial: HorizontalScreenLayoutPartialInfo = partials[partials.length - 1]; - previousPartial.masterBars.push(score.masterBars[currentBarIndex]); - previousPartial.width += result.width; - renderX += result.width; - currentPartial.x += renderX; - } else { - currentPartial.masterBars.push(score.masterBars[currentBarIndex]); - currentPartial.width += result.width; - // no targetPartial here because previous partials already handled this code - if (currentPartial.masterBars.length >= countPerPartial) { - if (partials.length === 0) { - // respect accolade and on first partial - currentPartial.width += this._system.accoladeWidth + this.pagePadding![0]; - } - renderX += currentPartial.width; - partials.push(currentPartial); - Logger.debug( - this.name, - `Finished partial from bar ${currentPartial.masterBars[0].index} to ${currentPartial.masterBars[currentPartial.masterBars.length - 1].index}`, - null - ); - currentPartial = new HorizontalScreenLayoutPartialInfo(); - currentPartial.x = renderX; - } + // complete partial if its full and we are not linked + if (currentPartial.masterBars.length >= countPerPartial && !result.isLinkedToPrevious) { + currentPartial = this._completePartial(partials, currentPartial); } + this._scaleBars(result); + + currentPartial.results.push(result); + currentPartial.masterBars.push(score.masterBars[currentBarIndex]); + currentPartial.width += result.width; currentBarIndex++; } + // don't miss the last partial if not empty if (currentPartial.masterBars.length > 0) { - if (partials.length === 0) { - currentPartial.width += this._system.accoladeWidth + this.pagePadding![0]; - } - partials.push(currentPartial); - Logger.debug( - this.name, - `Finished partial from bar ${currentPartial.masterBars[0].index} to ${currentPartial.masterBars[currentPartial.masterBars.length - 1].index}`, - null - ); + this._completePartial(partials, currentPartial); } this._finalizeStaffSystem(); this.height = Math.floor(this._system.y + this._system.height); - this.width = (this._system.x + this._system.width + this.pagePadding![2]); + this.width = this._system.x + this._system.width + this.pagePadding![2]; currentBarIndex = 0; let x = 0; @@ -181,8 +155,47 @@ export class HorizontalScreenLayout extends ScoreLayout { this.height *= this.renderer.settings.display.scale; } + private _scaleBars(result: MasterBarsRenderers) { + result.width = 0; + this._system!.width -= result.width; + for (const r of result.renderers) { + const barDisplayWidth = + r.staff!.system.staves.length > 1 ? r.bar.masterBar.displayWidth : r.bar.displayWidth; + if (barDisplayWidth > 0) { + r.scaleToWidth(barDisplayWidth); + } + const w = r.x + r.width; + if (w > result.width) { + result.width = w; + } + } + this._system!.width += result.width; + } + + private _completePartial( + partials: HorizontalScreenLayoutPartialInfo[], + currentPartial: HorizontalScreenLayoutPartialInfo + ) { + if (partials.length === 0) { + // respect accolade and on first partial + currentPartial.width += this._system!.accoladeWidth + this.pagePadding![0]; + } + + partials.push(currentPartial); + Logger.debug( + this.name, + `Finished partial from bar ${currentPartial.masterBars[0].index} to ${currentPartial.masterBars[currentPartial.masterBars.length - 1].index}`, + null + ); + + // start new partial + const newPartial = new HorizontalScreenLayoutPartialInfo(); + newPartial.x = currentPartial.x + currentPartial.width; + return newPartial; + } + private _finalizeStaffSystem() { - this._system!.scaleToWidth(this._system!.width); + this._system!.alignRenderers(); this._system!.finalizeSystem(); } } diff --git a/packages/alphatab/src/rendering/staves/RenderStaff.ts b/packages/alphatab/src/rendering/staves/RenderStaff.ts index 441ede018..911970034 100644 --- a/packages/alphatab/src/rendering/staves/RenderStaff.ts +++ b/packages/alphatab/src/rendering/staves/RenderStaff.ts @@ -188,6 +188,18 @@ export class RenderStaff { return lastBar; } + public alignRenderers() { + this._sharedLayoutData = new Map(); + const topOverflow: number = this.topOverflow; + let x = 0; + for (const renderer of this.barRenderers) { + renderer.x = x; + renderer.y = this.topPadding + topOverflow; + x += renderer.width; + } + return x; + } + public scaleToWidth(width: number): void { this._sharedLayoutData = new Map(); const topOverflow: number = this.topOverflow; diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 3f7c3042c..281b697b0 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -588,6 +588,16 @@ export class StaffSystem { this.width = width; } + public alignRenderers(): void { + this.width = 0; + for (const s of this.allStaves) { + const w = s.alignRenderers(); + if (w > this.width) { + this.width = w; + } + } + } + public paint(cx: number, cy: number, canvas: ICanvas): void { // const c = canvas.color; // canvas.color = Color.random(255); From 7af530726c79100faf5c5f441bf7930c62245ea9 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 21 Dec 2025 14:40:06 +0100 Subject: [PATCH 04/10] feat: add parchment layout --- packages/alphatab/src/DisplaySettings.ts | 1 + packages/alphatab/src/Environment.ts | 7 + packages/alphatab/src/LayoutMode.ts | 39 +- packages/alphatab/src/model/ModelUtils.ts | 15 + .../alphatab/src/rendering/BarRendererBase.ts | 8 - .../src/rendering/layout/PageViewLayout.ts | 417 +----------------- .../src/rendering/layout/ParchmentLayout.ts | 17 + .../rendering/layout/VerticalLayoutBase.ts | 403 +++++++++++++++++ .../src/rendering/staves/RenderStaff.ts | 4 +- .../src/rendering/staves/StaffSystem.ts | 56 ++- packages/playground/alphatex-editor.ts | 15 - packages/playground/control-template.html | 5 +- packages/playground/control.ts | 3 + 13 files changed, 529 insertions(+), 461 deletions(-) create mode 100644 packages/alphatab/src/rendering/layout/ParchmentLayout.ts create mode 100644 packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts diff --git a/packages/alphatab/src/DisplaySettings.ts b/packages/alphatab/src/DisplaySettings.ts index 5e298eb9a..581cdcdba 100644 --- a/packages/alphatab/src/DisplaySettings.ts +++ b/packages/alphatab/src/DisplaySettings.ts @@ -396,6 +396,7 @@ export class DisplaySettings { * * * Comparing files against each other (top/bottom comparison) * * Aligning the playback of multiple files on one screen assuming the same tempo (e.g. one file per track). + * @deprecated Use the {@link LayoutMode.Parchment} to display a music sheet respecting the systems layout. */ public systemsLayoutMode: SystemsLayoutMode = SystemsLayoutMode.Automatic; } diff --git a/packages/alphatab/src/Environment.ts b/packages/alphatab/src/Environment.ts index 20c5a5371..ca89c61cd 100644 --- a/packages/alphatab/src/Environment.ts +++ b/packages/alphatab/src/Environment.ts @@ -63,6 +63,7 @@ import { WideBeatVibratoEffectInfo } from '@coderline/alphatab/rendering/effects import { WideNoteVibratoEffectInfo } from '@coderline/alphatab/rendering/effects/WideNoteVibratoEffectInfo'; import { HorizontalScreenLayout } from '@coderline/alphatab/rendering/layout/HorizontalScreenLayout'; import { PageViewLayout } from '@coderline/alphatab/rendering/layout/PageViewLayout'; +import { ParchmentLayout } from '@coderline/alphatab/rendering/layout/ParchmentLayout'; import type { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; import { NumberedBarRendererFactory } from '@coderline/alphatab/rendering/NumberedBarRendererFactory'; @@ -623,6 +624,12 @@ export class Environment { return new HorizontalScreenLayout(r); }) ); + engines.set( + LayoutMode.Parchment, + new LayoutEngineFactory(true, r => { + return new ParchmentLayout(r); + }) + ); return engines; } diff --git a/packages/alphatab/src/LayoutMode.ts b/packages/alphatab/src/LayoutMode.ts index f690a2963..fc9fa2274 100644 --- a/packages/alphatab/src/LayoutMode.ts +++ b/packages/alphatab/src/LayoutMode.ts @@ -9,6 +9,43 @@ export enum LayoutMode { Page = 0, /** * Bars are aligned horizontally in [one horizontally endless system (row)](https://alphatab.net/docs/showcase/layouts#horizontal-layout) + * + * alphaTab holds following information in the data model and developers can change those values (e.g. by tapping into the `scoreLoaded`) event. + * These widths are respected when using this layout. + * + * **Used when single tracks are rendered:** + * + * * `score.tracks[index].staves[index].bars[index].displayWidth` - The absolute size of this bar when displayed. + * + * **Used when multiple tracks are rendered:** + * + * * `score.masterBars[index].displayWidth` - Like the `displayWidth` on bar level. */ - Horizontal = 1 + Horizontal = 1, + /** + * The bars are aligned in an [vertically endless page-style fashion](https://alphatab.net/docs/showcase/layouts#parchment) + * respecting the configured systems layout. + * + * The parchment layout uses the `systemsLayout` and `defaultSystemsLayout` to decide how many bars go into a single system (row). + * Additionally when sizing the bars within the system the `displayScale` is used. This scale is rather a ratio than an absolute percentage value but percentages work also: + * + * ![Parchment Layout](https://alphatab.net/img/reference/property/systems-layout-page-examples.png) + * + * File formats like Guitar Pro embed information about the layout in the file and alphaTab can read and use this information. + * + * alphaTab holds following information in the data model and developers can change those values (e.g. by tapping into the `scoreLoaded`) event. + * + * **Used when single tracks are rendered:** + * + * * `score.tracks[index].systemsLayout` - An array of numbers describing how many bars should be placed within each system (row). + * * `score.tracks[index].defaultSystemsLayout` - The number of bars to place in a system (row) when no value is defined in the `systemsLayout`. + * * `score.tracks[index].staves[index].bars[index].displayScale` - The relative size of this bar in the system it is placed. Note that this is not directly a percentage value. e.g. if there are 3 bars and all define scale 1, they are sized evenly. + * + * **Used when multiple tracks are rendered:** + * + * * `score.systemsLayout` - Like the `systemsLayout` on track level. + * * `score.defaultSystemsLayout` - Like the `defaultSystemsLayout` on track level. + * * `score.masterBars[index].displayScale` - Like the `displayScale` on bar level. + */ + Parchment = 2 } diff --git a/packages/alphatab/src/model/ModelUtils.ts b/packages/alphatab/src/model/ModelUtils.ts index 0e0302b32..a3a0d89c5 100644 --- a/packages/alphatab/src/model/ModelUtils.ts +++ b/packages/alphatab/src/model/ModelUtils.ts @@ -944,4 +944,19 @@ export class ModelUtils { } return a > b ? a : b; } + + public static getSystemLayout(score: Score, systemIndex: number, displayedTracks: Track[]) { + let defaultSystemsLayout: number; + let systemsLayout: number[]; + if (displayedTracks.length === 1) { + defaultSystemsLayout = displayedTracks[0].defaultSystemsLayout; + systemsLayout = displayedTracks[0].systemsLayout; + } else { + // multi track applies + defaultSystemsLayout = score.defaultSystemsLayout; + systemsLayout = score.systemsLayout; + } + + return systemIndex < systemsLayout.length ? systemsLayout[systemIndex] : defaultSystemsLayout; + } } diff --git a/packages/alphatab/src/rendering/BarRendererBase.ts b/packages/alphatab/src/rendering/BarRendererBase.ts index 9c1d9dd1f..e7d037647 100644 --- a/packages/alphatab/src/rendering/BarRendererBase.ts +++ b/packages/alphatab/src/rendering/BarRendererBase.ts @@ -245,14 +245,6 @@ export class BarRendererBase { return this.scoreRenderer.settings; } - /** - * Gets the scale with which the bar should be displayed in case the model - * scale should be respected. - */ - public get barDisplayScale(): number { - return this.staff!.system.staves.length > 1 ? this.bar.masterBar.displayScale : this.bar.displayScale; - } - protected wasFirstOfStaff: boolean = false; public get isFirstOfStaff(): boolean { diff --git a/packages/alphatab/src/rendering/layout/PageViewLayout.ts b/packages/alphatab/src/rendering/layout/PageViewLayout.ts index 14a364434..8c3288036 100644 --- a/packages/alphatab/src/rendering/layout/PageViewLayout.ts +++ b/packages/alphatab/src/rendering/layout/PageViewLayout.ts @@ -1,428 +1,23 @@ import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; -import { Logger } from '@coderline/alphatab/Logger'; -import { ScoreSubElement } from '@coderline/alphatab/model/Score'; -import { type ICanvas, TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import type { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; -import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; -import type { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; -import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { VerticalLayoutBase } from '@coderline/alphatab/rendering/layout/VerticalLayoutBase'; /** * This layout arranges the bars into a fixed width and dynamic height region. * @internal */ -export class PageViewLayout extends ScoreLayout { - private _systems: StaffSystem[] = []; - private _allMasterBarRenderers: MasterBarsRenderers[] = []; - private _barsFromPreviousSystem: MasterBarsRenderers[] = []; - +export class PageViewLayout extends VerticalLayoutBase { public get name(): string { return 'PageView'; } - protected doLayoutAndRender(): void { - let y: number = this.pagePadding![1]; - this.width = this.renderer.width; - this._allMasterBarRenderers = []; - // - // 1. Score Info - y = this._layoutAndRenderScoreInfo(y, -1); - // - // 2. Tunings - y = this._layoutAndRenderTunings(y, -1); - // - // 3. Chord Diagrms - y = this._layoutAndRenderChordDiagrams(y, -1); - // - // 4. One result per StaffSystem - y = this._layoutAndRenderScore(y); - - y = this.layoutAndRenderBottomScoreInfo(y); - - y = this.layoutAndRenderAnnotation(y); - - this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; - } - - public get supportsResize(): boolean { - return true; - } - - public get firstBarX(): number { - let x = this.pagePadding![0]; - if (this._systems.length > 0) { - x += this._systems[0].accoladeWidth; - } - return x; - } - - public doResize(): void { - let y: number = this.pagePadding![1]; - this.width = this.renderer.width; - const oldHeight: number = this.height; - // - // 1. Score Info - y = this._layoutAndRenderScoreInfo(y, oldHeight); - // - // 2. Tunings - y = this._layoutAndRenderTunings(y, oldHeight); - // - // 3. Chord Digrams - y = this._layoutAndRenderChordDiagrams(y, oldHeight); - // - // 4. One result per StaffSystem - y = this._resizeAndRenderScore(y, oldHeight); - - y = this.layoutAndRenderBottomScoreInfo(y); - - y = this.layoutAndRenderAnnotation(y); - - this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; - } - - private _layoutAndRenderTunings(y: number, totalHeight: number = -1): number { - if (!this.tuningGlyph) { - return y; - } - - const res: RenderingResources = this.renderer.settings.display.resources; - this.tuningGlyph.x = this.pagePadding![0]; - this.tuningGlyph.width = this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; - this.tuningGlyph.doLayout(); - - const tuningHeight = Math.round(this.tuningGlyph.height); - - const e = new RenderFinishedEventArgs(); - e.x = 0; - e.y = y; - e.width = this.scaledWidth; - e.height = tuningHeight; - e.totalWidth = this.scaledWidth; - e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; - - this.registerPartial(e, (canvas: ICanvas) => { - canvas.color = res.scoreInfoColor; - canvas.textAlign = TextAlign.Center; - this.tuningGlyph!.paint(0, 0, canvas); - }); - - return y + tuningHeight; - } - - private _layoutAndRenderChordDiagrams(y: number, totalHeight: number = -1): number { - if (!this.chordDiagrams) { - return y; - } - const res: RenderingResources = this.renderer.settings.display.resources; - this.chordDiagrams.x = this.pagePadding![0]; - this.chordDiagrams.width = this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; - this.chordDiagrams.doLayout(); - - const diagramHeight = Math.round(this.chordDiagrams.height); - - const e = new RenderFinishedEventArgs(); - e.x = 0; - e.y = y; - e.width = this.scaledWidth; - e.height = diagramHeight; - e.totalWidth = this.scaledWidth; - e.totalHeight = totalHeight < 0 ? y + diagramHeight : totalHeight; - - this.registerPartial(e, (canvas: ICanvas) => { - canvas.color = res.scoreInfoColor; - canvas.textAlign = TextAlign.Center; - this.chordDiagrams!.paint(0, 0, canvas); - }); - - return y + diagramHeight; - } - - private _layoutAndRenderScoreInfo(y: number, totalHeight: number = -1): number { - Logger.debug(this.name, 'Layouting score info'); - - const e = new RenderFinishedEventArgs(); - e.x = 0; - e.y = y; - - let infoHeight = 0; - - const res: RenderingResources = this.renderer.settings.display.resources; - - const scoreInfoGlyphs: TextGlyph[] = []; - - for (const [scoreElement, _notationElement] of ScoreLayout.headerElements.value) { - if (this.headerGlyphs.has(scoreElement)) { - const glyph: TextGlyph = this.headerGlyphs.get(scoreElement)!; - glyph.y = infoHeight; - this.alignScoreInfoGlyph(glyph); - - let lineHeight = glyph.font.size; - - // words and music on same line if not aligned on same side - if (scoreElement === ScoreSubElement.Words) { - if (this.headerGlyphs.has(ScoreSubElement.Music)) { - const musicGlyph = this.headerGlyphs.get(ScoreSubElement.Music)!; - if (musicGlyph.textAlign !== glyph.textAlign) { - lineHeight = 0; - } - } - } - - infoHeight += lineHeight; - - scoreInfoGlyphs.push(glyph); - } - } - - if (scoreInfoGlyphs.length > 0) { - infoHeight = Math.floor(infoHeight + 17); - e.width = this.scaledWidth; - e.height = infoHeight; - e.totalWidth = this.scaledWidth; - e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; - this.registerPartial(e, (canvas: ICanvas) => { - canvas.color = res.scoreInfoColor; - canvas.textAlign = TextAlign.Center; - for (const g of scoreInfoGlyphs) { - g.paint(0, 0, canvas); - } - }); - } - - return y + infoHeight; - } - - private _resizeAndRenderScore(y: number, oldHeight: number): number { - // if we have a fixed number of bars per row, we only need to refit them. - const barsPerRowActive = - this.renderer.settings.display.barsPerRow > 0 || - this.renderer.settings.display.systemsLayoutMode === SystemsLayoutMode.UseModelLayout; - - if (barsPerRowActive) { - for (let i: number = 0; i < this._systems.length; i++) { - const system: StaffSystem = this._systems[i]; - system.width = system.computedWidth; - this._fitSystem(system); - y += this._paintSystem(system, oldHeight); - } - } else { - // clear out staves during re-layout, this info is outdated during - // re-layout of the bars - for (const r of this._allMasterBarRenderers) { - for (const b of r.renderers) { - b.afterReverted(); - } - } - - this._systems = []; - let currentIndex: number = 0; - const maxWidth: number = this._maxWidth; - let system: StaffSystem = this.createEmptyStaffSystem(this._systems.length); - system.x = this.pagePadding![0]; - system.y = y; - while (currentIndex < this._allMasterBarRenderers.length) { - // 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 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); - while (renderers && !renderers.canWrap && system.masterBarsRenderers.length > 1) { - renderers = system.revertLastBar(); - currentIndex--; - } - // 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); - y += this._paintSystem(system, oldHeight); - // note: we do not increase currentIndex here to have it added to the next system - system = this.createEmptyStaffSystem(this._systems.length); - system.x = this.pagePadding![0]; - system.y = y; - } - } - system.isLast = this.lastBarIndex === system.lastBarIndex; - // don't forget to finish the last system - this._fitSystem(system); - y += this._paintSystem(system, oldHeight); - } - return y; - } - - private _layoutAndRenderScore(y: number): number { - const startIndex: number = this.firstBarIndex; - let currentBarIndex: number = startIndex; - const endBarIndex: number = this.lastBarIndex; - - this._systems = []; - while (currentBarIndex <= endBarIndex) { - // create system and align set proper coordinates - const system: StaffSystem = this._createStaffSystem(currentBarIndex, endBarIndex); - this._systems.push(system); - system.x = this.pagePadding![0]; - system.y = y; - currentBarIndex = system.lastBarIndex + 1; - // finalize system (sizing etc). - this._fitSystem(system); - Logger.debug( - this.name, - `Rendering partial from bar ${system.firstBarIndex} to ${system.lastBarIndex}`, - null - ); - y += this._paintSystem(system, y); - } - return y; - } - - private _paintSystem(system: StaffSystem, totalHeight: number): number { - // paint into canvas - const height: number = Math.floor(system.height); - - const args: RenderFinishedEventArgs = new RenderFinishedEventArgs(); - args.x = 0; - args.y = system.y; - args.totalWidth = this.scaledWidth; - args.totalHeight = totalHeight; - args.width = this.scaledWidth; - args.height = height; - args.firstMasterBarIndex = system.firstBarIndex; - args.lastMasterBarIndex = system.lastBarIndex; - - system.buildBoundingsLookup(0, 0); - this.registerPartial(args, canvas => { - this.renderer.canvas!.color = this.renderer.settings.display.resources.mainGlyphColor; - this.renderer.canvas!.textAlign = TextAlign.Left; - // NOTE: we use this negation trick to make the system paint itself to 0/0 coordinates - // since we use partial drawing - system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas); - }); - - // calculate coordinates for next system - return height; - } - - /** - * Realignes the bars in this line according to the available space - */ - private _fitSystem(system: StaffSystem): void { - if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) { - system.scaleToWidth(this._maxWidth); - } else { - system.scaleToWidth(system.width); - } - system.finalizeSystem(); - } - - private _getBarsPerSystem(rowIndex: number) { + protected override getBarsPerSystem(systemIndex: number) { let barsPerRow: number = this.renderer.settings.display.barsPerRow; - if (this.renderer.settings.display.systemsLayoutMode === SystemsLayoutMode.UseModelLayout) { - let defaultSystemsLayout: number; - let systemsLayout: number[]; - if (this.renderer.tracks!.length > 1) { - // multi track applies - defaultSystemsLayout = this.renderer.score!.defaultSystemsLayout; - systemsLayout = this.renderer.score!.systemsLayout; - } else { - defaultSystemsLayout = this.renderer.tracks![0].defaultSystemsLayout; - systemsLayout = this.renderer.tracks![0].systemsLayout; - } - barsPerRow = rowIndex < systemsLayout.length ? systemsLayout[rowIndex] : defaultSystemsLayout; + if (this.renderer.settings.display.systemsLayoutMode === SystemsLayoutMode.UseModelLayout) { + barsPerRow = ModelUtils.getSystemLayout(this.renderer.score!, systemIndex, this.renderer.tracks!); } return barsPerRow; } - - private _createStaffSystem(currentBarIndex: number, endIndex: number): StaffSystem { - const system: StaffSystem = this.createEmptyStaffSystem(this._systems.length); - const barsPerRow: number = this._getBarsPerSystem(system.index); - const maxWidth: number = this._maxWidth; - const end: number = endIndex + 1; - - let barIndex = currentBarIndex; - while (barIndex < end) { - if (this._barsFromPreviousSystem.length > 0) { - for (const renderer of this._barsFromPreviousSystem) { - system.addMasterBarRenderers(this.renderer.tracks!, renderer); - barIndex = renderer.lastMasterBarIndex; - } - } else { - const multiBarRestInfo = this.multiBarRestInfo; - const additionalMultiBarsRestBarIndices: number[] | null = - multiBarRestInfo !== null && multiBarRestInfo.has(barIndex) - ? multiBarRestInfo.get(barIndex)! - : null; - - const renderers = system.addBars(this.renderer.tracks!, barIndex, additionalMultiBarsRestBarIndices); - this._allMasterBarRenderers.push(renderers); - barIndex = renderers.lastMasterBarIndex; - } - this._barsFromPreviousSystem = []; - let systemIsFull: boolean = false; - // can bar placed in this line? - if (barsPerRow === -1 && system.width >= maxWidth && system.masterBarsRenderers.length !== 0) { - systemIsFull = true; - } else if (system.masterBarsRenderers.length === barsPerRow + 1) { - systemIsFull = true; - } - if (systemIsFull) { - let reverted = system.revertLastBar(); - if (reverted) { - this._barsFromPreviousSystem.push(reverted); - while (reverted && !reverted.canWrap && system.masterBarsRenderers.length > 1) { - reverted = system.revertLastBar(); - if (reverted) { - this._barsFromPreviousSystem.push(reverted); - } - } - } - system.isFull = true; - system.isLast = false; - this._barsFromPreviousSystem.reverse(); - return system; - } - if (this._needsLineBreak(barIndex)) { - system.isFull = true; - system.isLast = false; - return system; - } - system.x = 0; - barIndex++; - } - system.isLast = endIndex === system.lastBarIndex; - 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/layout/ParchmentLayout.ts b/packages/alphatab/src/rendering/layout/ParchmentLayout.ts new file mode 100644 index 000000000..a4926479d --- /dev/null +++ b/packages/alphatab/src/rendering/layout/ParchmentLayout.ts @@ -0,0 +1,17 @@ +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { VerticalLayoutBase } from '@coderline/alphatab/rendering/layout/VerticalLayoutBase'; + +/** + * This layout arranges the bars into a fixed width and dynamic height region + * respecting the systems layout specified in the data model. + * @internal + */ +export class ParchmentLayout extends VerticalLayoutBase { + public get name(): string { + return 'Parchment'; + } + + protected override getBarsPerSystem(systemIndex: number) { + return ModelUtils.getSystemLayout(this.renderer.score!, systemIndex, this.renderer.tracks!); + } +} diff --git a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts new file mode 100644 index 000000000..ffb3dd398 --- /dev/null +++ b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts @@ -0,0 +1,403 @@ +import { Logger } from '@coderline/alphatab/Logger'; +import { ScoreSubElement } from '@coderline/alphatab/model/Score'; +import { type ICanvas, TextAlign } from '@coderline/alphatab/platform/ICanvas'; +import type { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; +import { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; +import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; +import type { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; +import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; +import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; + +/** + * Base layout for page and parchment style layouts where we have an endless + * vertical page with fitted systems. + * @internal + */ +export abstract class VerticalLayoutBase extends ScoreLayout { + private _systems: StaffSystem[] = []; + private _allMasterBarRenderers: MasterBarsRenderers[] = []; + private _barsFromPreviousSystem: MasterBarsRenderers[] = []; + + protected doLayoutAndRender(): void { + let y: number = this.pagePadding![1]; + this.width = this.renderer.width; + this._allMasterBarRenderers = []; + // + // 1. Score Info + y = this._layoutAndRenderScoreInfo(y, -1); + // + // 2. Tunings + y = this._layoutAndRenderTunings(y, -1); + // + // 3. Chord Diagrms + y = this._layoutAndRenderChordDiagrams(y, -1); + // + // 4. One result per StaffSystem + y = this._layoutAndRenderScore(y); + + y = this.layoutAndRenderBottomScoreInfo(y); + + y = this.layoutAndRenderAnnotation(y); + + this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; + } + + public get supportsResize(): boolean { + return true; + } + + public get firstBarX(): number { + let x = this.pagePadding![0]; + if (this._systems.length > 0) { + x += this._systems[0].accoladeWidth; + } + return x; + } + + public doResize(): void { + let y: number = this.pagePadding![1]; + this.width = this.renderer.width; + const oldHeight: number = this.height; + // + // 1. Score Info + y = this._layoutAndRenderScoreInfo(y, oldHeight); + // + // 2. Tunings + y = this._layoutAndRenderTunings(y, oldHeight); + // + // 3. Chord Digrams + y = this._layoutAndRenderChordDiagrams(y, oldHeight); + // + // 4. One result per StaffSystem + y = this._resizeAndRenderScore(y, oldHeight); + + y = this.layoutAndRenderBottomScoreInfo(y); + + y = this.layoutAndRenderAnnotation(y); + + this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; + } + + private _layoutAndRenderTunings(y: number, totalHeight: number = -1): number { + if (!this.tuningGlyph) { + return y; + } + + const res: RenderingResources = this.renderer.settings.display.resources; + this.tuningGlyph.x = this.pagePadding![0]; + this.tuningGlyph.width = this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; + this.tuningGlyph.doLayout(); + + const tuningHeight = Math.round(this.tuningGlyph.height); + + const e = new RenderFinishedEventArgs(); + e.x = 0; + e.y = y; + e.width = this.scaledWidth; + e.height = tuningHeight; + e.totalWidth = this.scaledWidth; + e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; + + this.registerPartial(e, (canvas: ICanvas) => { + canvas.color = res.scoreInfoColor; + canvas.textAlign = TextAlign.Center; + this.tuningGlyph!.paint(0, 0, canvas); + }); + + return y + tuningHeight; + } + + private _layoutAndRenderChordDiagrams(y: number, totalHeight: number = -1): number { + if (!this.chordDiagrams) { + return y; + } + const res: RenderingResources = this.renderer.settings.display.resources; + this.chordDiagrams.x = this.pagePadding![0]; + this.chordDiagrams.width = this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; + this.chordDiagrams.doLayout(); + + const diagramHeight = Math.round(this.chordDiagrams.height); + + const e = new RenderFinishedEventArgs(); + e.x = 0; + e.y = y; + e.width = this.scaledWidth; + e.height = diagramHeight; + e.totalWidth = this.scaledWidth; + e.totalHeight = totalHeight < 0 ? y + diagramHeight : totalHeight; + + this.registerPartial(e, (canvas: ICanvas) => { + canvas.color = res.scoreInfoColor; + canvas.textAlign = TextAlign.Center; + this.chordDiagrams!.paint(0, 0, canvas); + }); + + return y + diagramHeight; + } + + private _layoutAndRenderScoreInfo(y: number, totalHeight: number = -1): number { + Logger.debug(this.name, 'Layouting score info'); + + const e = new RenderFinishedEventArgs(); + e.x = 0; + e.y = y; + + let infoHeight = 0; + + const res: RenderingResources = this.renderer.settings.display.resources; + + const scoreInfoGlyphs: TextGlyph[] = []; + + for (const [scoreElement, _notationElement] of ScoreLayout.headerElements.value) { + if (this.headerGlyphs.has(scoreElement)) { + const glyph: TextGlyph = this.headerGlyphs.get(scoreElement)!; + glyph.y = infoHeight; + this.alignScoreInfoGlyph(glyph); + + let lineHeight = glyph.font.size; + + // words and music on same line if not aligned on same side + if (scoreElement === ScoreSubElement.Words) { + if (this.headerGlyphs.has(ScoreSubElement.Music)) { + const musicGlyph = this.headerGlyphs.get(ScoreSubElement.Music)!; + if (musicGlyph.textAlign !== glyph.textAlign) { + lineHeight = 0; + } + } + } + + infoHeight += lineHeight; + + scoreInfoGlyphs.push(glyph); + } + } + + if (scoreInfoGlyphs.length > 0) { + infoHeight = Math.floor(infoHeight + 17); + e.width = this.scaledWidth; + e.height = infoHeight; + e.totalWidth = this.scaledWidth; + e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; + this.registerPartial(e, (canvas: ICanvas) => { + canvas.color = res.scoreInfoColor; + canvas.textAlign = TextAlign.Center; + for (const g of scoreInfoGlyphs) { + g.paint(0, 0, canvas); + } + }); + } + + return y + infoHeight; + } + + private _resizeAndRenderScore(y: number, oldHeight: number): number { + // if we have a fixed number of bars per row, we only need to refit them. + const barsPerRowActive = this.getBarsPerSystem(0) > 0; + if (barsPerRowActive) { + for (let i: number = 0; i < this._systems.length; i++) { + const system: StaffSystem = this._systems[i]; + system.width = system.computedWidth; + this._fitSystem(system); + y += this._paintSystem(system, oldHeight); + } + } else { + // clear out staves during re-layout, this info is outdated during + // re-layout of the bars + for (const r of this._allMasterBarRenderers) { + for (const b of r.renderers) { + b.afterReverted(); + } + } + + this._systems = []; + let currentIndex: number = 0; + const maxWidth: number = this._maxWidth; + let system: StaffSystem = this.createEmptyStaffSystem(this._systems.length); + system.x = this.pagePadding![0]; + system.y = y; + while (currentIndex < this._allMasterBarRenderers.length) { + // 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 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); + while (renderers && !renderers.canWrap && system.masterBarsRenderers.length > 1) { + renderers = system.revertLastBar(); + currentIndex--; + } + // 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); + y += this._paintSystem(system, oldHeight); + // note: we do not increase currentIndex here to have it added to the next system + system = this.createEmptyStaffSystem(this._systems.length); + system.x = this.pagePadding![0]; + system.y = y; + } + } + system.isLast = this.lastBarIndex === system.lastBarIndex; + // don't forget to finish the last system + this._fitSystem(system); + y += this._paintSystem(system, oldHeight); + } + return y; + } + + private _layoutAndRenderScore(y: number): number { + const startIndex: number = this.firstBarIndex; + let currentBarIndex: number = startIndex; + const endBarIndex: number = this.lastBarIndex; + + this._systems = []; + while (currentBarIndex <= endBarIndex) { + // create system and align set proper coordinates + const system: StaffSystem = this._createStaffSystem(currentBarIndex, endBarIndex); + this._systems.push(system); + system.x = this.pagePadding![0]; + system.y = y; + currentBarIndex = system.lastBarIndex + 1; + // finalize system (sizing etc). + this._fitSystem(system); + Logger.debug( + this.name, + `Rendering partial from bar ${system.firstBarIndex} to ${system.lastBarIndex}`, + null + ); + y += this._paintSystem(system, y); + } + return y; + } + + private _paintSystem(system: StaffSystem, totalHeight: number): number { + // paint into canvas + const height: number = Math.floor(system.height); + + const args: RenderFinishedEventArgs = new RenderFinishedEventArgs(); + args.x = 0; + args.y = system.y; + args.totalWidth = this.scaledWidth; + args.totalHeight = totalHeight; + args.width = this.scaledWidth; + args.height = height; + args.firstMasterBarIndex = system.firstBarIndex; + args.lastMasterBarIndex = system.lastBarIndex; + + system.buildBoundingsLookup(0, 0); + this.registerPartial(args, canvas => { + this.renderer.canvas!.color = this.renderer.settings.display.resources.mainGlyphColor; + this.renderer.canvas!.textAlign = TextAlign.Left; + // NOTE: we use this negation trick to make the system paint itself to 0/0 coordinates + // since we use partial drawing + system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas); + }); + + // calculate coordinates for next system + return height; + } + + /** + * Realignes the bars in this line according to the available space + */ + private _fitSystem(system: StaffSystem): void { + if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) { + system.scaleToWidth(this._maxWidth); + } else { + system.scaleToWidth(system.width); + } + system.finalizeSystem(); + } + + protected abstract getBarsPerSystem(systemIndex: number): number; + + private _createStaffSystem(currentBarIndex: number, endIndex: number): StaffSystem { + const system: StaffSystem = this.createEmptyStaffSystem(this._systems.length); + const barsPerRow: number = this.getBarsPerSystem(system.index); + const maxWidth: number = this._maxWidth; + const end: number = endIndex + 1; + + let barIndex = currentBarIndex; + while (barIndex < end) { + if (this._barsFromPreviousSystem.length > 0) { + for (const renderer of this._barsFromPreviousSystem) { + system.addMasterBarRenderers(this.renderer.tracks!, renderer); + barIndex = renderer.lastMasterBarIndex; + } + } else { + const multiBarRestInfo = this.multiBarRestInfo; + const additionalMultiBarsRestBarIndices: number[] | null = + multiBarRestInfo !== null && multiBarRestInfo.has(barIndex) + ? multiBarRestInfo.get(barIndex)! + : null; + + const renderers = system.addBars(this.renderer.tracks!, barIndex, additionalMultiBarsRestBarIndices); + this._allMasterBarRenderers.push(renderers); + barIndex = renderers.lastMasterBarIndex; + } + this._barsFromPreviousSystem = []; + let systemIsFull: boolean = false; + // can bar placed in this line? + if (barsPerRow === -1 && system.width >= maxWidth && system.masterBarsRenderers.length !== 0) { + systemIsFull = true; + } else if (system.masterBarsRenderers.length === barsPerRow + 1) { + systemIsFull = true; + } + if (systemIsFull) { + let reverted = system.revertLastBar(); + if (reverted) { + this._barsFromPreviousSystem.push(reverted); + while (reverted && !reverted.canWrap && system.masterBarsRenderers.length > 1) { + reverted = system.revertLastBar(); + if (reverted) { + this._barsFromPreviousSystem.push(reverted); + } + } + } + system.isFull = true; + system.isLast = false; + this._barsFromPreviousSystem.reverse(); + return system; + } + if (this._needsLineBreak(barIndex)) { + system.isFull = true; + system.isLast = false; + return system; + } + system.x = 0; + barIndex++; + } + system.isLast = endIndex === system.lastBarIndex; + 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/RenderStaff.ts b/packages/alphatab/src/rendering/staves/RenderStaff.ts index 911970034..0824b180c 100644 --- a/packages/alphatab/src/rendering/staves/RenderStaff.ts +++ b/packages/alphatab/src/rendering/staves/RenderStaff.ts @@ -206,12 +206,12 @@ export class RenderStaff { let x = 0; // scale the bars by keeping their respective ratio size - const scaleRatio = width / this.system.computedWidth; + const scale = width / this.system.computedStaffWidth; for (const renderer of this.barRenderers) { renderer.x = x; renderer.y = this.topPadding + topOverflow; - const actualBarWidth = renderer.computedWidth * scaleRatio; + const actualBarWidth = renderer.computedWidth * scale; renderer.scaleToWidth(actualBarWidth); x += renderer.width; } diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 281b697b0..2a0d311f3 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -179,11 +179,34 @@ export class StaffSystem { public isFull: boolean = false; /** - * The width that the content bars actually need + * The current width of the system to which the content is scaled. + * Includes accolade (tracknames, brackets etc) and the content. + * + * Used to determine the final size needed for rendering. */ public width: number = 0; + + /** + * The minimum/default width to which the system was sized + * when performing the layout. This is the size of the system if no + * fitting/resizing is performed. + * + * Includes accolade (tracknames, brackets etc) and the content. + * + * Used to perform a resizing/refitting of the system. + */ public computedWidth: number = 0; - public totalBarDisplayScale: number = 0; + + /** + * The minimum/default width to which the staves in this system were sized + * when performing the layout. This is the size of the system if no + * fitting/resizing is performed. + * + * Includes only the stave size without any other paddings or sizes like the accolade. + * + * Used to perform a resizing/refitting of the staves in the system. + */ + public computedStaffWidth: number = 0; public isLast: boolean = false; public masterBarsRenderers: MasterBarsRenderers[] = []; @@ -260,7 +283,7 @@ export class StaffSystem { this.firstVisibleStaff = firstVisibleStaff; this._calculateAccoladeSpacing(tracks); - this._updateWidthFromLastBar(); + this._applyLayoutAndUpdateWidth(); return renderers; } @@ -332,7 +355,7 @@ export class StaffSystem { barLayoutingInfo.finish(); // ensure same widths of new renderer - result.width = this._updateWidthFromLastBar(); + result.width = this._applyLayoutAndUpdateWidth(); return result; } @@ -342,7 +365,6 @@ export class StaffSystem { const toRemove: MasterBarsRenderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1]; this.masterBarsRenderers.splice(this.masterBarsRenderers.length - 1, 1); let width: number = 0; - let barDisplayScale: number = 0; let firstVisibleStaff: RenderStaff | undefined = undefined; for (const g of this.staves) { @@ -355,10 +377,6 @@ export class StaffSystem { if (computedWidth > width) { width = computedWidth; } - const newBarDisplayScale = lastBar.barDisplayScale; - if (newBarDisplayScale > barDisplayScale) { - barDisplayScale = newBarDisplayScale; - } lastBar.afterReverted(); if (s.isVisible) { @@ -379,32 +397,24 @@ export class StaffSystem { this.width -= width; this.computedWidth -= width; - this.totalBarDisplayScale -= barDisplayScale; + this.computedStaffWidth -= width; return toRemove; } return null; } - private _updateWidthFromLastBar(): number { + private _applyLayoutAndUpdateWidth(): number { let realWidth: number = 0; - let barDisplayScale: number = 0; - for (let i: number = 0, j: number = this.allStaves.length; i < j; i++) { - const s: RenderStaff = this.allStaves[i]; - + for (const s of this.allStaves) { const last = s.barRenderers[s.barRenderers.length - 1]; last.applyLayoutingInfo(); if (last.computedWidth > realWidth) { realWidth = last.computedWidth; } - - const newBarDisplayScale = last.barDisplayScale; - if (newBarDisplayScale > barDisplayScale) { - barDisplayScale = newBarDisplayScale; - } } this.width += realWidth; this.computedWidth += realWidth; - this.totalBarDisplayScale += barDisplayScale; + this.computedStaffWidth += realWidth; return realWidth; } @@ -582,8 +592,8 @@ export class StaffSystem { } public scaleToWidth(width: number): void { - for (let i: number = 0, j: number = this.allStaves.length; i < j; i++) { - this.allStaves[i].scaleToWidth(width); + for (const s of this.allStaves) { + s.scaleToWidth(width - this.accoladeWidth); } this.width = width; } diff --git a/packages/playground/alphatex-editor.ts b/packages/playground/alphatex-editor.ts index 1e7c4b791..4f07545c0 100644 --- a/packages/playground/alphatex-editor.ts +++ b/packages/playground/alphatex-editor.ts @@ -111,21 +111,6 @@ async function setupEditor(api: alphaTab.AlphaTabApi, element: HTMLElement) { 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-template.html b/packages/playground/control-template.html index 8e6d6a852..1bb770814 100644 --- a/packages/playground/control-template.html +++ b/packages/playground/control-template.html @@ -145,7 +145,10 @@ Horizontal - Vertical Layout + Vertical + + + Parchment diff --git a/packages/playground/control.ts b/packages/playground/control.ts index eb311af3e..54180cb69 100644 --- a/packages/playground/control.ts +++ b/packages/playground/control.ts @@ -603,6 +603,9 @@ export function setupControl(selector: string, customSettings: alphaTab.json.Set case 'horizontal': settings.display.layoutMode = alphaTab.LayoutMode.Horizontal; break; + case 'parchment': + settings.display.layoutMode = alphaTab.LayoutMode.Parchment; + break; } at.updateSettings(); From 1bbb075f139a6c74d9272a88f0840dc440f4b7c5 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Wed, 24 Dec 2025 16:16:48 +0100 Subject: [PATCH 05/10] wip: mark todos --- packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts | 1 + packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index 72c33422b..1ae7e34a8 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -195,6 +195,7 @@ export class HorizontalScreenLayout extends ScoreLayout { } private _finalizeStaffSystem() { + // TODO: lift alignrenderers to this level this._system!.alignRenderers(); this._system!.finalizeSystem(); } diff --git a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts index ffb3dd398..ad6297b3d 100644 --- a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts +++ b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts @@ -314,6 +314,7 @@ export abstract class VerticalLayoutBase extends ScoreLayout { * Realignes the bars in this line according to the available space */ private _fitSystem(system: StaffSystem): void { + // TODO lift scaleToWidth of system and staves to this level and apply bar scale factors if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) { system.scaleToWidth(this._maxWidth); } else { From e81c9438096979bf27ea54691c9d28358a0770c3 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Wed, 24 Dec 2025 17:02:35 +0100 Subject: [PATCH 06/10] chore: cleanup some todos. --- packages/alphatab/src/model/Chord.ts | 3 --- packages/alphatab/src/model/ElementStyle.ts | 2 -- packages/alphatab/src/model/ModelUtils.ts | 19 ++++++++++++------- packages/alphatab/src/model/Score.ts | 5 +++-- .../src/rendering/ScoreBeatContainerGlyph.ts | 2 -- .../glyphs/BendNoteHeadGroupGlyph.ts | 1 - .../rendering/glyphs/ScoreNoteChordGlyph.ts | 1 - .../glyphs/ScoreNoteChordGlyphBase.ts | 6 ------ .../src/rendering/utils/BeamingHelper.ts | 1 - .../src/rendering/utils/BeatBounds.ts | 2 +- .../visualTests/features/MultiVoice.test.ts | 2 -- .../WinForms/WinFormsUiFacade.cs | 2 +- .../src/AlphaTab.Windows/Wpf/WpfUiFacade.cs | 2 +- 13 files changed, 18 insertions(+), 30 deletions(-) diff --git a/packages/alphatab/src/model/Chord.ts b/packages/alphatab/src/model/Chord.ts index 4fc39d51f..801c3c0da 100644 --- a/packages/alphatab/src/model/Chord.ts +++ b/packages/alphatab/src/model/Chord.ts @@ -1,8 +1,5 @@ import type { Staff } from '@coderline/alphatab/model/Staff'; -// TODO: rework model to specify for each finger -// on which frets they are placed. - /** * A chord definition. * @json diff --git a/packages/alphatab/src/model/ElementStyle.ts b/packages/alphatab/src/model/ElementStyle.ts index 50c2c09c4..ef3e44312 100644 --- a/packages/alphatab/src/model/ElementStyle.ts +++ b/packages/alphatab/src/model/ElementStyle.ts @@ -12,6 +12,4 @@ export class ElementStyle { * even if some "higher level" element changes colors. */ public colors: Map = new Map(); - - // TODO: replace NotationSettings.elements by adding a visibility here? } diff --git a/packages/alphatab/src/model/ModelUtils.ts b/packages/alphatab/src/model/ModelUtils.ts index a3a0d89c5..37afff3ec 100644 --- a/packages/alphatab/src/model/ModelUtils.ts +++ b/packages/alphatab/src/model/ModelUtils.ts @@ -2,7 +2,7 @@ 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 { Duration } from '@coderline/alphatab/model/Duration'; import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; import { MasterBar } from '@coderline/alphatab/model/MasterBar'; import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; @@ -43,13 +43,18 @@ export class TuningParseResultTone { * @internal */ export class ModelUtils { + private static readonly _durationIndices = ModelUtils._buildDurationIndices(); + + private static _buildDurationIndices() { + return new Map( + Object.values(Duration) + .filter((k: any) => typeof k === 'number') + .map(d => [d as number as Duration, (d as number) < 0 ? 0 : Math.log2(d as number) | 0]) + ); + } + public static getIndex(duration: Duration): number { - const index: number = 0; - const value: number = duration; - if (value < 0) { - return index; - } - return Math.log2(duration) | 0; + return ModelUtils._durationIndices.get(duration)!; } public static keySignatureIsFlat(ks: number): boolean { diff --git a/packages/alphatab/src/model/Score.ts b/packages/alphatab/src/model/Score.ts index 833179f49..941716f43 100644 --- a/packages/alphatab/src/model/Score.ts +++ b/packages/alphatab/src/model/Score.ts @@ -359,9 +359,10 @@ export class Score { if (this.masterBars.length !== 0) { bar.previousMasterBar = this.masterBars[this.masterBars.length - 1]; bar.previousMasterBar.nextMasterBar = bar; - // TODO: this will not work on anacrusis. Correct anacrusis durations are only working + // NOTE: this will not work on anacrusis. Correct anacrusis durations are only working // when there are beats with playback positions already computed which requires full finish - // chicken-egg problem here. temporarily forcing anacrusis length here to 0 + // chicken-egg problem here. temporarily forcing anacrusis length here to 0, + // .finish() will correct these times bar.start = bar.previousMasterBar.start + (bar.previousMasterBar.isAnacrusis ? 0 : bar.previousMasterBar.calculateDuration()); diff --git a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts index 1c1a05faa..1fe750a47 100644 --- a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts @@ -120,8 +120,6 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { const tie: ScoreTieGlyph = new ScoreTieGlyph(`score.tie.${n.tieOrigin!.id}`, n.tieOrigin!, n, true); this.addTie(tie); } - // TODO: depending on the type we have other positioning - // we should place glyphs in the preNotesGlyph or postNotesGlyph if needed if (n.slideInType !== SlideInType.None || n.slideOutType !== SlideOutType.None) { const l: ScoreSlideLineGlyph = new ScoreSlideLineGlyph(n.slideInType, n.slideOutType, n, this); this.addTie(l); diff --git a/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts b/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts index fecb77d7d..a420b3368 100644 --- a/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts @@ -57,7 +57,6 @@ 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); diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts index 86d91fa1f..e8422cb62 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts @@ -56,7 +56,6 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { 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); diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts index 6022914cc..41c5e3338 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts @@ -8,9 +8,6 @@ import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRen import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -// 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 @@ -233,7 +230,6 @@ export class ScoreChordNoteHeadInfo { } 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; @@ -308,8 +304,6 @@ interface ScoreNoteGlyphInfo { */ 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; diff --git a/packages/alphatab/src/rendering/utils/BeamingHelper.ts b/packages/alphatab/src/rendering/utils/BeamingHelper.ts index abbceb559..115462c1e 100644 --- a/packages/alphatab/src/rendering/utils/BeamingHelper.ts +++ b/packages/alphatab/src/rendering/utils/BeamingHelper.ts @@ -370,7 +370,6 @@ export class BeamingHelper { } public static isFullBarJoin(a: Beat, b: Beat, barIndex: number): boolean { - // TODO: this getindex call seems expensive since we call this method very often. return ModelUtils.getIndex(a.duration) - 2 - barIndex > 0 && ModelUtils.getIndex(b.duration) - 2 - barIndex > 0; } diff --git a/packages/alphatab/src/rendering/utils/BeatBounds.ts b/packages/alphatab/src/rendering/utils/BeatBounds.ts index bdff5425a..6e0caa37b 100644 --- a/packages/alphatab/src/rendering/utils/BeatBounds.ts +++ b/packages/alphatab/src/rendering/utils/BeatBounds.ts @@ -63,7 +63,7 @@ export class BeatBounds { if (!notes) { return null; } - // TODO: can be likely optimized + // perf: can be likely optimized // a beat is mostly vertically aligned, we could sort the note bounds by Y // and then do a binary search on the Y-axis. for (const note of notes) { diff --git a/packages/alphatab/test/visualTests/features/MultiVoice.test.ts b/packages/alphatab/test/visualTests/features/MultiVoice.test.ts index 7be9178cf..6f6ab823c 100644 --- a/packages/alphatab/test/visualTests/features/MultiVoice.test.ts +++ b/packages/alphatab/test/visualTests/features/MultiVoice.test.ts @@ -6,8 +6,6 @@ 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; diff --git a/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs b/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs index eb4bc7def..bd4518ffd 100644 --- a/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs +++ b/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs @@ -148,7 +148,7 @@ public override void BeginUpdateRenderResults(RenderFinishedEventArgs? r) switch (body) { case string _: - // TODO: svg support + // NOTE: no svg support return; case AlphaSkiaImage skiaImage: using (skiaImage) diff --git a/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs b/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs index ae903ecdd..042051e27 100644 --- a/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs +++ b/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs @@ -159,7 +159,7 @@ public override void BeginUpdateRenderResults(RenderFinishedEventArgs? r) ImageSource? source = null; if (body is string) { - // TODO: svg support + // NOTE: no svg support return; } From d19d23e2fc000c30ec92e37faa1d69898ff4cc85 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Wed, 24 Dec 2025 17:13:15 +0100 Subject: [PATCH 07/10] refactor: lift layout specific logic --- .../layout/HorizontalScreenLayout.ts | 21 +++++++++++-- .../rendering/layout/VerticalLayoutBase.ts | 26 ++++++++++++++-- .../src/rendering/staves/RenderStaff.ts | 31 ++----------------- .../src/rendering/staves/StaffSystem.ts | 17 ---------- 4 files changed, 45 insertions(+), 50 deletions(-) diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index 1ae7e34a8..8e34cae0e 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -195,8 +195,25 @@ export class HorizontalScreenLayout extends ScoreLayout { } private _finalizeStaffSystem() { - // TODO: lift alignrenderers to this level - this._system!.alignRenderers(); + this._alignRenderers(); this._system!.finalizeSystem(); } + + private _alignRenderers(): void { + this.width = 0; + for (const s of this._system!.allStaves) { + s.resetSharedLayoutData(); + + let w = 0; + for (const renderer of s.barRenderers) { + renderer.x = w; + renderer.y = s.topPadding + s.topOverflow; + w += renderer.width; + } + + if (w > this.width) { + this.width = w; + } + } + } } diff --git a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts index ad6297b3d..78ac364d2 100644 --- a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts +++ b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts @@ -314,15 +314,35 @@ export abstract class VerticalLayoutBase extends ScoreLayout { * Realignes the bars in this line according to the available space */ private _fitSystem(system: StaffSystem): void { - // TODO lift scaleToWidth of system and staves to this level and apply bar scale factors if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) { - system.scaleToWidth(this._maxWidth); + this._scaleToWidth(system, this._maxWidth); } else { - system.scaleToWidth(system.width); + this._scaleToWidth(system, system.width); } system.finalizeSystem(); } + private _scaleToWidth(system: StaffSystem, width: number): void { + const staffWidth = width - system.accoladeWidth; + const scale = staffWidth / system.computedStaffWidth; + + for (const s of system.allStaves) { + s.resetSharedLayoutData(); + + // scale the bars by keeping their respective ratio size + let w = 0; + for (const renderer of s.barRenderers) { + renderer.x = w; + renderer.y = s.topPadding + s.topOverflow; + + const actualBarWidth = renderer.computedWidth * scale; + renderer.scaleToWidth(actualBarWidth); + w += renderer.width; + } + } + system.width = width; + } + protected abstract getBarsPerSystem(systemIndex: number): number; private _createStaffSystem(currentBarIndex: number, endIndex: number): StaffSystem { diff --git a/packages/alphatab/src/rendering/staves/RenderStaff.ts b/packages/alphatab/src/rendering/staves/RenderStaff.ts index 0824b180c..5a0aa6be1 100644 --- a/packages/alphatab/src/rendering/staves/RenderStaff.ts +++ b/packages/alphatab/src/rendering/staves/RenderStaff.ts @@ -170,7 +170,7 @@ export class RenderStaff { } public revertLastBar(): BarRendererBase { - this._sharedLayoutData = new Map(); + this.resetSharedLayoutData(); const lastBar: BarRendererBase = this.barRenderers[this.barRenderers.length - 1]; this.barRenderers.splice(this.barRenderers.length - 1, 1); @@ -188,33 +188,8 @@ export class RenderStaff { return lastBar; } - public alignRenderers() { - this._sharedLayoutData = new Map(); - const topOverflow: number = this.topOverflow; - let x = 0; - for (const renderer of this.barRenderers) { - renderer.x = x; - renderer.y = this.topPadding + topOverflow; - x += renderer.width; - } - return x; - } - - public scaleToWidth(width: number): void { - this._sharedLayoutData = new Map(); - const topOverflow: number = this.topOverflow; - let x = 0; - - // scale the bars by keeping their respective ratio size - const scale = width / this.system.computedStaffWidth; - for (const renderer of this.barRenderers) { - renderer.x = x; - renderer.y = this.topPadding + topOverflow; - - const actualBarWidth = renderer.computedWidth * scale; - renderer.scaleToWidth(actualBarWidth); - x += renderer.width; - } + public resetSharedLayoutData() { + this._sharedLayoutData.clear(); } public topOverflow = 0; diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 2a0d311f3..0d7a2f815 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -591,23 +591,6 @@ export class StaffSystem { return Math.ceil(this._contentHeight + this.topPadding + this.bottomPadding); } - public scaleToWidth(width: number): void { - for (const s of this.allStaves) { - s.scaleToWidth(width - this.accoladeWidth); - } - this.width = width; - } - - public alignRenderers(): void { - this.width = 0; - for (const s of this.allStaves) { - const w = s.alignRenderers(); - if (w > this.width) { - this.width = w; - } - } - } - public paint(cx: number, cy: number, canvas: ICanvas): void { // const c = canvas.color; // canvas.color = Color.random(255); From 3e8b29a23735322f832b4da89e258df47b17c7bf Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Wed, 24 Dec 2025 17:58:17 +0100 Subject: [PATCH 08/10] feat: reimplement bar scaling --- .../layout/HorizontalScreenLayout.ts | 6 ++- .../src/rendering/layout/PageViewLayout.ts | 4 ++ .../src/rendering/layout/ParchmentLayout.ts | 4 ++ .../rendering/layout/VerticalLayoutBase.ts | 14 ++++++- .../src/rendering/staves/StaffSystem.ts | 37 +++++++++++++++---- 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index 8e34cae0e..124f2d0ce 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -201,7 +201,8 @@ export class HorizontalScreenLayout extends ScoreLayout { private _alignRenderers(): void { this.width = 0; - for (const s of this._system!.allStaves) { + const system = this._system!; + for (const s of system.allStaves) { s.resetSharedLayoutData(); let w = 0; @@ -212,8 +213,9 @@ export class HorizontalScreenLayout extends ScoreLayout { } if (w > this.width) { - this.width = w; + system.width = w; } } + system.width += system.accoladeWidth; } } diff --git a/packages/alphatab/src/rendering/layout/PageViewLayout.ts b/packages/alphatab/src/rendering/layout/PageViewLayout.ts index 8c3288036..250e00a4c 100644 --- a/packages/alphatab/src/rendering/layout/PageViewLayout.ts +++ b/packages/alphatab/src/rendering/layout/PageViewLayout.ts @@ -20,4 +20,8 @@ export class PageViewLayout extends VerticalLayoutBase { return barsPerRow; } + + protected override get shouldApplyBarScale(): boolean { + return this.renderer.settings.display.systemsLayoutMode === SystemsLayoutMode.UseModelLayout; + } } diff --git a/packages/alphatab/src/rendering/layout/ParchmentLayout.ts b/packages/alphatab/src/rendering/layout/ParchmentLayout.ts index a4926479d..41153067a 100644 --- a/packages/alphatab/src/rendering/layout/ParchmentLayout.ts +++ b/packages/alphatab/src/rendering/layout/ParchmentLayout.ts @@ -14,4 +14,8 @@ export class ParchmentLayout extends VerticalLayoutBase { protected override getBarsPerSystem(systemIndex: number) { return ModelUtils.getSystemLayout(this.renderer.score!, systemIndex, this.renderer.tracks!); } + + protected override get shouldApplyBarScale(): boolean { + return true; + } } diff --git a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts index 78ac364d2..953369c61 100644 --- a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts +++ b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts @@ -322,9 +322,14 @@ export abstract class VerticalLayoutBase extends ScoreLayout { system.finalizeSystem(); } + protected abstract get shouldApplyBarScale(): boolean; + private _scaleToWidth(system: StaffSystem, width: number): void { const staffWidth = width - system.accoladeWidth; const scale = staffWidth / system.computedStaffWidth; + const shouldApplyBarScale = this.shouldApplyBarScale; + + const totalScale = system.totalBarDisplayScale; for (const s of system.allStaves) { s.resetSharedLayoutData(); @@ -335,7 +340,14 @@ export abstract class VerticalLayoutBase extends ScoreLayout { renderer.x = w; renderer.y = s.topPadding + s.topOverflow; - const actualBarWidth = renderer.computedWidth * scale; + let actualBarWidth: number; + if (shouldApplyBarScale) { + const barDisplayScale = system.getBarDisplayScale(renderer); + actualBarWidth = (barDisplayScale * staffWidth) / totalScale; + } else { + actualBarWidth = renderer.computedWidth * scale; + } + renderer.scaleToWidth(actualBarWidth); w += renderer.width; } diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 0d7a2f815..febef7990 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -181,18 +181,18 @@ export class StaffSystem { /** * The current width of the system to which the content is scaled. * Includes accolade (tracknames, brackets etc) and the content. - * - * Used to determine the final size needed for rendering. + * + * Used to determine the final size needed for rendering. */ public width: number = 0; /** * The minimum/default width to which the system was sized * when performing the layout. This is the size of the system if no - * fitting/resizing is performed. - * + * fitting/resizing is performed. + * * Includes accolade (tracknames, brackets etc) and the content. - * + * * Used to perform a resizing/refitting of the system. */ public computedWidth: number = 0; @@ -200,14 +200,20 @@ export class StaffSystem { /** * The minimum/default width to which the staves in this system were sized * when performing the layout. This is the size of the system if no - * fitting/resizing is performed. - * + * fitting/resizing is performed. + * * Includes only the stave size without any other paddings or sizes like the accolade. - * + * * Used to perform a resizing/refitting of the staves in the system. */ public computedStaffWidth: number = 0; + /** + * This is the simple sum of all display scales of the bars in this system. + * This value is mainly used in the parchment style layout for correct scaling of the bars. + */ + public totalBarDisplayScale: number = 0; + public isLast: boolean = false; public masterBarsRenderers: MasterBarsRenderers[] = []; public staves: StaffTrackGroup[] = []; @@ -360,11 +366,16 @@ export class StaffSystem { return result; } + public getBarDisplayScale(renderer:BarRendererBase){ + return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale; + } + public revertLastBar(): MasterBarsRenderers | null { if (this.masterBarsRenderers.length > 1) { const toRemove: MasterBarsRenderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1]; this.masterBarsRenderers.splice(this.masterBarsRenderers.length - 1, 1); let width: number = 0; + let barDisplayScale = 0; let firstVisibleStaff: RenderStaff | undefined = undefined; for (const g of this.staves) { @@ -379,6 +390,8 @@ export class StaffSystem { } lastBar.afterReverted(); + barDisplayScale = this.getBarDisplayScale(lastBar); + if (s.isVisible) { if (!firstVisibleStaffInGroup) { firstVisibleStaffInGroup = s; @@ -398,6 +411,7 @@ export class StaffSystem { this.width -= width; this.computedWidth -= width; this.computedStaffWidth -= width; + this.totalBarDisplayScale -= barDisplayScale; return toRemove; } return null; @@ -405,13 +419,20 @@ export class StaffSystem { private _applyLayoutAndUpdateWidth(): number { let realWidth: number = 0; + + let barDisplayScale = 0; for (const s of this.allStaves) { const last = s.barRenderers[s.barRenderers.length - 1]; last.applyLayoutingInfo(); + + barDisplayScale = this.getBarDisplayScale(last); + if (last.computedWidth > realWidth) { realWidth = last.computedWidth; } } + + this.totalBarDisplayScale += barDisplayScale; this.width += realWidth; this.computedWidth += realWidth; this.computedStaffWidth += realWidth; From c6e4df9c1ac6d03e7b2429dae568cf09c091ced5 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Thu, 25 Dec 2025 10:16:11 +0100 Subject: [PATCH 09/10] fix: minor adjustmends on scaling --- .../rendering/layout/HorizontalScreenLayout.ts | 3 +++ .../src/rendering/layout/VerticalLayoutBase.ts | 13 +++++++++++-- .../src/rendering/staves/StaffSystem.ts | 17 ++--------------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index 124f2d0ce..b5c7139fa 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -209,6 +209,9 @@ export class HorizontalScreenLayout extends ScoreLayout { for (const renderer of s.barRenderers) { renderer.x = w; renderer.y = s.topPadding + s.topOverflow; + // note: this will ensure aspects like beaming helpers + // and overflows are prepared for finalization + renderer.scaleToWidth(renderer.width); w += renderer.width; } diff --git a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts index 953369c61..7b9a9317f 100644 --- a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts +++ b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts @@ -326,11 +326,20 @@ export abstract class VerticalLayoutBase extends ScoreLayout { private _scaleToWidth(system: StaffSystem, width: number): void { const staffWidth = width - system.accoladeWidth; - const scale = staffWidth / system.computedStaffWidth; const shouldApplyBarScale = this.shouldApplyBarScale; const totalScale = system.totalBarDisplayScale; + // NOTE: it currently delivers best results if we evenly distribute the available space across bars + // scaling bars relatively to their computed width, rather causes distortions whenever bars have + // pre-beat glyphs. + + // most precise scaling would come if we use the contents (voiceContainerGlyph) width as a calculation + // factor. but this would make the calculation additionally complex with not much gain. + + const difference: number = width - system.computedWidth; + const spacePerBar: number = difference / system.masterBarsRenderers.length; + for (const s of system.allStaves) { s.resetSharedLayoutData(); @@ -345,7 +354,7 @@ export abstract class VerticalLayoutBase extends ScoreLayout { const barDisplayScale = system.getBarDisplayScale(renderer); actualBarWidth = (barDisplayScale * staffWidth) / totalScale; } else { - actualBarWidth = renderer.computedWidth * scale; + actualBarWidth = renderer.computedWidth + spacePerBar; } renderer.scaleToWidth(actualBarWidth); diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index febef7990..81e76874f 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -197,17 +197,6 @@ export class StaffSystem { */ public computedWidth: number = 0; - /** - * The minimum/default width to which the staves in this system were sized - * when performing the layout. This is the size of the system if no - * fitting/resizing is performed. - * - * Includes only the stave size without any other paddings or sizes like the accolade. - * - * Used to perform a resizing/refitting of the staves in the system. - */ - public computedStaffWidth: number = 0; - /** * This is the simple sum of all display scales of the bars in this system. * This value is mainly used in the parchment style layout for correct scaling of the bars. @@ -366,8 +355,8 @@ export class StaffSystem { return result; } - public getBarDisplayScale(renderer:BarRendererBase){ - return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale; + public getBarDisplayScale(renderer: BarRendererBase) { + return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale; } public revertLastBar(): MasterBarsRenderers | null { @@ -410,7 +399,6 @@ export class StaffSystem { this.width -= width; this.computedWidth -= width; - this.computedStaffWidth -= width; this.totalBarDisplayScale -= barDisplayScale; return toRemove; } @@ -435,7 +423,6 @@ export class StaffSystem { this.totalBarDisplayScale += barDisplayScale; this.width += realWidth; this.computedWidth += realWidth; - this.computedStaffWidth += realWidth; return realWidth; } From 217cd50accdd31ec9209505f79e0e614a0bb59cb Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Thu, 25 Dec 2025 10:16:21 +0100 Subject: [PATCH 10/10] test: update tests to use new layout --- .../importer/MusicXmlImporterTestHelper.ts | 10 +++---- .../features/EffectsAndAnnotations.test.ts | 6 ++-- .../test/visualTests/features/Layout.test.ts | 29 +++++++++---------- .../visualTests/features/MultiVoice.test.ts | 4 +-- .../features/SpecialTracks.test.ts | 4 +-- .../features/SystemsLayout.test.ts | 10 +++---- 6 files changed, 29 insertions(+), 34 deletions(-) diff --git a/packages/alphatab/test/importer/MusicXmlImporterTestHelper.ts b/packages/alphatab/test/importer/MusicXmlImporterTestHelper.ts index 6326e6d25..055823d79 100644 --- a/packages/alphatab/test/importer/MusicXmlImporterTestHelper.ts +++ b/packages/alphatab/test/importer/MusicXmlImporterTestHelper.ts @@ -1,8 +1,10 @@ import { MusicXmlImporter } from '@coderline/alphatab/importer/MusicXmlImporter'; import { UnsupportedFormatError } from '@coderline/alphatab/importer/UnsupportedFormatError'; import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { Bar } from '@coderline/alphatab/model/Bar'; import { Beat } from '@coderline/alphatab/model/Beat'; +import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import { MasterBar } from '@coderline/alphatab/model/MasterBar'; import { Note } from '@coderline/alphatab/model/Note'; import type { Score } from '@coderline/alphatab/model/Score'; @@ -10,12 +12,10 @@ import { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; import { Voice } from '@coderline/alphatab/model/Voice'; import { Settings } from '@coderline/alphatab/Settings'; -import { TestPlatform } from 'test/TestPlatform'; -import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; -import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { assert } from 'chai'; +import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; +import { TestPlatform } from 'test/TestPlatform'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from 'test/visualTests/VisualTestHelper'; -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; /** * @internal @@ -68,7 +68,7 @@ export class MusicXmlImporterTestHelper { if (render) { settings.display.justifyLastSystem = score.masterBars.length > 4; if (score.tracks.some(t => t.systemsLayout.length > 0)) { - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; } prepare?.(settings); diff --git a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts index 46183dadd..31553b26c 100644 --- a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -1,10 +1,10 @@ -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { BeatBarreEffectInfo } from '@coderline/alphatab/rendering/effects/BeatBarreEffectInfo'; import { Settings } from '@coderline/alphatab/Settings'; +import { expect } from 'chai'; import { TestPlatform } from 'test/TestPlatform'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from 'test/visualTests/VisualTestHelper'; -import { expect } from 'chai'; describe('EffectsAndAnnotationsTests', () => { it('markers', async () => { @@ -250,7 +250,7 @@ describe('EffectsAndAnnotationsTests', () => { it('rasgueado', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('effects-and-annotations/rasgueado.gp', settings); }); diff --git a/packages/alphatab/test/visualTests/features/Layout.test.ts b/packages/alphatab/test/visualTests/features/Layout.test.ts index 8b6b9551f..f45f8d4fe 100644 --- a/packages/alphatab/test/visualTests/features/Layout.test.ts +++ b/packages/alphatab/test/visualTests/features/Layout.test.ts @@ -1,4 +1,3 @@ -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { Settings } from '@coderline/alphatab/Settings'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from 'test/visualTests/VisualTestHelper'; @@ -62,7 +61,7 @@ describe('LayoutTests', () => { it('brackets-braces-none', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/brackets-braces-none.gp', settings, o => { o.tracks = [0, 1, 2, 3, 4, 5, 6, 7, 8]; }); @@ -70,7 +69,7 @@ describe('LayoutTests', () => { it('brackets-braces-similar', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/brackets-braces-similar.gp', settings, o => { o.tracks = [0, 1, 2, 3, 4, 5, 6, 7, 8]; }); @@ -78,7 +77,7 @@ describe('LayoutTests', () => { it('brackets-braces-staves', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/brackets-braces-staves.gp', settings, o => { o.tracks = [0, 1, 2, 3, 4, 5, 6, 7, 8]; }); @@ -86,7 +85,7 @@ describe('LayoutTests', () => { it('brackets-braces-system-divider', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/system-divider.gp', settings, o => { o.tracks = [0, 1]; }); @@ -94,31 +93,31 @@ describe('LayoutTests', () => { it('track-names-full-name-all', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-full-name-all.gp', settings); }); it('track-names-full-name-short-name', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-full-name-short-name.gp', settings); }); it('track-names-full-name-horizontal', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-full-name-horizontal.gp', settings); }); it('track-names-first-system', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-first-system.gp', settings); }); it('track-names-all-systems-multi', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-all-systems-multi.gp', settings, o => { o.tracks = [0, 1]; }); @@ -126,7 +125,7 @@ describe('LayoutTests', () => { it('system-layout-tex', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTestTex( ` \\track { defaultSystemsLayout 3 } @@ -234,7 +233,7 @@ describe('LayoutTests', () => { undefined, o => { o.tracks = o.score.tracks.map(t => t.index); - o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + o.settings.display.layoutMode = LayoutMode.Parchment; } ); }); @@ -266,7 +265,7 @@ describe('LayoutTests', () => { undefined, o => { o.tracks = o.score.tracks.map(t => t.index); - o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + o.settings.display.layoutMode = LayoutMode.Parchment; } ); }); @@ -297,7 +296,7 @@ describe('LayoutTests', () => { undefined, o => { o.tracks = o.score.tracks.map(t => t.index); - o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + o.settings.display.layoutMode = LayoutMode.Parchment; } ); @@ -325,7 +324,7 @@ describe('LayoutTests', () => { undefined, o => { o.tracks = o.score.tracks.map(t => t.index); - o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + o.settings.display.layoutMode = LayoutMode.Parchment; } ); }); diff --git a/packages/alphatab/test/visualTests/features/MultiVoice.test.ts b/packages/alphatab/test/visualTests/features/MultiVoice.test.ts index 6f6ab823c..2c21ece9b 100644 --- a/packages/alphatab/test/visualTests/features/MultiVoice.test.ts +++ b/packages/alphatab/test/visualTests/features/MultiVoice.test.ts @@ -1,4 +1,3 @@ -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { Settings } from '@coderline/alphatab/Settings'; import { TestPlatform } from 'test/TestPlatform'; @@ -8,9 +7,8 @@ describe('MultiVoiceTests', () => { describe('displace', async () => { async function test(tex: string) { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; settings.display.justifyLastSystem = true; - settings.display.layoutMode = LayoutMode.Page; + settings.display.layoutMode = LayoutMode.Parchment; const fileName = TestPlatform.currentTestName.replaceAll(':', '_').replaceAll(',', '').replaceAll(' ', '_'); await VisualTestHelper.runVisualTestTex( diff --git a/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts b/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts index e6463e9a0..448c30d17 100644 --- a/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts +++ b/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts @@ -1,4 +1,4 @@ -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; describe('SpecialTracksTests', () => { @@ -116,7 +116,7 @@ describe('SpecialTracksTests', () => { 'test-data/visual-tests/special-tracks/numbered-durations.png', undefined, o => { - o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + o.settings.display.layoutMode = LayoutMode.Parchment; o.tracks = o.score.tracks.map(t => t.index); } ); diff --git a/packages/alphatab/test/visualTests/features/SystemsLayout.test.ts b/packages/alphatab/test/visualTests/features/SystemsLayout.test.ts index 35f7004a8..d4f810fe3 100644 --- a/packages/alphatab/test/visualTests/features/SystemsLayout.test.ts +++ b/packages/alphatab/test/visualTests/features/SystemsLayout.test.ts @@ -21,7 +21,7 @@ describe('SystemsLayoutTests', () => { it('bars-adjusted-model', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( @@ -34,7 +34,7 @@ describe('SystemsLayoutTests', () => { it('multi-track-single-track', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( @@ -47,7 +47,7 @@ describe('SystemsLayoutTests', () => { it('multi-track-two-tracks', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; const options = await VisualTestOptions.file( 'systems-layout/multi-track-different.gp', @@ -60,7 +60,7 @@ describe('SystemsLayoutTests', () => { it('resized', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( @@ -74,7 +74,6 @@ describe('SystemsLayoutTests', () => { it('horizontal-fixed-sizes-single-track', async () => { const settings = new Settings(); settings.display.layoutMode = LayoutMode.Horizontal; - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; const score = ScoreLoader.loadScoreFromBytes( await TestPlatform.loadFile('test-data/visual-tests/systems-layout/multi-track-different.gp') @@ -101,7 +100,6 @@ describe('SystemsLayoutTests', () => { it('horizontal-fixed-sizes-two-tracks', async () => { const settings = new Settings(); settings.display.layoutMode = LayoutMode.Horizontal; - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; const score = ScoreLoader.loadScoreFromBytes( await TestPlatform.loadFile('test-data/visual-tests/systems-layout/multi-track-different.gp') );