diff --git a/packages/alphatab/src/EngravingSettings.ts b/packages/alphatab/src/EngravingSettings.ts index 5517fb094..177affaa7 100644 --- a/packages/alphatab/src/EngravingSettings.ts +++ b/packages/alphatab/src/EngravingSettings.ts @@ -3,6 +3,7 @@ import { JsonHelper } from '@coderline/alphatab/io/JsonHelper'; import { Logger } from '@coderline/alphatab/Logger'; import { Duration } from '@coderline/alphatab/model/Duration'; import { MusicFontSymbol, MusicFontSymbolLookup } from '@coderline/alphatab/model/MusicFontSymbol'; +import { RestPosition } from '@coderline/alphatab/model/RestPosition'; import type { SmuflMetadata } from '@coderline/alphatab/SmuflMetadata'; /** @@ -256,6 +257,22 @@ export class EngravingSettings { */ public stemFlagOffsets: Map = new Map(); + /** + * Overrides the vertical position of rests for the primary voice (voice 0). + * Use {@link RestPosition} constants for named staff line positions. + * When null, the position is calculated automatically from the staff line count. + * @public + */ + public restPositionMain: RestPosition | null = null; + + /** + * Overrides the vertical position of rests for secondary voices (voice 1+). + * Use {@link RestPosition} constants for named staff line positions. + * When null, the position is calculated automatically from the staff line count. + * @public + */ + public restPositionSecondary: RestPosition | null = null; + /** * A lookup containing the offset from the visual top to the glyph center. * The glyph center is the origin coordinate at which the glyph paths start when drawn on the alphabetic baseline. diff --git a/packages/alphatab/src/generated/EngravingSettingsCloner.ts b/packages/alphatab/src/generated/EngravingSettingsCloner.ts index 08dc51b81..89fd832d0 100644 --- a/packages/alphatab/src/generated/EngravingSettingsCloner.ts +++ b/packages/alphatab/src/generated/EngravingSettingsCloner.ts @@ -41,6 +41,8 @@ export class EngravingSettingsCloner { clone.repeatOffsetX = new Map(original.repeatOffsetX); clone.standardStemLength = original.standardStemLength; clone.stemFlagOffsets = new Map(original.stemFlagOffsets); + clone.restPositionMain = original.restPositionMain; + clone.restPositionSecondary = original.restPositionSecondary; clone.glyphTop = new Map(original.glyphTop); clone.glyphBottom = new Map(original.glyphBottom); clone.glyphWidths = new Map(original.glyphWidths); diff --git a/packages/alphatab/src/generated/EngravingSettingsJson.ts b/packages/alphatab/src/generated/EngravingSettingsJson.ts index 3c1cdaa49..892923421 100644 --- a/packages/alphatab/src/generated/EngravingSettingsJson.ts +++ b/packages/alphatab/src/generated/EngravingSettingsJson.ts @@ -6,6 +6,7 @@ import { MusicFontSymbol } from "@coderline/alphatab/model/MusicFontSymbol"; import { EngravingStemInfoJson } from "@coderline/alphatab/generated/EngravingStemInfoJson"; import { Duration } from "@coderline/alphatab/model/Duration"; +import { RestPosition } from "@coderline/alphatab/model/RestPosition"; /** * This class holds all all spacing, thickness and scaling metrics * related to engraving the music notation. @@ -184,6 +185,20 @@ export interface EngravingSettingsJson { * @smufl 1.4 */ stemFlagOffsets?: Map, number>; + /** + * Overrides the vertical position of rests for the primary voice (voice 0). + * Use {@link RestPosition} constants for named staff line positions. + * When null, the position is calculated automatically from the staff line count. + * @public + */ + restPositionMain?: (RestPosition | null) | keyof typeof RestPosition | Lowercase; + /** + * Overrides the vertical position of rests for secondary voices (voice 1+). + * Use {@link RestPosition} constants for named staff line positions. + * When null, the position is calculated automatically from the staff line count. + * @public + */ + restPositionSecondary?: (RestPosition | null) | keyof typeof RestPosition | Lowercase; /** * A lookup containing the offset from the visual top to the glyph center. * The glyph center is the origin coordinate at which the glyph paths start when drawn on the alphabetic baseline. diff --git a/packages/alphatab/src/generated/EngravingSettingsSerializer.ts b/packages/alphatab/src/generated/EngravingSettingsSerializer.ts index 19cc823e4..7aed7cd7c 100644 --- a/packages/alphatab/src/generated/EngravingSettingsSerializer.ts +++ b/packages/alphatab/src/generated/EngravingSettingsSerializer.ts @@ -9,6 +9,7 @@ import { EngravingStemInfoSerializer } from "@coderline/alphatab/generated/Engra import { MusicFontSymbol } from "@coderline/alphatab/model/MusicFontSymbol"; import { EngravingStemInfo } from "@coderline/alphatab/EngravingSettings"; import { Duration } from "@coderline/alphatab/model/Duration"; +import { RestPosition } from "@coderline/alphatab/model/RestPosition"; /** * @internal */ @@ -79,6 +80,8 @@ export class EngravingSettingsSerializer { m.set(k.toString(), v); } } + o.set("restpositionmain", obj.restPositionMain as number | null); + o.set("restpositionsecondary", obj.restPositionSecondary as number | null); { const m = new Map(); o.set("glyphtop", m); @@ -275,6 +278,12 @@ export class EngravingSettingsSerializer { obj.stemFlagOffsets.set(JsonHelper.parseEnum(k, Duration)!, v as number); }); return true; + case "restpositionmain": + obj.restPositionMain = JsonHelper.parseEnum(v, RestPosition) ?? null; + return true; + case "restpositionsecondary": + obj.restPositionSecondary = JsonHelper.parseEnum(v, RestPosition) ?? null; + return true; case "glyphtop": obj.glyphTop = new Map(); JsonHelper.forEach(v, (v, k) => { diff --git a/packages/alphatab/src/model/RestPosition.ts b/packages/alphatab/src/model/RestPosition.ts new file mode 100644 index 000000000..5ad85de87 --- /dev/null +++ b/packages/alphatab/src/model/RestPosition.ts @@ -0,0 +1,54 @@ +/** + * Defines named vertical positions for rests on a standard 5-line staff, + * expressed as steps in alphaTab's internal coordinate system. + * Use these values with {@link EngravingSettings.restPositionMain} and + * {@link EngravingSettings.restPositionSecondary}. + * @public + */ + +/* + * The settings assume a 5 line staff or lower and are adjusted proportionally within ScoreBeatGlypth._createRestGlyphs() + * based upon the renderer.bar.staff.standardNotationLineCount value + */ +export enum RestPosition { + /** + * Bottom line of the staff (line 1 in standard notation). + */ + Line1 = 8.5, + /** + * Space between line 1 and line 2. + */ + Line1Space = 7.5, + /** + * Second line from the bottom. + */ + Line2 = 6.5, + /** + * Space between line 2 and line 3. + */ + Line2Space = 5.5, + /** + * Middle line of the staff. + */ + Line3 = 4.5, + /** + * Space between line 3 and line 4. + */ + Line3Space = 3.5, + /** + * Second line from the top. + */ + Line4 = 2.5, + /** + * Space between line 4 and line 5. + */ + Line4Space = 1.5, + /** + * Top line of the staff (line 5 in standard notation). + */ + Line5 = 0.5, + /** + * Space above the top line. + */ + Line5Space = -0.5 +} diff --git a/packages/alphatab/src/model/_barrel.ts b/packages/alphatab/src/model/_barrel.ts index c5c0e1a6a..a911057c4 100644 --- a/packages/alphatab/src/model/_barrel.ts +++ b/packages/alphatab/src/model/_barrel.ts @@ -22,6 +22,7 @@ export { Color } from '@coderline/alphatab/model/Color'; export { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; export { Direction } from '@coderline/alphatab/model/Direction'; export { Duration } from '@coderline/alphatab/model/Duration'; +export { RestPosition } from '@coderline/alphatab/model/RestPosition'; export { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; export { FadeType } from '@coderline/alphatab/model/FadeType'; export { FermataType, Fermata } from '@coderline/alphatab/model/Fermata'; diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts index 695fd2478..6d14e65fa 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts @@ -300,17 +300,37 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { } private _createRestGlyphs() { + const sr = this.renderer as ScoreBarRenderer; + const engraving = sr.resources.engravingSettings; + const override = this.container.beat.voice.index === 0 ? engraving.restPositionMain : engraving.restPositionSecondary; + const lineCount = this.renderer.bar.staff.standardNotationLineCount; + + let steps: number; - let steps = Math.ceil((this.renderer.bar.staff.standardNotationLineCount - 1) / 2) * 2; + // If there is a restPosition settings override and there are 5 staff lines or fewer then use + // it after adjusting its value based on the lineCount other wise just use the standard placement + if (override !== null && lineCount <= 5) { + steps = override - (5 - lineCount) * 2; + + if ( + this.container.beat.duration === Duration.Whole && + (lineCount == 1 || lineCount == 3) + ) { + steps -= 1; + } + } + else { + steps = Math.ceil((lineCount - 1) / 2) * 2; + } // this positioning is quite strange, for most staff line counts // the whole/rest are aligned as half below the whole rest. // but for staff line count 1 and 3 they are aligned centered on the same line. if ( this.container.beat.duration === Duration.Whole && - this.renderer.bar.staff.standardNotationLineCount !== 1 && - this.renderer.bar.staff.standardNotationLineCount !== 3 + lineCount !== 1 && + lineCount !== 3 ) { steps -= 2; } diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-default-multi-voice.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-default-multi-voice.png new file mode 100644 index 000000000..4eae9e566 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-default-multi-voice.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-default.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-default.png new file mode 100644 index 000000000..3dab563be Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-durations.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-durations.png new file mode 100644 index 000000000..b6422793b Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-durations.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line1.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line1.png new file mode 100644 index 000000000..16aa5c666 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line1.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line1Space.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line1Space.png new file mode 100644 index 000000000..67b62e160 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line1Space.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line2.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line2.png new file mode 100644 index 000000000..12eb4537d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line2.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line2Space.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line2Space.png new file mode 100644 index 000000000..176199741 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line2Space.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line3.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line3.png new file mode 100644 index 000000000..5f0948d6b Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line3.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line3Space.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line3Space.png new file mode 100644 index 000000000..ffaae270e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line3Space.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line4.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line4.png new file mode 100644 index 000000000..a350c0846 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line4.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line4Space.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line4Space.png new file mode 100644 index 000000000..f5abd1e42 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line4Space.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line5.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line5.png new file mode 100644 index 000000000..b6422793b Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line5.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line5Space.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line5Space.png new file mode 100644 index 000000000..bc5b52534 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-main-Line5Space.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-both-set.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-both-set.png new file mode 100644 index 000000000..548cf6b76 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-both-set.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-main-only.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-main-only.png new file mode 100644 index 000000000..605625156 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-main-only.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-secondary-only.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-secondary-only.png new file mode 100644 index 000000000..7992c9324 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-multi-voice-secondary-only.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-1.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-1.png new file mode 100644 index 000000000..98ec8ceff Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-1.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-2.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-2.png new file mode 100644 index 000000000..66fa92cd5 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-2.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-3.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-3.png new file mode 100644 index 000000000..ca720253a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-3.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-4.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-4.png new file mode 100644 index 000000000..019dba397 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-4.png differ diff --git a/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-5.png b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-5.png new file mode 100644 index 000000000..16aa5c666 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/rest-position/rest-position-staff-lines-5.png differ diff --git a/packages/alphatab/test/visualTests/features/RestPosition.test.ts b/packages/alphatab/test/visualTests/features/RestPosition.test.ts new file mode 100644 index 000000000..10a816355 --- /dev/null +++ b/packages/alphatab/test/visualTests/features/RestPosition.test.ts @@ -0,0 +1,92 @@ +import { describe, it } from 'vitest'; +import { Settings } from '@coderline/alphatab/Settings'; +import { RestPosition } from '@coderline/alphatab/model/RestPosition'; +import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; +const primaryDurations = `r.1 | r.2 e5.2 | r.4 e5.4 *3 | r.8 e5.8 *7 | r.16 e5.16 *15 | r.32 e5.32 *31`; +const secondaryDurations = `e4.1 | e4.2 r.2 | e4.4 *3 r.4 | e4.8 *7 r.8 | e4.16 *15 r.16 | e4.32 *31 r.32`; +const restPositionEntries = Object.entries(RestPosition).filter(([k]) => isNaN(Number(k))) as [string, RestPosition][]; + +describe('RestPositionTests', () => { + it('rest-position-default', async () => { + await VisualTestHelper.runVisualTestTex( + primaryDurations, + 'test-data/visual-tests/rest-position/rest-position-default.png' + ); + }); + + it('rest-position-default-multi-voice', async () => { + await VisualTestHelper.runVisualTestTex( + `\\voice ${primaryDurations} \\voice ${secondaryDurations}`, + 'test-data/visual-tests/rest-position/rest-position-default-multi-voice.png' + ); + }); + + describe('rest-position-main', () => { + for (const [name, value] of restPositionEntries) { + it(`position-${name}`, async () => { + const settings = new Settings(); + settings.display.resources.engravingSettings.restPositionMain = value; + await VisualTestHelper.runVisualTestTex( + primaryDurations, + `test-data/visual-tests/rest-position/rest-position-main-${name}.png`, + settings + ); + }); + } + }); + + it('rest-position-durations', async () => { + const settings = new Settings(); + settings.display.resources.engravingSettings.restPositionMain = RestPosition.Line5; + await VisualTestHelper.runVisualTestTex( + primaryDurations, + 'test-data/visual-tests/rest-position/rest-position-durations.png', + settings + ); + }); + + it('rest-position-multi-voice-both-set', async () => { + const settings = new Settings(); + settings.display.resources.engravingSettings.restPositionMain = RestPosition.Line5; + settings.display.resources.engravingSettings.restPositionSecondary = RestPosition.Line1; + await VisualTestHelper.runVisualTestTex( + `\\voice ${primaryDurations} \\voice ${secondaryDurations}`, + 'test-data/visual-tests/rest-position/rest-position-multi-voice-both-set.png', + settings + ); + }); + + it('rest-position-multi-voice-main-only', async () => { + const settings = new Settings(); + settings.display.resources.engravingSettings.restPositionMain = RestPosition.Line5; + await VisualTestHelper.runVisualTestTex( + `\\voice ${primaryDurations} \\voice ${secondaryDurations}`, + 'test-data/visual-tests/rest-position/rest-position-multi-voice-main-only.png', + settings + ); + }); + + it('rest-position-multi-voice-secondary-only', async () => { + const settings = new Settings(); + settings.display.resources.engravingSettings.restPositionSecondary = RestPosition.Line1; + await VisualTestHelper.runVisualTestTex( + `\\voice ${primaryDurations} \\voice ${secondaryDurations}`, + 'test-data/visual-tests/rest-position/rest-position-multi-voice-secondary-only.png', + settings + ); + }); + + describe('rest-position-staff-lines', () => { + for (const lineCount of [1, 2, 3, 4, 5]) { + it(`linecount-${lineCount}`, async () => { + const settings = new Settings(); + settings.display.resources.engravingSettings.restPositionMain = RestPosition.Line1; + await VisualTestHelper.runVisualTestTex( + `\\staff { score ${lineCount} } ${primaryDurations}`, + `test-data/visual-tests/rest-position/rest-position-staff-lines-${lineCount}.png`, + settings + ); + }); + } + }); +});