diff --git a/packages/alphatab/scripts/CloneEmitter.ts b/packages/alphatab/scripts/CloneEmitter.ts index af1f1933e..43f0caed0 100644 --- a/packages/alphatab/scripts/CloneEmitter.ts +++ b/packages/alphatab/scripts/CloneEmitter.ts @@ -260,7 +260,7 @@ function generateClonePropertyStatements( function generateCloneBody( program: ts.Program, - input: ts.ClassDeclaration, + input: ts.ClassDeclaration | ts.InterfaceDeclaration, importer: (name: string, module: string) => void ): ts.Block { const propertiesToSerialize = input.members @@ -299,7 +299,7 @@ function generateCloneBody( function createCloneMethod( program: ts.Program, - input: ts.ClassDeclaration, + input: ts.ClassDeclaration | ts.InterfaceDeclaration, importer: (name: string, module: string) => void ) { return ts.factory.createMethodDeclaration( diff --git a/packages/alphatab/scripts/EmitterBase.ts b/packages/alphatab/scripts/EmitterBase.ts index f38fa3a95..929be40d3 100644 --- a/packages/alphatab/scripts/EmitterBase.ts +++ b/packages/alphatab/scripts/EmitterBase.ts @@ -61,8 +61,8 @@ export function generateFile(program: ts.Program, sourceFile: ts.SourceFile, fil export function generateClass( program: ts.Program, - classDeclaration: ts.ClassDeclaration, - generate: (program: ts.Program, classDeclaration: ts.ClassDeclaration) => ts.SourceFile + classDeclaration: ts.ClassDeclaration | ts.InterfaceDeclaration, + generate: (program: ts.Program, classDeclaration: ts.ClassDeclaration | ts.InterfaceDeclaration) => ts.SourceFile ) { const sourceFileName = path.relative( path.join(path.dirname(program.getCompilerOptions().configFilePath as string), 'src'), @@ -83,11 +83,14 @@ export function generateClass( export default function createEmitter( jsDocMarker: string, - generate: (program: ts.Program, classDeclaration: ts.ClassDeclaration) => ts.SourceFile + generate: (program: ts.Program, classDeclaration: ts.ClassDeclaration | ts.InterfaceDeclaration) => ts.SourceFile ) { function scanSourceFile(program: ts.Program, sourceFile: ts.SourceFile) { for (const stmt of sourceFile.statements) { - if (ts.isClassDeclaration(stmt) && ts.getJSDocTags(stmt).some(t => t.tagName.text === jsDocMarker)) { + if ( + (ts.isClassDeclaration(stmt) || ts.isInterfaceDeclaration(stmt)) && + ts.getJSDocTags(stmt).some(t => t.tagName.text === jsDocMarker) + ) { generateClass(program, stmt, generate); } } diff --git a/packages/alphatab/scripts/JsonDeclarationEmitter.ts b/packages/alphatab/scripts/JsonDeclarationEmitter.ts index 0d0bfff94..43ef66788 100644 --- a/packages/alphatab/scripts/JsonDeclarationEmitter.ts +++ b/packages/alphatab/scripts/JsonDeclarationEmitter.ts @@ -147,7 +147,7 @@ function createJsonMember( function createJsonMembers( program: ts.Program, - input: ts.ClassDeclaration, + input: ts.ClassDeclaration | ts.InterfaceDeclaration, importer: (name: string, module: string) => void ): ts.TypeElement[] { const hasMatchingSetter = (name: string): boolean => diff --git a/packages/alphatab/scripts/Serializer.fromJson.ts b/packages/alphatab/scripts/Serializer.fromJson.ts index 605f47f9b..5466da4b6 100644 --- a/packages/alphatab/scripts/Serializer.fromJson.ts +++ b/packages/alphatab/scripts/Serializer.fromJson.ts @@ -3,7 +3,7 @@ import * as ts from 'typescript'; import type { TypeSchema } from './TypeSchema'; function generateFromJsonBody( - input: ts.ClassDeclaration, + input: ts.ClassDeclaration | ts.InterfaceDeclaration, serializable: TypeSchema, importer: (name: string, module: string) => void ) { @@ -33,7 +33,7 @@ function generateFromJsonBody( } export function createFromJsonMethod( - input: ts.ClassDeclaration, + input: ts.ClassDeclaration | ts.InterfaceDeclaration, serializable: TypeSchema, importer: (name: string, module: string) => void ) { diff --git a/packages/alphatab/scripts/Serializer.setProperty.ts b/packages/alphatab/scripts/Serializer.setProperty.ts index ff75b644f..f216d996a 100644 --- a/packages/alphatab/scripts/Serializer.setProperty.ts +++ b/packages/alphatab/scripts/Serializer.setProperty.ts @@ -342,13 +342,17 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri importer(itemSerializer, findSerializerModule(prop.type)); if (prop.type.isNullable || prop.type.isOptional) { - importer(prop.type.typeAsString, prop.type.modulePath); + if (!prop.type.isRecord) { + // @record types are constructed via `{}` object literal (not `new T()`), so no value-import is needed. + importer(prop.type.typeAsString, prop.type.modulePath); + } + const constructExpr = prop.type.isRecord ? '{}' : `new ${prop.type.typeAsString}()`; caseStatements.push( createNodeFromSource( ` - if (v) { - obj.${fieldName} = new ${prop.type.typeAsString}(); - ${itemSerializer}.fromJson(obj.${fieldName}, v); + if (v) { + obj.${fieldName} = ${constructExpr}; + ${itemSerializer}.fromJson(obj.${fieldName}, v); } else { obj.${fieldName} = ${prop.type.isNullable ? 'null' : 'undefined'} }`, @@ -379,7 +383,8 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri const itemSerializer = `${prop.type.typeAsString}Serializer`; importer(itemSerializer, findSerializerModule(prop.type)); - if (prop.type.isNullable || prop.type.isOptional) { + if ((prop.type.isNullable || prop.type.isOptional) && !prop.type.isRecord) { + // @record types are constructed via `{}` object literal (not `new T()`), so no value-import is needed. importer(prop.type.typeAsString, prop.type.modulePath); } @@ -427,11 +432,13 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri ts.factory.createBlock( [ assignField( - ts.factory.createNewExpression( - ts.factory.createIdentifier(prop.type.typeAsString), - undefined, - [] - ) + prop.type.isRecord + ? ts.factory.createObjectLiteralExpression([], false) + : ts.factory.createNewExpression( + ts.factory.createIdentifier(prop.type.typeAsString), + undefined, + [] + ) ), ts.factory.createExpressionStatement( ts.factory.createCallExpression( @@ -456,7 +463,16 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri ], true ), - ts.factory.createBlock([assignField(ts.factory.createNull())], true) + ts.factory.createBlock( + [ + assignField( + prop.type.isOptional + ? ts.factory.createIdentifier('undefined') + : ts.factory.createNull() + ) + ], + true + ) ), ts.factory.createReturnStatement(ts.factory.createTrue()) ], @@ -509,11 +525,13 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri ), ts.factory.createBlock([ assignField( - ts.factory.createNewExpression( - ts.factory.createIdentifier(prop.type.typeAsString), - [], - [] - ) + prop.type.isRecord + ? ts.factory.createObjectLiteralExpression([], false) + : ts.factory.createNewExpression( + ts.factory.createIdentifier(prop.type.typeAsString), + [], + [] + ) ) ]) )), @@ -604,7 +622,7 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri } export function createSetPropertyMethod( - input: ts.ClassDeclaration, + input: ts.ClassDeclaration | ts.InterfaceDeclaration, serializable: TypeSchema, importer: (name: string, module: string) => void ) { diff --git a/packages/alphatab/scripts/Serializer.toJson.ts b/packages/alphatab/scripts/Serializer.toJson.ts index ed2c5656b..2d7c301b3 100644 --- a/packages/alphatab/scripts/Serializer.toJson.ts +++ b/packages/alphatab/scripts/Serializer.toJson.ts @@ -283,14 +283,30 @@ function generateToJsonBody(serializable: TypeSchema, importer: (name: string, m } else { const itemSerializer = `${prop.type.typeAsString}Serializer`; importer(itemSerializer, findSerializerModule(prop.type)); - propertyStatements.push( - createNodeFromSource( - ` - o.set(${JSON.stringify(jsonName)}, ${itemSerializer}.toJson(obj.${fieldName})); - `, - ts.SyntaxKind.ExpressionStatement - ) - ); + if (prop.type.isRecord && (prop.type.isNullable || prop.type.isOptional)) { + // @record-typed optional / nullable fields skip emission when unset, so the JSON + // output omits null entries rather than carrying a Map value the C# JS-style + // TypeOf shim would render as `undefined` (TypeHelper.cs:606+). + propertyStatements.push( + createNodeFromSource( + ` + if(obj.${fieldName}) { + o.set(${JSON.stringify(jsonName)}, ${itemSerializer}.toJson(obj.${fieldName})); + } + `, + ts.SyntaxKind.IfStatement + ) + ); + } else { + propertyStatements.push( + createNodeFromSource( + ` + o.set(${JSON.stringify(jsonName)}, ${itemSerializer}.toJson(obj.${fieldName})); + `, + ts.SyntaxKind.ExpressionStatement + ) + ); + } } if (prop.target) { @@ -314,13 +330,13 @@ function generateToJsonBody(serializable: TypeSchema, importer: (name: string, m } export function createToJsonMethod( - input: ts.ClassDeclaration, + input: ts.ClassDeclaration | ts.InterfaceDeclaration, serializable: TypeSchema, importer: (name: string, module: string) => void ) { const methodDecl = createNodeFromSource( `public class Serializer { - public static toJson(obj: ${input.name!.text} | null): Map | null { + public static toJson(obj: ${input.name!.text} | null | undefined): Map | null { } }`, ts.SyntaxKind.MethodDeclaration diff --git a/packages/alphatab/scripts/TypeSchema.ts b/packages/alphatab/scripts/TypeSchema.ts index fc23768a2..1eddfa5ad 100644 --- a/packages/alphatab/scripts/TypeSchema.ts +++ b/packages/alphatab/scripts/TypeSchema.ts @@ -15,6 +15,7 @@ export type TypeWithNullableInfo = { readonly modulePath: string; readonly isCloneable: boolean; readonly isJsonImmutable: boolean; + readonly isRecord: boolean; readonly isNumberType: boolean; readonly isMap: boolean; readonly isSet: boolean; @@ -28,23 +29,28 @@ export type TypeWithNullableInfo = { type Writeable = { -readonly [P in keyof T]: T[P] }; -export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration) { +export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration | ts.InterfaceDeclaration) { const schema: TypeSchema = { properties: [], isStrict: !!ts.getJSDocTags(input).find(t => t.tagName.text === 'json_strict'), hasToJsonExtension: false, - hasSetPropertyExtension: false + hasSetPropertyExtension: false, + isRecord: !!ts.getJSDocTags(input).find(t => t.tagName.text === 'record') }; - const accessorHasSetter = (cls: ts.ClassDeclaration, name: string): boolean => + const accessorHasSetter = (cls: ts.ClassDeclaration | ts.InterfaceDeclaration, name: string): boolean => cls.members.some(m => ts.isSetAccessorDeclaration(m) && (m.name as ts.Identifier).text === name); const handleMember = ( - cls: ts.ClassDeclaration, - member: ts.ClassDeclaration['members'][0], + cls: ts.ClassDeclaration | ts.InterfaceDeclaration, + member: ts.ClassElement | ts.TypeElement, typeArgumentMapping: Map | undefined ) => { - if (ts.isPropertyDeclaration(member) || ts.isGetAccessorDeclaration(member)) { + if ( + ts.isPropertyDeclaration(member) || + ts.isPropertySignature(member) || + ts.isGetAccessorDeclaration(member) + ) { // Only the getter side of an accessor pair contributes a schema entry; the setter is // handled implicitly via the assignment generated by the serializer. A getter without // a matching setter is a computed read-only property and is skipped — it cannot be @@ -55,11 +61,10 @@ export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration) ) { return; } - if ( - !member.modifiers?.find( - m => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.PrivateKeyword - ) - ) { + const isStaticOrPrivate = !!member.modifiers?.some( + m => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.PrivateKeyword + ); + if (!isStaticOrPrivate) { const jsonNames = [(member.name as ts.Identifier).text.toLowerCase()]; const jsDoc = ts.getJSDocTags(member); @@ -71,7 +76,9 @@ export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration) const asRaw = !!jsDoc.find(t => t.tagName.text === 'json_raw'); const isReadonly = !!jsDoc.find(t => t.tagName.text === 'json_read_only'); const isAccessor = ts.isGetAccessorDeclaration(member); - const isOptional = ts.isPropertyDeclaration(member) && !!member.questionToken; + const isOptional = + (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) && + !!member.questionToken; // Heuristic: a deprecated getter+setter pair is almost always a // backwards-compat alias for a canonical property — round-tripping it via @@ -96,7 +103,7 @@ export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration) isJsonReadOnly: isReadonly, isReadOnly: isAccessor ? false - : member.modifiers!.some(m => m.kind == ts.SyntaxKind.ReadonlyKeyword), + : !!member.modifiers?.some(m => m.kind == ts.SyntaxKind.ReadonlyKeyword), name: (member.name as ts.Identifier).text, jsDocTags: jsDoc, type: getTypeWithNullableInfo( @@ -121,7 +128,7 @@ export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration) } }; - let hierarchy: ts.ClassDeclaration | undefined = input; + let hierarchy: ts.ClassDeclaration | ts.InterfaceDeclaration | undefined = input; let typeArgumentMapping: Map | undefined; const checker = program.getTypeChecker(); while (hierarchy) { @@ -179,6 +186,7 @@ export function getTypeWithNullableInfo( typeAsString: '', modulePath: '', isJsonImmutable: false, + isRecord: false, isNumberType: false, isMap: false, isSet: false, @@ -191,7 +199,17 @@ export function getTypeWithNullableInfo( let mainType: ts.Type | undefined; const fillBaseInfoFrom = (tsType: ts.Type) => { - const valueDeclaration = tsType.symbol?.valueDeclaration; + // For interfaces (e.g. @record types), there is no valueDeclaration; fall back to + // the first declaration if it is an InterfaceDeclaration. We intentionally do NOT + // fall back for generic classes etc., because picking up arbitrary declarations + // would mis-resolve type parameters and break unrelated emitters. + let valueDeclaration: ts.Declaration | undefined = tsType.symbol?.valueDeclaration; + if (!valueDeclaration && tsType.symbol?.declarations) { + const interfaceDecl = tsType.symbol.declarations.find(d => ts.isInterfaceDeclaration(d)); + if (interfaceDecl) { + valueDeclaration = interfaceDecl; + } + } mainType = tsType; typeInfo.typeAsString = checker.typeToString(tsType, undefined, undefined); @@ -209,6 +227,7 @@ export function getTypeWithNullableInfo( if (typeInfo.jsDocTags) { typeInfo.isJsonImmutable = !!typeInfo.jsDocTags.find(t => t.tagName.text === 'json_immutable'); typeInfo.isCloneable = !!typeInfo.jsDocTags.find(t => t.tagName.text === 'cloneable'); + typeInfo.isRecord = !!typeInfo.jsDocTags.find(t => t.tagName.text === 'record'); } if (tsType.flags & ts.ObjectFlags.Reference) { @@ -249,6 +268,7 @@ export function getTypeWithNullableInfo( modulePath: '', isCloneable: false, isJsonImmutable: false, + isRecord: false, isNumberType: true, isMap: false, isSet: false, @@ -421,6 +441,7 @@ export interface TypeSchema { isStrict: boolean; hasToJsonExtension: boolean; hasSetPropertyExtension: boolean; + isRecord: boolean; properties: TypeProperty[]; } diff --git a/packages/alphatab/src/Environment.ts b/packages/alphatab/src/Environment.ts index 70cebd901..8388f3bd0 100644 --- a/packages/alphatab/src/Environment.ts +++ b/packages/alphatab/src/Environment.ts @@ -566,31 +566,15 @@ export class Environment { private static _createDefaultStaveProfiles(): Map> { const staveProfiles = new Map>(); - - // the general layout is repeating the same pattern across the different notation staffs: - // * general effects before notation renderer, shown also if notation renderer is hidden (`before-xxxx-always`) - // * effects specific to the notation renderer, hidden if the nottation renderer is hidden (`before-xxxx-hideable`) - // * the notation renderer itself, hidden based on settings (`xxxx`) - - staveProfiles.set( - StaveProfile.Default, - new Set([ - SlashBarRenderer.StaffId, - ScoreBarRenderer.StaffId, - NumberedBarRenderer.StaffId, - TabBarRenderer.StaffId - ]) - ); - staveProfiles.set( - StaveProfile.ScoreTab, - new Set([ - SlashBarRenderer.StaffId, - ScoreBarRenderer.StaffId, - NumberedBarRenderer.StaffId, - TabBarRenderer.StaffId - ]) - ); - + const all = new Set([ + SlashBarRenderer.StaffId, + ScoreBarRenderer.StaffId, + NumberedBarRenderer.StaffId, + TabBarRenderer.StaffId + ]); + + staveProfiles.set(StaveProfile.Default, all); + staveProfiles.set(StaveProfile.ScoreTab, all); staveProfiles.set(StaveProfile.Score, new Set([ScoreBarRenderer.StaffId])); staveProfiles.set(StaveProfile.Tab, new Set([TabBarRenderer.StaffId])); staveProfiles.set(StaveProfile.TabMixed, new Set([TabBarRenderer.StaffId])); diff --git a/packages/alphatab/src/generated/CoreSettingsSerializer.ts b/packages/alphatab/src/generated/CoreSettingsSerializer.ts index 5b1dd0127..474245f45 100644 --- a/packages/alphatab/src/generated/CoreSettingsSerializer.ts +++ b/packages/alphatab/src/generated/CoreSettingsSerializer.ts @@ -17,7 +17,7 @@ export class CoreSettingsSerializer { } JsonHelper.forEach(m, (v, k) => CoreSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: CoreSettings | null): Map | null { + public static toJson(obj: CoreSettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts index 5c21a52e6..3c821bbb2 100644 --- a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts +++ b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts @@ -19,7 +19,7 @@ export class DisplaySettingsSerializer { } JsonHelper.forEach(m, (v, k) => DisplaySettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: DisplaySettings | null): Map | null { + public static toJson(obj: DisplaySettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/EngravingSettingsSerializer.ts b/packages/alphatab/src/generated/EngravingSettingsSerializer.ts index 19cc823e4..f6aee68c8 100644 --- a/packages/alphatab/src/generated/EngravingSettingsSerializer.ts +++ b/packages/alphatab/src/generated/EngravingSettingsSerializer.ts @@ -19,7 +19,7 @@ export class EngravingSettingsSerializer { } JsonHelper.forEach(m, (v, k) => EngravingSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: EngravingSettings | null): Map | null { + public static toJson(obj: EngravingSettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/EngravingStemInfoSerializer.ts b/packages/alphatab/src/generated/EngravingStemInfoSerializer.ts index a414a4001..2da211f64 100644 --- a/packages/alphatab/src/generated/EngravingStemInfoSerializer.ts +++ b/packages/alphatab/src/generated/EngravingStemInfoSerializer.ts @@ -15,7 +15,7 @@ export class EngravingStemInfoSerializer { } JsonHelper.forEach(m, (v, k) => EngravingStemInfoSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: EngravingStemInfo | null): Map | null { + public static toJson(obj: EngravingStemInfo | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/ExporterSettingsSerializer.ts b/packages/alphatab/src/generated/ExporterSettingsSerializer.ts index 4f6b6d46e..93821bb6c 100644 --- a/packages/alphatab/src/generated/ExporterSettingsSerializer.ts +++ b/packages/alphatab/src/generated/ExporterSettingsSerializer.ts @@ -15,7 +15,7 @@ export class ExporterSettingsSerializer { } JsonHelper.forEach(m, (v, k) => ExporterSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: ExporterSettings | null): Map | null { + public static toJson(obj: ExporterSettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/ImporterSettingsSerializer.ts b/packages/alphatab/src/generated/ImporterSettingsSerializer.ts index 90545e615..66c17f71c 100644 --- a/packages/alphatab/src/generated/ImporterSettingsSerializer.ts +++ b/packages/alphatab/src/generated/ImporterSettingsSerializer.ts @@ -15,7 +15,7 @@ export class ImporterSettingsSerializer { } JsonHelper.forEach(m, (v, k) => ImporterSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: ImporterSettings | null): Map | null { + public static toJson(obj: ImporterSettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/NotationSettingsSerializer.ts b/packages/alphatab/src/generated/NotationSettingsSerializer.ts index 2ef43f289..446885435 100644 --- a/packages/alphatab/src/generated/NotationSettingsSerializer.ts +++ b/packages/alphatab/src/generated/NotationSettingsSerializer.ts @@ -19,7 +19,7 @@ export class NotationSettingsSerializer { } JsonHelper.forEach(m, (v, k) => NotationSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: NotationSettings | null): Map | null { + public static toJson(obj: NotationSettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/PlayerSettingsSerializer.ts b/packages/alphatab/src/generated/PlayerSettingsSerializer.ts index 60c6dd2a7..82f02505c 100644 --- a/packages/alphatab/src/generated/PlayerSettingsSerializer.ts +++ b/packages/alphatab/src/generated/PlayerSettingsSerializer.ts @@ -20,7 +20,7 @@ export class PlayerSettingsSerializer { } JsonHelper.forEach(m, (v, k) => PlayerSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: PlayerSettings | null): Map | null { + public static toJson(obj: PlayerSettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/RenderingResourcesSerializer.ts b/packages/alphatab/src/generated/RenderingResourcesSerializer.ts index 67abbc6e2..ddd732c3e 100644 --- a/packages/alphatab/src/generated/RenderingResourcesSerializer.ts +++ b/packages/alphatab/src/generated/RenderingResourcesSerializer.ts @@ -19,7 +19,7 @@ export class RenderingResourcesSerializer { } JsonHelper.forEach(m, (v, k) => RenderingResourcesSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: RenderingResources | null): Map | null { + public static toJson(obj: RenderingResources | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/SettingsSerializer.ts b/packages/alphatab/src/generated/SettingsSerializer.ts index 091f7023f..1b65ccbec 100644 --- a/packages/alphatab/src/generated/SettingsSerializer.ts +++ b/packages/alphatab/src/generated/SettingsSerializer.ts @@ -21,7 +21,7 @@ export class SettingsSerializer { } JsonHelper.forEach(m, (v, k) => SettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: Settings | null): Map | null { + public static toJson(obj: Settings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/SlidePlaybackSettingsSerializer.ts b/packages/alphatab/src/generated/SlidePlaybackSettingsSerializer.ts index 1d382a1b4..6b2b115d9 100644 --- a/packages/alphatab/src/generated/SlidePlaybackSettingsSerializer.ts +++ b/packages/alphatab/src/generated/SlidePlaybackSettingsSerializer.ts @@ -15,7 +15,7 @@ export class SlidePlaybackSettingsSerializer { } JsonHelper.forEach(m, (v, k) => SlidePlaybackSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: SlidePlaybackSettings | null): Map | null { + public static toJson(obj: SlidePlaybackSettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/VibratoPlaybackSettingsSerializer.ts b/packages/alphatab/src/generated/VibratoPlaybackSettingsSerializer.ts index 05033d0c3..4f56fc5d8 100644 --- a/packages/alphatab/src/generated/VibratoPlaybackSettingsSerializer.ts +++ b/packages/alphatab/src/generated/VibratoPlaybackSettingsSerializer.ts @@ -15,7 +15,7 @@ export class VibratoPlaybackSettingsSerializer { } JsonHelper.forEach(m, (v, k) => VibratoPlaybackSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } - public static toJson(obj: VibratoPlaybackSettings | null): Map | null { + public static toJson(obj: VibratoPlaybackSettings | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/AutomationSerializer.ts b/packages/alphatab/src/generated/model/AutomationSerializer.ts index aa09881d4..1794f4c02 100644 --- a/packages/alphatab/src/generated/model/AutomationSerializer.ts +++ b/packages/alphatab/src/generated/model/AutomationSerializer.ts @@ -18,7 +18,7 @@ export class AutomationSerializer { } JsonHelper.forEach(m, (v, k) => AutomationSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Automation | null): Map | null { + public static toJson(obj: Automation | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/BackingTrackSerializer.ts b/packages/alphatab/src/generated/model/BackingTrackSerializer.ts index b2087d22e..a3fe746de 100644 --- a/packages/alphatab/src/generated/model/BackingTrackSerializer.ts +++ b/packages/alphatab/src/generated/model/BackingTrackSerializer.ts @@ -15,7 +15,7 @@ export class BackingTrackSerializer { } JsonHelper.forEach(m, (v, k) => BackingTrackSerializer.setProperty(obj, k, v)); } - public static toJson(obj: BackingTrack | null): Map | null { + public static toJson(obj: BackingTrack | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/BarSerializer.ts b/packages/alphatab/src/generated/model/BarSerializer.ts index fc757c6df..67bdce6e2 100644 --- a/packages/alphatab/src/generated/model/BarSerializer.ts +++ b/packages/alphatab/src/generated/model/BarSerializer.ts @@ -7,6 +7,10 @@ import { Bar } from "@coderline/alphatab/model/Bar"; import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { VoiceSerializer } from "@coderline/alphatab/generated/model/VoiceSerializer"; import { SustainPedalMarkerSerializer } from "@coderline/alphatab/generated/model/SustainPedalMarkerSerializer"; +import { ScoreBarOverrideSerializer } from "@coderline/alphatab/generated/model/ScoreBarOverrideSerializer"; +import { TabBarOverrideSerializer } from "@coderline/alphatab/generated/model/TabBarOverrideSerializer"; +import { SlashBarOverrideSerializer } from "@coderline/alphatab/generated/model/SlashBarOverrideSerializer"; +import { NumberedBarOverrideSerializer } from "@coderline/alphatab/generated/model/NumberedBarOverrideSerializer"; import { BarStyleSerializer } from "@coderline/alphatab/generated/model/BarStyleSerializer"; import { Clef } from "@coderline/alphatab/model/Clef"; import { Ottavia } from "@coderline/alphatab/model/Ottavia"; @@ -28,7 +32,7 @@ export class BarSerializer { } JsonHelper.forEach(m, (v, k) => BarSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Bar | null): Map | null { + public static toJson(obj: Bar | null | undefined): Map | null { if (!obj) { return null; } @@ -46,6 +50,18 @@ export class BarSerializer { o.set("keysignature", obj.keySignature as number); o.set("keysignaturetype", obj.keySignatureType as number); o.set("barnumberdisplay", obj.barNumberDisplay as number | undefined); + if (obj.scoreDisplay) { + o.set("scoredisplay", ScoreBarOverrideSerializer.toJson(obj.scoreDisplay)); + } + if (obj.tabDisplay) { + o.set("tabdisplay", TabBarOverrideSerializer.toJson(obj.tabDisplay)); + } + if (obj.slashDisplay) { + o.set("slashdisplay", SlashBarOverrideSerializer.toJson(obj.slashDisplay)); + } + if (obj.numberedDisplay) { + o.set("numbereddisplay", NumberedBarOverrideSerializer.toJson(obj.numberedDisplay)); + } if (obj.style) { o.set("style", BarStyleSerializer.toJson(obj.style)); } @@ -102,6 +118,42 @@ export class BarSerializer { case "barnumberdisplay": obj.barNumberDisplay = JsonHelper.parseEnum(v, BarNumberDisplay); return true; + case "scoredisplay": + if (v) { + obj.scoreDisplay = {}; + ScoreBarOverrideSerializer.fromJson(obj.scoreDisplay, v); + } + else { + obj.scoreDisplay = undefined; + } + return true; + case "tabdisplay": + if (v) { + obj.tabDisplay = {}; + TabBarOverrideSerializer.fromJson(obj.tabDisplay, v); + } + else { + obj.tabDisplay = undefined; + } + return true; + case "slashdisplay": + if (v) { + obj.slashDisplay = {}; + SlashBarOverrideSerializer.fromJson(obj.slashDisplay, v); + } + else { + obj.slashDisplay = undefined; + } + return true; + case "numbereddisplay": + if (v) { + obj.numberedDisplay = {}; + NumberedBarOverrideSerializer.fromJson(obj.numberedDisplay, v); + } + else { + obj.numberedDisplay = undefined; + } + return true; case "style": if (v) { obj.style = new BarStyle(); diff --git a/packages/alphatab/src/generated/model/BarStyleSerializer.ts b/packages/alphatab/src/generated/model/BarStyleSerializer.ts index f6cf52e04..24e2cb431 100644 --- a/packages/alphatab/src/generated/model/BarStyleSerializer.ts +++ b/packages/alphatab/src/generated/model/BarStyleSerializer.ts @@ -17,7 +17,7 @@ export class BarStyleSerializer { } JsonHelper.forEach(m, (v, k) => BarStyleSerializer.setProperty(obj, k, v)); } - public static toJson(obj: BarStyle | null): Map | null { + public static toJson(obj: BarStyle | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/BeamingRulesSerializer.ts b/packages/alphatab/src/generated/model/BeamingRulesSerializer.ts index b0f7c779b..7afe3594d 100644 --- a/packages/alphatab/src/generated/model/BeamingRulesSerializer.ts +++ b/packages/alphatab/src/generated/model/BeamingRulesSerializer.ts @@ -16,7 +16,7 @@ export class BeamingRulesSerializer { } JsonHelper.forEach(m, (v, k) => BeamingRulesSerializer.setProperty(obj, k, v)); } - public static toJson(obj: BeamingRules | null): Map | null { + public static toJson(obj: BeamingRules | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/BeatSerializer.ts b/packages/alphatab/src/generated/model/BeatSerializer.ts index adbaeba78..d8e2461b9 100644 --- a/packages/alphatab/src/generated/model/BeatSerializer.ts +++ b/packages/alphatab/src/generated/model/BeatSerializer.ts @@ -42,7 +42,7 @@ export class BeatSerializer { } JsonHelper.forEach(m, (v, k) => BeatSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Beat | null): Map | null { + public static toJson(obj: Beat | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/BeatStyleSerializer.ts b/packages/alphatab/src/generated/model/BeatStyleSerializer.ts index 05535677d..26ffe404a 100644 --- a/packages/alphatab/src/generated/model/BeatStyleSerializer.ts +++ b/packages/alphatab/src/generated/model/BeatStyleSerializer.ts @@ -17,7 +17,7 @@ export class BeatStyleSerializer { } JsonHelper.forEach(m, (v, k) => BeatStyleSerializer.setProperty(obj, k, v)); } - public static toJson(obj: BeatStyle | null): Map | null { + public static toJson(obj: BeatStyle | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/BendPointSerializer.ts b/packages/alphatab/src/generated/model/BendPointSerializer.ts index 69d28ef8e..d65aa43cf 100644 --- a/packages/alphatab/src/generated/model/BendPointSerializer.ts +++ b/packages/alphatab/src/generated/model/BendPointSerializer.ts @@ -15,7 +15,7 @@ export class BendPointSerializer { } JsonHelper.forEach(m, (v, k) => BendPointSerializer.setProperty(obj, k, v)); } - public static toJson(obj: BendPoint | null): Map | null { + public static toJson(obj: BendPoint | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/ChordSerializer.ts b/packages/alphatab/src/generated/model/ChordSerializer.ts index ceda18235..c329146d6 100644 --- a/packages/alphatab/src/generated/model/ChordSerializer.ts +++ b/packages/alphatab/src/generated/model/ChordSerializer.ts @@ -15,7 +15,7 @@ export class ChordSerializer { } JsonHelper.forEach(m, (v, k) => ChordSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Chord | null): Map | null { + public static toJson(obj: Chord | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/ElementDisplaySerializer.ts b/packages/alphatab/src/generated/model/ElementDisplaySerializer.ts new file mode 100644 index 000000000..d8283ff6f --- /dev/null +++ b/packages/alphatab/src/generated/model/ElementDisplaySerializer.ts @@ -0,0 +1,44 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { ElementDisplay } from "@coderline/alphatab/model/ElementDisplay"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { StaffPlacement } from "@coderline/alphatab/model/ElementDisplay"; +import { SystemDisplay } from "@coderline/alphatab/model/ElementDisplay"; +/** + * @internal + */ +export class ElementDisplaySerializer { + public static fromJson(obj: ElementDisplay, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => ElementDisplaySerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: ElementDisplay | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("isvisible", obj.isVisible); + o.set("staffplacement", obj.staffPlacement as number | undefined); + o.set("systemdisplay", obj.systemDisplay as number | undefined); + return o; + } + public static setProperty(obj: ElementDisplay, property: string, v: unknown): boolean { + switch (property) { + case "isvisible": + obj.isVisible = v as boolean | undefined; + return true; + case "staffplacement": + obj.staffPlacement = JsonHelper.parseEnum(v, StaffPlacement); + return true; + case "systemdisplay": + obj.systemDisplay = JsonHelper.parseEnum(v, SystemDisplay); + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/FermataSerializer.ts b/packages/alphatab/src/generated/model/FermataSerializer.ts index 1aa480951..30d6110da 100644 --- a/packages/alphatab/src/generated/model/FermataSerializer.ts +++ b/packages/alphatab/src/generated/model/FermataSerializer.ts @@ -16,7 +16,7 @@ export class FermataSerializer { } JsonHelper.forEach(m, (v, k) => FermataSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Fermata | null): Map | null { + public static toJson(obj: Fermata | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/HeaderFooterStyleSerializer.ts b/packages/alphatab/src/generated/model/HeaderFooterStyleSerializer.ts index 56b524ea2..7340aa43e 100644 --- a/packages/alphatab/src/generated/model/HeaderFooterStyleSerializer.ts +++ b/packages/alphatab/src/generated/model/HeaderFooterStyleSerializer.ts @@ -16,7 +16,7 @@ export class HeaderFooterStyleSerializer { } JsonHelper.forEach(m, (v, k) => HeaderFooterStyleSerializer.setProperty(obj, k, v)); } - public static toJson(obj: HeaderFooterStyle | null): Map | null { + public static toJson(obj: HeaderFooterStyle | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts b/packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts index ba1fd5228..8dd1e61e6 100644 --- a/packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts +++ b/packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts @@ -17,7 +17,7 @@ export class InstrumentArticulationSerializer { } JsonHelper.forEach(m, (v, k) => InstrumentArticulationSerializer.setProperty(obj, k, v)); } - public static toJson(obj: InstrumentArticulation | null): Map | null { + public static toJson(obj: InstrumentArticulation | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/MasterBarSerializer.ts b/packages/alphatab/src/generated/model/MasterBarSerializer.ts index af898c27f..824686884 100644 --- a/packages/alphatab/src/generated/model/MasterBarSerializer.ts +++ b/packages/alphatab/src/generated/model/MasterBarSerializer.ts @@ -27,7 +27,7 @@ export class MasterBarSerializer { } JsonHelper.forEach(m, (v, k) => MasterBarSerializer.setProperty(obj, k, v)); } - public static toJson(obj: MasterBar | null): Map | null { + public static toJson(obj: MasterBar | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/NoteSerializer.ts b/packages/alphatab/src/generated/model/NoteSerializer.ts index 559de5dc5..873f99277 100644 --- a/packages/alphatab/src/generated/model/NoteSerializer.ts +++ b/packages/alphatab/src/generated/model/NoteSerializer.ts @@ -31,7 +31,7 @@ export class NoteSerializer { } JsonHelper.forEach(m, (v, k) => NoteSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Note | null): Map | null { + public static toJson(obj: Note | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/NoteStyleSerializer.ts b/packages/alphatab/src/generated/model/NoteStyleSerializer.ts index defb596bf..6615c5bbc 100644 --- a/packages/alphatab/src/generated/model/NoteStyleSerializer.ts +++ b/packages/alphatab/src/generated/model/NoteStyleSerializer.ts @@ -18,7 +18,7 @@ export class NoteStyleSerializer { } JsonHelper.forEach(m, (v, k) => NoteStyleSerializer.setProperty(obj, k, v)); } - public static toJson(obj: NoteStyle | null): Map | null { + public static toJson(obj: NoteStyle | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/NumberedBarOverrideSerializer.ts b/packages/alphatab/src/generated/model/NumberedBarOverrideSerializer.ts new file mode 100644 index 000000000..5e9b8dee3 --- /dev/null +++ b/packages/alphatab/src/generated/model/NumberedBarOverrideSerializer.ts @@ -0,0 +1,49 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { NumberedBarOverride } from "@coderline/alphatab/model/BarOverrides"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ElementDisplaySerializer } from "@coderline/alphatab/generated/model/ElementDisplaySerializer"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; +/** + * @internal + */ +export class NumberedBarOverrideSerializer { + public static fromJson(obj: NumberedBarOverride, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => NumberedBarOverrideSerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: NumberedBarOverride | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + if (obj.timeSignature) { + o.set("timesignature", ElementDisplaySerializer.toJson(obj.timeSignature)); + } + o.set("barnumber", obj.barNumber as number | undefined); + return o; + } + public static setProperty(obj: NumberedBarOverride, property: string, v: unknown): boolean { + switch (property) { + case "barnumber": + obj.barNumber = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; + } + if (["timesignature"].indexOf(property) >= 0) { + if (v) { + obj.timeSignature = {}; + ElementDisplaySerializer.fromJson(obj.timeSignature, v as Map); + } + else { + obj.timeSignature = undefined; + } + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/NumberedStaffConfigSerializer.ts b/packages/alphatab/src/generated/model/NumberedStaffConfigSerializer.ts new file mode 100644 index 000000000..c600eff1e --- /dev/null +++ b/packages/alphatab/src/generated/model/NumberedStaffConfigSerializer.ts @@ -0,0 +1,49 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { NumberedStaffConfig } from "@coderline/alphatab/model/StaffConfigs"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ElementDisplaySerializer } from "@coderline/alphatab/generated/model/ElementDisplaySerializer"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; +/** + * @internal + */ +export class NumberedStaffConfigSerializer { + public static fromJson(obj: NumberedStaffConfig, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => NumberedStaffConfigSerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: NumberedStaffConfig | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + if (obj.timeSignature) { + o.set("timesignature", ElementDisplaySerializer.toJson(obj.timeSignature)); + } + o.set("barnumber", obj.barNumber as number | undefined); + return o; + } + public static setProperty(obj: NumberedStaffConfig, property: string, v: unknown): boolean { + switch (property) { + case "barnumber": + obj.barNumber = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; + } + if (["timesignature"].indexOf(property) >= 0) { + if (v) { + obj.timeSignature = {}; + ElementDisplaySerializer.fromJson(obj.timeSignature, v as Map); + } + else { + obj.timeSignature = undefined; + } + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/PlaybackInformationSerializer.ts b/packages/alphatab/src/generated/model/PlaybackInformationSerializer.ts index 2535de92e..d53ddbb95 100644 --- a/packages/alphatab/src/generated/model/PlaybackInformationSerializer.ts +++ b/packages/alphatab/src/generated/model/PlaybackInformationSerializer.ts @@ -15,7 +15,7 @@ export class PlaybackInformationSerializer { } JsonHelper.forEach(m, (v, k) => PlaybackInformationSerializer.setProperty(obj, k, v)); } - public static toJson(obj: PlaybackInformation | null): Map | null { + public static toJson(obj: PlaybackInformation | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts index 10ac9a9ac..a3595739a 100644 --- a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts +++ b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts @@ -5,6 +5,10 @@ // import { RenderStylesheet } from "@coderline/alphatab/model/RenderStylesheet"; import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ScoreStaffConfigSerializer } from "@coderline/alphatab/generated/model/ScoreStaffConfigSerializer"; +import { TabStaffConfigSerializer } from "@coderline/alphatab/generated/model/TabStaffConfigSerializer"; +import { SlashStaffConfigSerializer } from "@coderline/alphatab/generated/model/SlashStaffConfigSerializer"; +import { NumberedStaffConfigSerializer } from "@coderline/alphatab/generated/model/NumberedStaffConfigSerializer"; import { BracketExtendMode } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNamePolicy } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameMode } from "@coderline/alphatab/model/RenderStylesheet"; @@ -20,7 +24,7 @@ export class RenderStylesheetSerializer { } JsonHelper.forEach(m, (v, k) => RenderStylesheetSerializer.setProperty(obj, k, v)); } - public static toJson(obj: RenderStylesheet | null): Map | null { + public static toJson(obj: RenderStylesheet | null | undefined): Map | null { if (!obj) { return null; } @@ -64,6 +68,10 @@ export class RenderStylesheetSerializer { o.set("hideemptystavesinfirstsystem", obj.hideEmptyStavesInFirstSystem); o.set("showsinglestaffbrackets", obj.showSingleStaffBrackets); o.set("barnumberdisplay", obj.barNumberDisplay as number); + o.set("scoreconfig", ScoreStaffConfigSerializer.toJson(obj.scoreConfig)); + o.set("tabconfig", TabStaffConfigSerializer.toJson(obj.tabConfig)); + o.set("slashconfig", SlashStaffConfigSerializer.toJson(obj.slashConfig)); + o.set("numberedconfig", NumberedStaffConfigSerializer.toJson(obj.numberedConfig)); return o; } public static setProperty(obj: RenderStylesheet, property: string, v: unknown): boolean { @@ -137,6 +145,18 @@ export class RenderStylesheetSerializer { case "barnumberdisplay": obj.barNumberDisplay = JsonHelper.parseEnum(v, BarNumberDisplay)!; return true; + case "scoreconfig": + ScoreStaffConfigSerializer.fromJson(obj.scoreConfig, v); + return true; + case "tabconfig": + TabStaffConfigSerializer.fromJson(obj.tabConfig, v); + return true; + case "slashconfig": + SlashStaffConfigSerializer.fromJson(obj.slashConfig, v); + return true; + case "numberedconfig": + NumberedStaffConfigSerializer.fromJson(obj.numberedConfig, v); + return true; } return false; } diff --git a/packages/alphatab/src/generated/model/ScoreBarOverrideSerializer.ts b/packages/alphatab/src/generated/model/ScoreBarOverrideSerializer.ts new file mode 100644 index 000000000..30c2863d0 --- /dev/null +++ b/packages/alphatab/src/generated/model/ScoreBarOverrideSerializer.ts @@ -0,0 +1,75 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { ScoreBarOverride } from "@coderline/alphatab/model/BarOverrides"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ElementDisplaySerializer } from "@coderline/alphatab/generated/model/ElementDisplaySerializer"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; +/** + * @internal + */ +export class ScoreBarOverrideSerializer { + public static fromJson(obj: ScoreBarOverride, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => ScoreBarOverrideSerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: ScoreBarOverride | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + if (obj.clef) { + o.set("clef", ElementDisplaySerializer.toJson(obj.clef)); + } + if (obj.keySignature) { + o.set("keysignature", ElementDisplaySerializer.toJson(obj.keySignature)); + } + if (obj.timeSignature) { + o.set("timesignature", ElementDisplaySerializer.toJson(obj.timeSignature)); + } + o.set("barnumber", obj.barNumber as number | undefined); + return o; + } + public static setProperty(obj: ScoreBarOverride, property: string, v: unknown): boolean { + switch (property) { + case "barnumber": + obj.barNumber = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; + } + if (["clef"].indexOf(property) >= 0) { + if (v) { + obj.clef = {}; + ElementDisplaySerializer.fromJson(obj.clef, v as Map); + } + else { + obj.clef = undefined; + } + return true; + } + if (["keysignature"].indexOf(property) >= 0) { + if (v) { + obj.keySignature = {}; + ElementDisplaySerializer.fromJson(obj.keySignature, v as Map); + } + else { + obj.keySignature = undefined; + } + return true; + } + if (["timesignature"].indexOf(property) >= 0) { + if (v) { + obj.timeSignature = {}; + ElementDisplaySerializer.fromJson(obj.timeSignature, v as Map); + } + else { + obj.timeSignature = undefined; + } + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/ScoreSerializer.ts b/packages/alphatab/src/generated/model/ScoreSerializer.ts index efd5c8400..c3f800f9b 100644 --- a/packages/alphatab/src/generated/model/ScoreSerializer.ts +++ b/packages/alphatab/src/generated/model/ScoreSerializer.ts @@ -24,7 +24,7 @@ export class ScoreSerializer { } JsonHelper.forEach(m, (v, k) => ScoreSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Score | null): Map | null { + public static toJson(obj: Score | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/ScoreStaffConfigSerializer.ts b/packages/alphatab/src/generated/model/ScoreStaffConfigSerializer.ts new file mode 100644 index 000000000..4b23e2e32 --- /dev/null +++ b/packages/alphatab/src/generated/model/ScoreStaffConfigSerializer.ts @@ -0,0 +1,75 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { ScoreStaffConfig } from "@coderline/alphatab/model/StaffConfigs"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ElementDisplaySerializer } from "@coderline/alphatab/generated/model/ElementDisplaySerializer"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; +/** + * @internal + */ +export class ScoreStaffConfigSerializer { + public static fromJson(obj: ScoreStaffConfig, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => ScoreStaffConfigSerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: ScoreStaffConfig | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + if (obj.clef) { + o.set("clef", ElementDisplaySerializer.toJson(obj.clef)); + } + if (obj.keySignature) { + o.set("keysignature", ElementDisplaySerializer.toJson(obj.keySignature)); + } + if (obj.timeSignature) { + o.set("timesignature", ElementDisplaySerializer.toJson(obj.timeSignature)); + } + o.set("barnumber", obj.barNumber as number | undefined); + return o; + } + public static setProperty(obj: ScoreStaffConfig, property: string, v: unknown): boolean { + switch (property) { + case "barnumber": + obj.barNumber = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; + } + if (["clef"].indexOf(property) >= 0) { + if (v) { + obj.clef = {}; + ElementDisplaySerializer.fromJson(obj.clef, v as Map); + } + else { + obj.clef = undefined; + } + return true; + } + if (["keysignature"].indexOf(property) >= 0) { + if (v) { + obj.keySignature = {}; + ElementDisplaySerializer.fromJson(obj.keySignature, v as Map); + } + else { + obj.keySignature = undefined; + } + return true; + } + if (["timesignature"].indexOf(property) >= 0) { + if (v) { + obj.timeSignature = {}; + ElementDisplaySerializer.fromJson(obj.timeSignature, v as Map); + } + else { + obj.timeSignature = undefined; + } + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/ScoreStyleSerializer.ts b/packages/alphatab/src/generated/model/ScoreStyleSerializer.ts index 7176c5f96..7e512e71b 100644 --- a/packages/alphatab/src/generated/model/ScoreStyleSerializer.ts +++ b/packages/alphatab/src/generated/model/ScoreStyleSerializer.ts @@ -19,7 +19,7 @@ export class ScoreStyleSerializer { } JsonHelper.forEach(m, (v, k) => ScoreStyleSerializer.setProperty(obj, k, v)); } - public static toJson(obj: ScoreStyle | null): Map | null { + public static toJson(obj: ScoreStyle | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/SectionSerializer.ts b/packages/alphatab/src/generated/model/SectionSerializer.ts index 75b14c19a..7e944c73e 100644 --- a/packages/alphatab/src/generated/model/SectionSerializer.ts +++ b/packages/alphatab/src/generated/model/SectionSerializer.ts @@ -15,7 +15,7 @@ export class SectionSerializer { } JsonHelper.forEach(m, (v, k) => SectionSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Section | null): Map | null { + public static toJson(obj: Section | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/SlashBarOverrideSerializer.ts b/packages/alphatab/src/generated/model/SlashBarOverrideSerializer.ts new file mode 100644 index 000000000..f9b073224 --- /dev/null +++ b/packages/alphatab/src/generated/model/SlashBarOverrideSerializer.ts @@ -0,0 +1,62 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { SlashBarOverride } from "@coderline/alphatab/model/BarOverrides"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ElementDisplaySerializer } from "@coderline/alphatab/generated/model/ElementDisplaySerializer"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; +/** + * @internal + */ +export class SlashBarOverrideSerializer { + public static fromJson(obj: SlashBarOverride, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => SlashBarOverrideSerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: SlashBarOverride | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + if (obj.keySignature) { + o.set("keysignature", ElementDisplaySerializer.toJson(obj.keySignature)); + } + if (obj.timeSignature) { + o.set("timesignature", ElementDisplaySerializer.toJson(obj.timeSignature)); + } + o.set("barnumber", obj.barNumber as number | undefined); + return o; + } + public static setProperty(obj: SlashBarOverride, property: string, v: unknown): boolean { + switch (property) { + case "barnumber": + obj.barNumber = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; + } + if (["keysignature"].indexOf(property) >= 0) { + if (v) { + obj.keySignature = {}; + ElementDisplaySerializer.fromJson(obj.keySignature, v as Map); + } + else { + obj.keySignature = undefined; + } + return true; + } + if (["timesignature"].indexOf(property) >= 0) { + if (v) { + obj.timeSignature = {}; + ElementDisplaySerializer.fromJson(obj.timeSignature, v as Map); + } + else { + obj.timeSignature = undefined; + } + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/SlashStaffConfigSerializer.ts b/packages/alphatab/src/generated/model/SlashStaffConfigSerializer.ts new file mode 100644 index 000000000..a6fdba9bf --- /dev/null +++ b/packages/alphatab/src/generated/model/SlashStaffConfigSerializer.ts @@ -0,0 +1,62 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { SlashStaffConfig } from "@coderline/alphatab/model/StaffConfigs"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ElementDisplaySerializer } from "@coderline/alphatab/generated/model/ElementDisplaySerializer"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; +/** + * @internal + */ +export class SlashStaffConfigSerializer { + public static fromJson(obj: SlashStaffConfig, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => SlashStaffConfigSerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: SlashStaffConfig | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + if (obj.keySignature) { + o.set("keysignature", ElementDisplaySerializer.toJson(obj.keySignature)); + } + if (obj.timeSignature) { + o.set("timesignature", ElementDisplaySerializer.toJson(obj.timeSignature)); + } + o.set("barnumber", obj.barNumber as number | undefined); + return o; + } + public static setProperty(obj: SlashStaffConfig, property: string, v: unknown): boolean { + switch (property) { + case "barnumber": + obj.barNumber = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; + } + if (["keysignature"].indexOf(property) >= 0) { + if (v) { + obj.keySignature = {}; + ElementDisplaySerializer.fromJson(obj.keySignature, v as Map); + } + else { + obj.keySignature = undefined; + } + return true; + } + if (["timesignature"].indexOf(property) >= 0) { + if (v) { + obj.timeSignature = {}; + ElementDisplaySerializer.fromJson(obj.timeSignature, v as Map); + } + else { + obj.timeSignature = undefined; + } + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/StaffSerializer.ts b/packages/alphatab/src/generated/model/StaffSerializer.ts index 2fefcc21d..5724ee38f 100644 --- a/packages/alphatab/src/generated/model/StaffSerializer.ts +++ b/packages/alphatab/src/generated/model/StaffSerializer.ts @@ -8,6 +8,10 @@ import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { BarSerializer } from "@coderline/alphatab/generated/model/BarSerializer"; import { ChordSerializer } from "@coderline/alphatab/generated/model/ChordSerializer"; import { TuningSerializer } from "@coderline/alphatab/generated/model/TuningSerializer"; +import { ScoreStaffConfigSerializer } from "@coderline/alphatab/generated/model/ScoreStaffConfigSerializer"; +import { TabStaffConfigSerializer } from "@coderline/alphatab/generated/model/TabStaffConfigSerializer"; +import { SlashStaffConfigSerializer } from "@coderline/alphatab/generated/model/SlashStaffConfigSerializer"; +import { NumberedStaffConfigSerializer } from "@coderline/alphatab/generated/model/NumberedStaffConfigSerializer"; import { Bar } from "@coderline/alphatab/model/Bar"; import { Chord } from "@coderline/alphatab/model/Chord"; /** @@ -20,7 +24,7 @@ export class StaffSerializer { } JsonHelper.forEach(m, (v, k) => StaffSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Staff | null): Map | null { + public static toJson(obj: Staff | null | undefined): Map | null { if (!obj) { return null; } @@ -41,6 +45,18 @@ export class StaffSerializer { o.set("shownumbered", obj.showNumbered); o.set("showtablature", obj.showTablature); o.set("showstandardnotation", obj.showStandardNotation); + if (obj.scoreConfig) { + o.set("scoreconfig", ScoreStaffConfigSerializer.toJson(obj.scoreConfig)); + } + if (obj.tabConfig) { + o.set("tabconfig", TabStaffConfigSerializer.toJson(obj.tabConfig)); + } + if (obj.slashConfig) { + o.set("slashconfig", SlashStaffConfigSerializer.toJson(obj.slashConfig)); + } + if (obj.numberedConfig) { + o.set("numberedconfig", NumberedStaffConfigSerializer.toJson(obj.numberedConfig)); + } o.set("ispercussion", obj.isPercussion); o.set("standardnotationlinecount", obj.standardNotationLineCount); return o; @@ -87,6 +103,42 @@ export class StaffSerializer { case "showstandardnotation": obj.showStandardNotation = v! as boolean; return true; + case "scoreconfig": + if (v) { + obj.scoreConfig = {}; + ScoreStaffConfigSerializer.fromJson(obj.scoreConfig, v); + } + else { + obj.scoreConfig = undefined; + } + return true; + case "tabconfig": + if (v) { + obj.tabConfig = {}; + TabStaffConfigSerializer.fromJson(obj.tabConfig, v); + } + else { + obj.tabConfig = undefined; + } + return true; + case "slashconfig": + if (v) { + obj.slashConfig = {}; + SlashStaffConfigSerializer.fromJson(obj.slashConfig, v); + } + else { + obj.slashConfig = undefined; + } + return true; + case "numberedconfig": + if (v) { + obj.numberedConfig = {}; + NumberedStaffConfigSerializer.fromJson(obj.numberedConfig, v); + } + else { + obj.numberedConfig = undefined; + } + return true; case "ispercussion": obj.isPercussion = v! as boolean; return true; diff --git a/packages/alphatab/src/generated/model/SustainPedalMarkerSerializer.ts b/packages/alphatab/src/generated/model/SustainPedalMarkerSerializer.ts index e5d7021f1..5bbf8026d 100644 --- a/packages/alphatab/src/generated/model/SustainPedalMarkerSerializer.ts +++ b/packages/alphatab/src/generated/model/SustainPedalMarkerSerializer.ts @@ -16,7 +16,7 @@ export class SustainPedalMarkerSerializer { } JsonHelper.forEach(m, (v, k) => SustainPedalMarkerSerializer.setProperty(obj, k, v)); } - public static toJson(obj: SustainPedalMarker | null): Map | null { + public static toJson(obj: SustainPedalMarker | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/SyncPointDataSerializer.ts b/packages/alphatab/src/generated/model/SyncPointDataSerializer.ts index 1feab991b..03829daaf 100644 --- a/packages/alphatab/src/generated/model/SyncPointDataSerializer.ts +++ b/packages/alphatab/src/generated/model/SyncPointDataSerializer.ts @@ -15,7 +15,7 @@ export class SyncPointDataSerializer { } JsonHelper.forEach(m, (v, k) => SyncPointDataSerializer.setProperty(obj, k, v)); } - public static toJson(obj: SyncPointData | null): Map | null { + public static toJson(obj: SyncPointData | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/TabBarOverrideSerializer.ts b/packages/alphatab/src/generated/model/TabBarOverrideSerializer.ts new file mode 100644 index 000000000..400634001 --- /dev/null +++ b/packages/alphatab/src/generated/model/TabBarOverrideSerializer.ts @@ -0,0 +1,62 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { TabBarOverride } from "@coderline/alphatab/model/BarOverrides"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ElementDisplaySerializer } from "@coderline/alphatab/generated/model/ElementDisplaySerializer"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; +/** + * @internal + */ +export class TabBarOverrideSerializer { + public static fromJson(obj: TabBarOverride, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => TabBarOverrideSerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: TabBarOverride | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + if (obj.clef) { + o.set("clef", ElementDisplaySerializer.toJson(obj.clef)); + } + if (obj.timeSignature) { + o.set("timesignature", ElementDisplaySerializer.toJson(obj.timeSignature)); + } + o.set("barnumber", obj.barNumber as number | undefined); + return o; + } + public static setProperty(obj: TabBarOverride, property: string, v: unknown): boolean { + switch (property) { + case "barnumber": + obj.barNumber = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; + } + if (["clef"].indexOf(property) >= 0) { + if (v) { + obj.clef = {}; + ElementDisplaySerializer.fromJson(obj.clef, v as Map); + } + else { + obj.clef = undefined; + } + return true; + } + if (["timesignature"].indexOf(property) >= 0) { + if (v) { + obj.timeSignature = {}; + ElementDisplaySerializer.fromJson(obj.timeSignature, v as Map); + } + else { + obj.timeSignature = undefined; + } + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/TabStaffConfigSerializer.ts b/packages/alphatab/src/generated/model/TabStaffConfigSerializer.ts new file mode 100644 index 000000000..81a16251a --- /dev/null +++ b/packages/alphatab/src/generated/model/TabStaffConfigSerializer.ts @@ -0,0 +1,80 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { TabStaffConfig } from "@coderline/alphatab/model/StaffConfigs"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { ElementDisplaySerializer } from "@coderline/alphatab/generated/model/ElementDisplaySerializer"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; +import { TabRhythmMode } from "@coderline/alphatab/NotationSettings"; +/** + * @internal + */ +export class TabStaffConfigSerializer { + public static fromJson(obj: TabStaffConfig, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => TabStaffConfigSerializer.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: TabStaffConfig | null | undefined): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + if (obj.clef) { + o.set("clef", ElementDisplaySerializer.toJson(obj.clef)); + } + if (obj.timeSignature) { + o.set("timesignature", ElementDisplaySerializer.toJson(obj.timeSignature)); + } + o.set("barnumber", obj.barNumber as number | undefined); + o.set("rhythm", obj.rhythm as number | undefined); + if (obj.rests) { + o.set("rests", ElementDisplaySerializer.toJson(obj.rests)); + } + return o; + } + public static setProperty(obj: TabStaffConfig, property: string, v: unknown): boolean { + switch (property) { + case "barnumber": + obj.barNumber = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; + case "rhythm": + obj.rhythm = JsonHelper.parseEnum(v, TabRhythmMode); + return true; + } + if (["clef"].indexOf(property) >= 0) { + if (v) { + obj.clef = {}; + ElementDisplaySerializer.fromJson(obj.clef, v as Map); + } + else { + obj.clef = undefined; + } + return true; + } + if (["timesignature"].indexOf(property) >= 0) { + if (v) { + obj.timeSignature = {}; + ElementDisplaySerializer.fromJson(obj.timeSignature, v as Map); + } + else { + obj.timeSignature = undefined; + } + return true; + } + if (["rests"].indexOf(property) >= 0) { + if (v) { + obj.rests = {}; + ElementDisplaySerializer.fromJson(obj.rests, v as Map); + } + else { + obj.rests = undefined; + } + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/TrackSerializer.ts b/packages/alphatab/src/generated/model/TrackSerializer.ts index 3f72b0a92..9965daa04 100644 --- a/packages/alphatab/src/generated/model/TrackSerializer.ts +++ b/packages/alphatab/src/generated/model/TrackSerializer.ts @@ -23,7 +23,7 @@ export class TrackSerializer { } JsonHelper.forEach(m, (v, k) => TrackSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Track | null): Map | null { + public static toJson(obj: Track | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/TrackStyleSerializer.ts b/packages/alphatab/src/generated/model/TrackStyleSerializer.ts index f0dfc7dd1..31862b2e0 100644 --- a/packages/alphatab/src/generated/model/TrackStyleSerializer.ts +++ b/packages/alphatab/src/generated/model/TrackStyleSerializer.ts @@ -17,7 +17,7 @@ export class TrackStyleSerializer { } JsonHelper.forEach(m, (v, k) => TrackStyleSerializer.setProperty(obj, k, v)); } - public static toJson(obj: TrackStyle | null): Map | null { + public static toJson(obj: TrackStyle | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts b/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts index 7150e3f7c..3f280b23d 100644 --- a/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts +++ b/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts @@ -16,7 +16,7 @@ export class TremoloPickingEffectSerializer { } JsonHelper.forEach(m, (v, k) => TremoloPickingEffectSerializer.setProperty(obj, k, v)); } - public static toJson(obj: TremoloPickingEffect | null): Map | null { + public static toJson(obj: TremoloPickingEffect | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/TuningSerializer.ts b/packages/alphatab/src/generated/model/TuningSerializer.ts index 6a81cf9ed..037ea0ed7 100644 --- a/packages/alphatab/src/generated/model/TuningSerializer.ts +++ b/packages/alphatab/src/generated/model/TuningSerializer.ts @@ -15,7 +15,7 @@ export class TuningSerializer { } JsonHelper.forEach(m, (v, k) => TuningSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Tuning | null): Map | null { + public static toJson(obj: Tuning | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/VoiceSerializer.ts b/packages/alphatab/src/generated/model/VoiceSerializer.ts index 1c717ff9f..88770df64 100644 --- a/packages/alphatab/src/generated/model/VoiceSerializer.ts +++ b/packages/alphatab/src/generated/model/VoiceSerializer.ts @@ -19,7 +19,7 @@ export class VoiceSerializer { } JsonHelper.forEach(m, (v, k) => VoiceSerializer.setProperty(obj, k, v)); } - public static toJson(obj: Voice | null): Map | null { + public static toJson(obj: Voice | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/generated/model/VoiceStyleSerializer.ts b/packages/alphatab/src/generated/model/VoiceStyleSerializer.ts index 2089e7739..6b9b148ce 100644 --- a/packages/alphatab/src/generated/model/VoiceStyleSerializer.ts +++ b/packages/alphatab/src/generated/model/VoiceStyleSerializer.ts @@ -17,7 +17,7 @@ export class VoiceStyleSerializer { } JsonHelper.forEach(m, (v, k) => VoiceStyleSerializer.setProperty(obj, k, v)); } - public static toJson(obj: VoiceStyle | null): Map | null { + public static toJson(obj: VoiceStyle | null | undefined): Map | null { if (!obj) { return null; } diff --git a/packages/alphatab/src/model/Bar.ts b/packages/alphatab/src/model/Bar.ts index 32c85e3cd..ef6e8d305 100644 --- a/packages/alphatab/src/model/Bar.ts +++ b/packages/alphatab/src/model/Bar.ts @@ -1,5 +1,11 @@ import { Clef } from '@coderline/alphatab/model/Clef'; import type { MasterBar } from '@coderline/alphatab/model/MasterBar'; +import type { + NumberedBarOverride, + ScoreBarOverride, + SlashBarOverride, + TabBarOverride +} from '@coderline/alphatab/model/BarOverrides'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import type { Staff } from '@coderline/alphatab/model/Staff'; @@ -399,10 +405,61 @@ export class Bar { public keySignatureType: KeySignatureType = KeySignatureType.Major; /** - * How bar numbers should be displayed. - * If specified, overrides the value from the stylesheet on score level. + * How bar numbers should be displayed on this specific bar. + * @deprecated Use {@link scoreDisplay}, {@link tabDisplay}, + * {@link slashDisplay}, or {@link numberedDisplay} `.barNumber` for + * per-staff-type per-bar control. The setter broadcasts the value + * to all four override bags (lazy-creating each); on `undefined` + * it clears `.barNumber` on each existing bag without removing it. */ - public barNumberDisplay?: BarNumberDisplay; + public get barNumberDisplay(): BarNumberDisplay | undefined { + return this.scoreDisplay?.barNumber; + } + public set barNumberDisplay(value: BarNumberDisplay | undefined) { + if (value !== undefined) { + this.scoreDisplay ??= {}; + this.scoreDisplay.barNumber = value; + this.tabDisplay ??= {}; + this.tabDisplay.barNumber = value; + this.slashDisplay ??= {}; + this.slashDisplay.barNumber = value; + this.numberedDisplay ??= {}; + this.numberedDisplay.barNumber = value; + } else { + if (this.scoreDisplay) { + this.scoreDisplay.barNumber = undefined; + } + if (this.tabDisplay) { + this.tabDisplay.barNumber = undefined; + } + if (this.slashDisplay) { + this.slashDisplay.barNumber = undefined; + } + if (this.numberedDisplay) { + this.numberedDisplay.barNumber = undefined; + } + } + } + + /** + * Per-bar override for the standard-notation staff's display. + */ + public scoreDisplay?: ScoreBarOverride; + + /** + * Per-bar override for the tablature staff's display. + */ + public tabDisplay?: TabBarOverride; + + /** + * Per-bar override for the slash staff's display. + */ + public slashDisplay?: SlashBarOverride; + + /** + * Per-bar override for the numbered (jianpu) staff's display. + */ + public numberedDisplay?: NumberedBarOverride; /** * The shortest duration contained across beats in this bar. diff --git a/packages/alphatab/src/model/BarOverrides.ts b/packages/alphatab/src/model/BarOverrides.ts new file mode 100644 index 000000000..21be724a5 --- /dev/null +++ b/packages/alphatab/src/model/BarOverrides.ts @@ -0,0 +1,50 @@ +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import type { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; + +/** + * Per-bar override for the standard-notation staff's display. + * @record + * @json + * @public + */ +export interface ScoreBarOverride { + clef?: ElementDisplay; + keySignature?: ElementDisplay; + timeSignature?: ElementDisplay; + barNumber?: BarNumberDisplay; +} + +/** + * Per-bar override for the tablature staff's display. + * @record + * @json + * @public + */ +export interface TabBarOverride { + clef?: ElementDisplay; + timeSignature?: ElementDisplay; + barNumber?: BarNumberDisplay; +} + +/** + * Per-bar override for the slash staff's display. + * @record + * @json + * @public + */ +export interface SlashBarOverride { + keySignature?: ElementDisplay; + timeSignature?: ElementDisplay; + barNumber?: BarNumberDisplay; +} + +/** + * Per-bar override for the numbered (jianpu) staff's display. + * @record + * @json + * @public + */ +export interface NumberedBarOverride { + timeSignature?: ElementDisplay; + barNumber?: BarNumberDisplay; +} diff --git a/packages/alphatab/src/model/ElementDisplay.ts b/packages/alphatab/src/model/ElementDisplay.ts new file mode 100644 index 000000000..f62d1607c --- /dev/null +++ b/packages/alphatab/src/model/ElementDisplay.ts @@ -0,0 +1,67 @@ +/** + * Spatial selector for an element across the staves of a system. + * + * One axis of {@link ElementDisplay}. The renderer dispatches per-staff + * on this axis to decide which staves paint the element. + * @public + */ +export enum StaffPlacement { + /** + * Paint the element on every staff whose + * {@link ElementDisplay.isVisible} resolves to `true`. + */ + AllStaves = 0, + /** + * Paint only on the cascade-primary render-staff for each model + * {@link Staff}. Priority: `score → tab → slash → numbered`. + */ + Primary = 1 +} + +/** + * Temporal selector for an element across the systems of the score. + * + * One axis of {@link ElementDisplay}. Independent of + * {@link StaffPlacement}. + * @public + */ +export enum SystemDisplay { + /** + * Restate the element at the start of every system. + */ + AllSystems = 0, + /** + * Show only on the first system; subsequent systems do not restate. + */ + FirstSystemOnly = 1 +} + +/** + * Per-axis visibility / placement / system-display selector for an + * element on a staff type. Used as the value type for the clef, + * key signature, time signature, and rests entries on the per-staff-type + * configuration carriers. + * + * Each axis is independently optional. An `undefined` axis defers to + * the outer layer in the three-layer resolution chain (per-bar → + * per-staff → score-wide stylesheet). + * @record + * @json + * @public + */ +export interface ElementDisplay { + /** + * Whether to paint the element at all. + */ + isVisible?: boolean; + + /** + * Spatial selector across the staves of a system. + */ + staffPlacement?: StaffPlacement; + + /** + * Temporal selector across the systems of the score. + */ + systemDisplay?: SystemDisplay; +} diff --git a/packages/alphatab/src/model/RenderStylesheet.ts b/packages/alphatab/src/model/RenderStylesheet.ts index 81e4a4ad5..f5a74ca01 100644 --- a/packages/alphatab/src/model/RenderStylesheet.ts +++ b/packages/alphatab/src/model/RenderStylesheet.ts @@ -1,3 +1,11 @@ +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import type { + NumberedStaffConfig, + ScoreStaffConfig, + SlashStaffConfig, + TabStaffConfig +} from '@coderline/alphatab/model/StaffConfigs'; import type { Track } from '@coderline/alphatab/model/Track'; /** @@ -205,7 +213,57 @@ export class RenderStylesheet { public showSingleStaffBrackets: boolean = false; /** - * How bar numbers should be displayed. + * How bar numbers should be displayed score-wide. + * @deprecated Use {@link scoreConfig}, {@link tabConfig}, + * {@link slashConfig}, or {@link numberedConfig} `.barNumber` for + * per-staff-type control. The setter broadcasts to all four + * staff-type entries. + */ + public get barNumberDisplay(): BarNumberDisplay { + return this.scoreConfig.barNumber!; + } + public set barNumberDisplay(value: BarNumberDisplay) { + this.scoreConfig.barNumber = value; + this.tabConfig.barNumber = value; + this.slashConfig.barNumber = value; + this.numberedConfig.barNumber = value; + } + + /** + * Score-wide display configuration for the standard-notation staff. + */ + public scoreConfig: ScoreStaffConfig = { + clef: { isVisible: true, staffPlacement: StaffPlacement.AllStaves, systemDisplay: SystemDisplay.AllSystems }, + keySignature: { isVisible: true, staffPlacement: StaffPlacement.AllStaves, systemDisplay: SystemDisplay.AllSystems }, + timeSignature: { isVisible: true, staffPlacement: StaffPlacement.AllStaves, systemDisplay: SystemDisplay.AllSystems }, + barNumber: BarNumberDisplay.AllBars + }; + + /** + * Score-wide display configuration for the tablature staff. + */ + public tabConfig: TabStaffConfig = { + clef: { isVisible: true, staffPlacement: StaffPlacement.AllStaves, systemDisplay: SystemDisplay.AllSystems }, + timeSignature: { isVisible: true, staffPlacement: StaffPlacement.Primary, systemDisplay: SystemDisplay.AllSystems }, + barNumber: BarNumberDisplay.AllBars, + rhythm: TabRhythmMode.Automatic, + rests: { isVisible: true, staffPlacement: StaffPlacement.Primary } + }; + + /** + * Score-wide display configuration for the slash staff. + */ + public slashConfig: SlashStaffConfig = { + keySignature: { isVisible: false }, + timeSignature: { isVisible: true, staffPlacement: StaffPlacement.Primary, systemDisplay: SystemDisplay.AllSystems }, + barNumber: BarNumberDisplay.AllBars + }; + + /** + * Score-wide display configuration for the numbered (jianpu) staff. */ - public barNumberDisplay: BarNumberDisplay = BarNumberDisplay.AllBars; + public numberedConfig: NumberedStaffConfig = { + timeSignature: { isVisible: true, staffPlacement: StaffPlacement.Primary, systemDisplay: SystemDisplay.AllSystems }, + barNumber: BarNumberDisplay.AllBars + }; } diff --git a/packages/alphatab/src/model/Staff.ts b/packages/alphatab/src/model/Staff.ts index 82cb2e2f9..9174b4d50 100644 --- a/packages/alphatab/src/model/Staff.ts +++ b/packages/alphatab/src/model/Staff.ts @@ -1,5 +1,11 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Chord } from '@coderline/alphatab/model/Chord'; +import type { + NumberedStaffConfig, + ScoreStaffConfig, + SlashStaffConfig, + TabStaffConfig +} from '@coderline/alphatab/model/StaffConfigs'; import type { Track } from '@coderline/alphatab/model/Track'; import { Tuning } from '@coderline/alphatab/model/Tuning'; import type { Settings } from '@coderline/alphatab/Settings'; @@ -100,6 +106,26 @@ export class Staff { */ public showStandardNotation: boolean = true; + /** + * Per-{@link Staff} override for the standard-notation staff's display. + */ + public scoreConfig?: ScoreStaffConfig; + + /** + * Per-{@link Staff} override for the tablature staff's display. + */ + public tabConfig?: TabStaffConfig; + + /** + * Per-{@link Staff} override for the slash staff's display. + */ + public slashConfig?: SlashStaffConfig; + + /** + * Per-{@link Staff} override for the numbered (jianpu) staff's display. + */ + public numberedConfig?: NumberedStaffConfig; + /** * Gets or sets whether the staff contains percussion notation */ diff --git a/packages/alphatab/src/model/StaffConfigs.ts b/packages/alphatab/src/model/StaffConfigs.ts new file mode 100644 index 000000000..16f6c7e0c --- /dev/null +++ b/packages/alphatab/src/model/StaffConfigs.ts @@ -0,0 +1,55 @@ +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import type { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; +import type { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; + +/** + * Per-staff-type display configuration for the standard-notation staff. + * @record + * @json + * @public + */ +export interface ScoreStaffConfig { + clef?: ElementDisplay; + keySignature?: ElementDisplay; + timeSignature?: ElementDisplay; + barNumber?: BarNumberDisplay; +} + +/** + * Per-staff-type display configuration for the tablature staff. + * @record + * @json + * @public + */ +export interface TabStaffConfig { + clef?: ElementDisplay; + timeSignature?: ElementDisplay; + barNumber?: BarNumberDisplay; + rhythm?: TabRhythmMode; + rests?: ElementDisplay; +} + +/** + * Per-staff-type display configuration for the slash staff. + * @record + * @json + * @public + */ +export interface SlashStaffConfig { + keySignature?: ElementDisplay; + timeSignature?: ElementDisplay; + barNumber?: BarNumberDisplay; +} + +/** + * Per-staff-type display configuration for the numbered (jianpu) staff. + * The "1=X" key designation is rendered as an above-staff effect-band + * label, not a header glyph, so this config has no `keySignature` field. + * @record + * @json + * @public + */ +export interface NumberedStaffConfig { + timeSignature?: ElementDisplay; + barNumber?: BarNumberDisplay; +} diff --git a/packages/alphatab/src/model/_barrel.ts b/packages/alphatab/src/model/_barrel.ts index c5c0e1a6a..4a450a87d 100644 --- a/packages/alphatab/src/model/_barrel.ts +++ b/packages/alphatab/src/model/_barrel.ts @@ -70,3 +70,20 @@ export { WahPedal } from '@coderline/alphatab/model/WahPedal'; export { WhammyType } from '@coderline/alphatab/model/WhammyType'; export { ElementStyle } from '@coderline/alphatab/model/ElementStyle'; export { BackingTrack } from '@coderline/alphatab/model/BackingTrack'; +export { + StaffPlacement, + SystemDisplay, + type ElementDisplay +} from '@coderline/alphatab/model/ElementDisplay'; +export type { + ScoreStaffConfig, + TabStaffConfig, + SlashStaffConfig, + NumberedStaffConfig +} from '@coderline/alphatab/model/StaffConfigs'; +export type { + ScoreBarOverride, + TabBarOverride, + SlashBarOverride, + NumberedBarOverride +} from '@coderline/alphatab/model/BarOverrides'; diff --git a/packages/alphatab/src/rendering/BarRendererBase.ts b/packages/alphatab/src/rendering/BarRendererBase.ts index 7dff9e573..57b5e0d8d 100644 --- a/packages/alphatab/src/rendering/BarRendererBase.ts +++ b/packages/alphatab/src/rendering/BarRendererBase.ts @@ -130,14 +130,14 @@ export class BarRendererBase { if (!this.bar || !this.bar.nextBar) { return null; } - return this.scoreRenderer.layout!.getRendererForBar(this.staff!.staffId, this.bar.nextBar); + return this.scoreRenderer.layout!.getRendererForBar(this.staff!.staffId,this.bar.nextBar); } public get previousRenderer(): BarRendererBase | null { if (!this.bar || !this.bar.previousBar) { return null; } - return this.scoreRenderer.layout!.getRendererForBar(this.staff!.staffId, this.bar.previousBar); + return this.scoreRenderer.layout!.getRendererForBar(this.staff!.staffId,this.bar.previousBar); } public scoreRenderer: ScoreRenderer; diff --git a/packages/alphatab/src/rendering/BarRendererFactory.ts b/packages/alphatab/src/rendering/BarRendererFactory.ts index 969e01bfb..a7a74cf42 100644 --- a/packages/alphatab/src/rendering/BarRendererFactory.ts +++ b/packages/alphatab/src/rendering/BarRendererFactory.ts @@ -68,6 +68,13 @@ export abstract class BarRendererFactory { public abstract get staffId(): string; + /** + * Priority in the staff-display cascade. Lower wins. The lowest-priority + * render-staff among siblings sharing the same model {@link Staff} + * is the {@link StaffPlacement.Primary} painter. + */ + public abstract get cascadePriority(): number; + public constructor(effectBands: EffectBandInfo[]) { this.effectBands = effectBands; } diff --git a/packages/alphatab/src/rendering/LineBarRenderer.ts b/packages/alphatab/src/rendering/LineBarRenderer.ts index 2749201e1..5b08e46ac 100644 --- a/packages/alphatab/src/rendering/LineBarRenderer.ts +++ b/packages/alphatab/src/rendering/LineBarRenderer.ts @@ -11,6 +11,8 @@ import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; import { NotationElement, NotationMode } from '@coderline/alphatab/NotationSettings'; import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; import { BarRendererBase, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; @@ -663,17 +665,31 @@ export abstract class LineBarRenderer extends BarRendererBase { } } + public resolveClefDisplay(): ElementDisplay { + return { isVisible: false }; + } + + public resolveKeySignatureDisplay(): ElementDisplay { + return { isVisible: false }; + } + + public abstract resolveTimeSignatureDisplay(): ElementDisplay; + + protected abstract resolveBarNumberDisplay(): BarNumberDisplay; + + public resolveRestsDisplay(): ElementDisplay { + return { isVisible: false }; + } + + public resolveRhythm(): TabRhythmMode { + return TabRhythmMode.Hidden; + } + public shouldCreateBarNumber(): boolean { - let display = BarNumberDisplay.AllBars; if (!this.settings.notation.isNotationElementVisible(NotationElement.BarNumber)) { - display = BarNumberDisplay.Hide; - } else if (this.bar.barNumberDisplay !== undefined) { - display = this.bar.barNumberDisplay!; - } else { - display = this.bar.staff.track.score.stylesheet.barNumberDisplay; + return false; } - - switch (display) { + switch (this.resolveBarNumberDisplay()) { case BarNumberDisplay.AllBars: return true; case BarNumberDisplay.FirstOfSystem: diff --git a/packages/alphatab/src/rendering/NumberedBarRenderer.ts b/packages/alphatab/src/rendering/NumberedBarRenderer.ts index 1a6f1045c..dbcbac403 100644 --- a/packages/alphatab/src/rendering/NumberedBarRenderer.ts +++ b/packages/alphatab/src/rendering/NumberedBarRenderer.ts @@ -1,6 +1,6 @@ import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; -import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; +import { BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; @@ -9,6 +9,7 @@ import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; import type { Voice } from '@coderline/alphatab/model/Voice'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; @@ -20,7 +21,8 @@ import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/Sc import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { NumberedBeatContainerGlyph } from '@coderline/alphatab/rendering/NumberedBeatContainerGlyph'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { StaffDisplayResolver } from '@coderline/alphatab/rendering/staves/StaffDisplayResolver'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import type { BeamingHelper, BeamingHelperDrawInfo } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; @@ -34,9 +36,24 @@ export class NumberedBarRenderer extends LineBarRenderer { public simpleWhammyOverflow: number = 0; - private _isOnlyNumbered: boolean; public shortestDuration = Duration.QuadrupleWhole; + public override resolveTimeSignatureDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + this.bar.numberedDisplay?.timeSignature, + this.bar.staff.numberedConfig?.timeSignature, + this.bar.staff.track.score.stylesheet.numberedConfig.timeSignature + ); + } + + protected override resolveBarNumberDisplay(): BarNumberDisplay { + return ( + this.bar.numberedDisplay?.barNumber ?? + this.bar.staff.numberedConfig?.barNumber ?? + this.bar.staff.track.score.stylesheet.numberedConfig.barNumber! + ); + } + get dotSpacing(): number { return this.smuflMetrics.glyphHeights.get(MusicFontSymbol.AugmentationDot)! * 2; } @@ -57,11 +74,6 @@ export class NumberedBarRenderer extends LineBarRenderer { return BarSubElement.NumberedStaffLine; } - public constructor(renderer: ScoreRenderer, bar: Bar) { - super(renderer, bar); - this._isOnlyNumbered = !bar.staff.showSlash && !bar.staff.showTablature && !bar.staff.showStandardNotation; - } - public override get lineSpacing(): number { return this.smuflMetrics.oneStaffSpace; } @@ -254,7 +266,7 @@ export class NumberedBarRenderer extends LineBarRenderer { protected override createPreBeatGlyphs(): void { this.wasFirstOfStaff = this.isFirstOfStaff; - if (this.index === 0 || (this.bar.masterBar.isRepeatStart && this._isOnlyNumbered)) { + if (this.index === 0 || (this.bar.masterBar.isRepeatStart && this.staff!.isCascadePrimary)) { this.addPreBeatGlyph(new BarLineGlyph(false, this.bar.staff.track.score.stylesheet.extendBarLines)); } this.createLinePreBeatGlyphs(); @@ -267,8 +279,11 @@ export class NumberedBarRenderer extends LineBarRenderer { } protected override createLinePreBeatGlyphs(): void { + // No header KS glyph: the "1=X" key designation is rendered as an + // above-staff label by {@link NumberedBarKeySignatureEffectInfo}. + const timeSignatureDisplay = this.resolveTimeSignatureDisplay(); if ( - this._isOnlyNumbered && + StaffDisplayResolver.isPrimaryForElement(this.staff!, timeSignatureDisplay) && (!this.bar.previousBar || (this.bar.previousBar && this.bar.masterBar.timeSignatureNumerator !== @@ -302,7 +317,7 @@ export class NumberedBarRenderer extends LineBarRenderer { } protected override createPostBeatGlyphs(): void { - if (this._isOnlyNumbered) { + if (this.staff!.isCascadePrimary) { super.createPostBeatGlyphs(); } } diff --git a/packages/alphatab/src/rendering/NumberedBarRendererFactory.ts b/packages/alphatab/src/rendering/NumberedBarRendererFactory.ts index 50705e033..51dfeff1c 100644 --- a/packages/alphatab/src/rendering/NumberedBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/NumberedBarRendererFactory.ts @@ -11,10 +11,14 @@ import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer' * @internal */ export class NumberedBarRendererFactory extends BarRendererFactory { - public get staffId(): string { + public override get staffId(): string { return NumberedBarRenderer.StaffId; } + public override get cascadePriority(): number { + return 3; + } + public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { return new NumberedBarRenderer(renderer, bar); } diff --git a/packages/alphatab/src/rendering/ScoreBarRenderer.ts b/packages/alphatab/src/rendering/ScoreBarRenderer.ts index 4fd40f847..96afbb364 100644 --- a/packages/alphatab/src/rendering/ScoreBarRenderer.ts +++ b/packages/alphatab/src/rendering/ScoreBarRenderer.ts @@ -16,9 +16,12 @@ import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { KeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/KeySignatureGlyph'; import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; +import { StaffDisplayResolver } from '@coderline/alphatab/rendering/staves/StaffDisplayResolver'; import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; @@ -30,6 +33,7 @@ import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementS */ export class ScoreBarRenderer extends LineBarRenderer { public static readonly StaffId: string = 'score'; + private static _sharpKsSteps: number[] = [-1, 2, -2, 1, 4, 0, 3]; private static _flatKsSteps: number[] = [3, 0, 4, 1, 5, 2, 6]; @@ -40,6 +44,38 @@ export class ScoreBarRenderer extends LineBarRenderer { this.accidentalHelper = new AccidentalHelper(this); } + public override resolveClefDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + this.bar.scoreDisplay?.clef, + this.bar.staff.scoreConfig?.clef, + this.bar.staff.track.score.stylesheet.scoreConfig.clef + ); + } + + public override resolveKeySignatureDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + this.bar.scoreDisplay?.keySignature, + this.bar.staff.scoreConfig?.keySignature, + this.bar.staff.track.score.stylesheet.scoreConfig.keySignature + ); + } + + public override resolveTimeSignatureDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + this.bar.scoreDisplay?.timeSignature, + this.bar.staff.scoreConfig?.timeSignature, + this.bar.staff.track.score.stylesheet.scoreConfig.timeSignature + ); + } + + protected override resolveBarNumberDisplay(): BarNumberDisplay { + return ( + this.bar.scoreDisplay?.barNumber ?? + this.bar.staff.scoreConfig?.barNumber ?? + this.bar.staff.track.score.stylesheet.scoreConfig.barNumber! + ); + } + public override get repeatsBarSubElement(): BarSubElement { return BarSubElement.StandardNotationRepeats; } @@ -174,10 +210,12 @@ export class ScoreBarRenderer extends LineBarRenderer { protected override createLinePreBeatGlyphs(): void { // Clef let hasClef = false; + const clefDisplay = this.resolveClefDisplay(); if ( - this.isFirstOfStaff || - this.bar.clef !== this.bar.previousBar!.clef || - this.bar.clefOttava !== this.bar.previousBar!.clefOttava + StaffDisplayResolver.isPrimaryForElement(this.staff!, clefDisplay) && + (this.isFirstOfStaff || + this.bar.clef !== this.bar.previousBar!.clef || + this.bar.clefOttava !== this.bar.previousBar!.clefOttava) ) { // SMUFL: Clefs should be positioned such that the pitch the clef refers to is on the baseline // (e.g. the F clef is placed such that the upper dot is above and the lower dot below the baseline). @@ -208,25 +246,30 @@ export class ScoreBarRenderer extends LineBarRenderer { hasClef = true; } // Key signature + const keySignatureDisplay = this.resolveKeySignatureDisplay(); if ( - hasClef || - (this.index === 0 && this.bar.keySignature !== KeySignature.C) || - (this.bar.previousBar && this.bar.keySignature !== this.bar.previousBar.keySignature) + StaffDisplayResolver.isPrimaryForElement(this.staff!, keySignatureDisplay) && + (hasClef || + (this.index === 0 && this.bar.keySignature !== KeySignature.C) || + (this.bar.previousBar && this.bar.keySignature !== this.bar.previousBar.keySignature)) ) { this.createStartSpacing(); this._createKeySignatureGlyphs(); } // Time Signature + const timeSignatureDisplay = this.resolveTimeSignatureDisplay(); if ( - !this.bar.previousBar || - (this.bar.previousBar && - this.bar.masterBar.timeSignatureNumerator !== this.bar.previousBar.masterBar.timeSignatureNumerator) || - (this.bar.previousBar && - this.bar.masterBar.timeSignatureDenominator !== - this.bar.previousBar.masterBar.timeSignatureDenominator) || - (this.bar.previousBar && - this.bar.masterBar.isFreeTime && - this.bar.masterBar.isFreeTime !== this.bar.previousBar.masterBar.isFreeTime) + StaffDisplayResolver.isPrimaryForElement(this.staff!, timeSignatureDisplay) && + (!this.bar.previousBar || + (this.bar.previousBar && + this.bar.masterBar.timeSignatureNumerator !== + this.bar.previousBar.masterBar.timeSignatureNumerator) || + (this.bar.previousBar && + this.bar.masterBar.timeSignatureDenominator !== + this.bar.previousBar.masterBar.timeSignatureDenominator) || + (this.bar.previousBar && + this.bar.masterBar.isFreeTime && + this.bar.masterBar.isFreeTime !== this.bar.previousBar.masterBar.isFreeTime)) ) { this.createStartSpacing(); this._createTimeSignatureGlyphs(); diff --git a/packages/alphatab/src/rendering/ScoreBarRendererFactory.ts b/packages/alphatab/src/rendering/ScoreBarRendererFactory.ts index c1634bbcc..fee8f83af 100644 --- a/packages/alphatab/src/rendering/ScoreBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/ScoreBarRendererFactory.ts @@ -11,10 +11,14 @@ import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer' * @internal */ export class ScoreBarRendererFactory extends BarRendererFactory { - public get staffId(): string { + public override get staffId(): string { return ScoreBarRenderer.StaffId; } + public override get cascadePriority(): number { + return 0; + } + public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { return new ScoreBarRenderer(renderer, bar); } diff --git a/packages/alphatab/src/rendering/SlashBarRenderer.ts b/packages/alphatab/src/rendering/SlashBarRenderer.ts index 59e352e2e..e88efcf34 100644 --- a/packages/alphatab/src/rendering/SlashBarRenderer.ts +++ b/packages/alphatab/src/rendering/SlashBarRenderer.ts @@ -1,6 +1,8 @@ import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; import type { Note } from '@coderline/alphatab/model/Note'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; import type { Voice } from '@coderline/alphatab/model/Voice'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { LineBarRenderer } from '@coderline/alphatab/rendering//LineBarRenderer'; @@ -10,6 +12,7 @@ import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/Sc import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { SlashBeatContainerGlyph } from '@coderline/alphatab/rendering/SlashBeatContainerGlyph'; +import { StaffDisplayResolver } from '@coderline/alphatab/rendering/staves/StaffDisplayResolver'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; @@ -22,15 +25,36 @@ export class SlashBarRenderer extends LineBarRenderer { public static readonly StaffId: string = 'slash'; public simpleWhammyOverflow: number = 0; - private _isOnlySlash: boolean; public constructor(renderer: ScoreRenderer, bar: Bar) { super(renderer, bar); - // ignore numbered notation here - this._isOnlySlash = !bar.staff.showTablature && !bar.staff.showStandardNotation; this.helpers.preferredBeamDirection = BeamDirection.Up; } + public override resolveKeySignatureDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + this.bar.slashDisplay?.keySignature, + this.bar.staff.slashConfig?.keySignature, + this.bar.staff.track.score.stylesheet.slashConfig.keySignature + ); + } + + public override resolveTimeSignatureDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + this.bar.slashDisplay?.timeSignature, + this.bar.staff.slashConfig?.timeSignature, + this.bar.staff.track.score.stylesheet.slashConfig.timeSignature + ); + } + + protected override resolveBarNumberDisplay(): BarNumberDisplay { + return ( + this.bar.slashDisplay?.barNumber ?? + this.bar.staff.slashConfig?.barNumber ?? + this.bar.staff.track.score.stylesheet.slashConfig.barNumber! + ); + } + public override get repeatsBarSubElement(): BarSubElement { return BarSubElement.SlashRepeats; } @@ -125,9 +149,9 @@ export class SlashBarRenderer extends LineBarRenderer { } protected override createLinePreBeatGlyphs(): void { - // Key signature + const timeSignatureDisplay = this.resolveTimeSignatureDisplay(); if ( - this._isOnlySlash && + StaffDisplayResolver.isPrimaryForElement(this.staff!, timeSignatureDisplay) && (!this.bar.previousBar || (this.bar.previousBar && this.bar.masterBar.timeSignatureNumerator !== diff --git a/packages/alphatab/src/rendering/SlashBarRendererFactory.ts b/packages/alphatab/src/rendering/SlashBarRendererFactory.ts index 24b6ef53d..01e2fb7bc 100644 --- a/packages/alphatab/src/rendering/SlashBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/SlashBarRendererFactory.ts @@ -11,10 +11,14 @@ import { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRenderer * @internal */ export class SlashBarRendererFactory extends BarRendererFactory { - public get staffId(): string { + public override get staffId(): string { return SlashBarRenderer.StaffId; } + public override get cascadePriority(): number { + return 2; + } + public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { return new SlashBarRenderer(renderer, bar); } diff --git a/packages/alphatab/src/rendering/TabBarRenderer.ts b/packages/alphatab/src/rendering/TabBarRenderer.ts index e420ed510..60abc06ea 100644 --- a/packages/alphatab/src/rendering/TabBarRenderer.ts +++ b/packages/alphatab/src/rendering/TabBarRenderer.ts @@ -18,7 +18,9 @@ import { TabClefGlyph } from '@coderline/alphatab/rendering/glyphs/TabClefGlyph' import type { TabNoteChordGlyph } from '@coderline/alphatab/rendering/glyphs/TabNoteChordGlyph'; import { TabTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/TabTimeSignatureGlyph'; import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; +import { StaffDisplayResolver } from '@coderline/alphatab/rendering/staves/StaffDisplayResolver'; import type { ReservedLayoutAreaSlot } from '@coderline/alphatab/rendering/utils/BarCollisionHelper'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; @@ -33,11 +35,53 @@ export class TabBarRenderer extends LineBarRenderer { private _hasTuplets = false; - public showTimeSignature: boolean = false; - public showRests: boolean = false; - public showTiedNotes: boolean = false; + public override resolveClefDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + this.bar.tabDisplay?.clef, + this.bar.staff.tabConfig?.clef, + this.bar.staff.track.score.stylesheet.tabConfig.clef + ); + } - private _showMultiBarRest: boolean = false; + public override resolveTimeSignatureDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + this.bar.tabDisplay?.timeSignature, + this.bar.staff.tabConfig?.timeSignature, + this.bar.staff.track.score.stylesheet.tabConfig.timeSignature + ); + } + + public override resolveRestsDisplay(): ElementDisplay { + return StaffDisplayResolver.merge( + undefined, + this.bar.staff.tabConfig?.rests, + this.bar.staff.track.score.stylesheet.tabConfig.rests + ); + } + + public override resolveRhythm(): TabRhythmMode { + return this.bar.staff.tabConfig?.rhythm ?? this.bar.staff.track.score.stylesheet.tabConfig.rhythm!; + } + + protected override resolveBarNumberDisplay(): BarNumberDisplay { + return ( + this.bar.tabDisplay?.barNumber ?? + this.bar.staff.tabConfig?.barNumber ?? + this.bar.staff.track.score.stylesheet.tabConfig.barNumber! + ); + } + + public get showTimeSignature(): boolean { + return StaffDisplayResolver.isPrimaryForElement(this.staff!, this.resolveTimeSignatureDisplay()); + } + + public get showRests(): boolean { + return StaffDisplayResolver.isPrimaryForElement(this.staff!, this.resolveRestsDisplay()); + } + + public get showTiedNotes(): boolean { + return this.staff!.isCascadePrimary; + } /** * Layout-time staff-line gap cache, bucketed by string-line index. @@ -52,7 +96,7 @@ export class TabBarRenderer extends LineBarRenderer { private _gapCount: number = 0; public override get showMultiBarRest(): boolean { - return this._showMultiBarRest; + return this.staff!.isCascadePrimary; } public override get repeatsBarSubElement(): BarSubElement { @@ -324,16 +368,6 @@ export class TabBarRenderer extends LineBarRenderer { } public override doLayout(): void { - const hasStandardNotation = - this.bar.staff.showStandardNotation && this.scoreRenderer.layout!.profile.has(ScoreBarRenderer.StaffId); - - if (!hasStandardNotation) { - this.showTimeSignature = true; - this.showRests = true; - this.showTiedNotes = true; - this._showMultiBarRest = true; - } - super.doLayout(); const hasNoteOnTopString = this.minString === 0; @@ -382,14 +416,16 @@ export class TabBarRenderer extends LineBarRenderer { protected override createLinePreBeatGlyphs(): void { // Clef - if (this.isFirstOfStaff) { + const clefDisplay = this.resolveClefDisplay(); + if (StaffDisplayResolver.isPrimaryForElement(this.staff!, clefDisplay) && this.isFirstOfStaff) { const center: number = (this.bar.staff.tuning.length - 1) / 2; this.createStartSpacing(); this.addPreBeatGlyph(new TabClefGlyph(0, this.getLineY(center))); } // Time Signature + const timeSignatureDisplay = this.resolveTimeSignatureDisplay(); if ( - this.showTimeSignature && + StaffDisplayResolver.isPrimaryForElement(this.staff!, timeSignatureDisplay) && (!this.bar.previousBar || (this.bar.previousBar && this.bar.masterBar.timeSignatureNumerator !== diff --git a/packages/alphatab/src/rendering/TabBarRendererFactory.ts b/packages/alphatab/src/rendering/TabBarRendererFactory.ts index df2521c38..54ca6d3c7 100644 --- a/packages/alphatab/src/rendering/TabBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/TabBarRendererFactory.ts @@ -11,10 +11,14 @@ import { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; * @internal */ export class TabBarRendererFactory extends BarRendererFactory { - public get staffId(): string { + public override get staffId(): string { return TabBarRenderer.StaffId; } + public override get cascadePriority(): number { + return 1; + } + public constructor(effectBands: EffectBandInfo[]) { super(effectBands); this.hideOnPercussionTrack = true; diff --git a/packages/alphatab/src/rendering/staves/RenderStaff.ts b/packages/alphatab/src/rendering/staves/RenderStaff.ts index 9d4ba737d..a63194e9f 100644 --- a/packages/alphatab/src/rendering/staves/RenderStaff.ts +++ b/packages/alphatab/src/rendering/staves/RenderStaff.ts @@ -11,6 +11,7 @@ import { import { EffectSystemPlacement } from '@coderline/alphatab/rendering/EffectSystemPlacement'; import { StaffSystemSkyline } from '@coderline/alphatab/rendering/skyline/StaffSystemSkyline'; import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; +import { type IStaffDisplayContext, StaffDisplayResolver } from '@coderline/alphatab/rendering/staves/StaffDisplayResolver'; import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; import type { StaffTrackGroup } from '@coderline/alphatab/rendering/staves/StaffTrackGroup'; @@ -19,7 +20,7 @@ import type { StaffTrackGroup } from '@coderline/alphatab/rendering/staves/Staff * It stores BarRenderer instances created from a given factory. * @internal */ -export class RenderStaff { +export class RenderStaff implements IStaffDisplayContext { private _factory: BarRendererFactory; private _sharedLayoutData: Map = new Map(); @@ -55,6 +56,28 @@ export class RenderStaff { return this._factory.staffId; } + public get cascadePriority(): number { + return this._factory.cascadePriority; + } + + private _isCascadePrimary: boolean = false; + private _isCascadePrimaryComputed: boolean = false; + public get isCascadePrimary(): boolean { + if (!this._isCascadePrimaryComputed) { + this._isCascadePrimary = StaffDisplayResolver.computeCascadePrimary(this); + this._isCascadePrimaryComputed = true; + } + return this._isCascadePrimary; + } + + public get systemIndex(): number { + return this.system.index; + } + + public get cascadeSiblings(): Iterable { + return this.staffTrackGroup.staves; + } + /** * This is the visual offset from top where the * Staff contents actually start. Used for grouping diff --git a/packages/alphatab/src/rendering/staves/StaffDisplayResolver.ts b/packages/alphatab/src/rendering/staves/StaffDisplayResolver.ts new file mode 100644 index 000000000..67fdebac0 --- /dev/null +++ b/packages/alphatab/src/rendering/staves/StaffDisplayResolver.ts @@ -0,0 +1,88 @@ +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import type { Staff } from '@coderline/alphatab/model/Staff'; + +/** + * Per-staff view required by {@link StaffDisplayResolver} to evaluate + * placement decisions. Exposed as an interface so unit tests can supply + * lightweight stand-ins without instantiating a full render pipeline. + * {@link RenderStaff} implements this directly. + * @internal + */ +export interface IStaffDisplayContext { + readonly modelStaff: Staff; + readonly cascadePriority: number; + readonly systemIndex: number; + readonly isCascadePrimary: boolean; + readonly cascadeSiblings: Iterable; +} + +/** + * Helpers for the staff-placement cascade and the per-axis + * {@link ElementDisplay} merge. Per-element resolution lives on the + * renderer subclasses themselves. + * @internal + */ +export class StaffDisplayResolver { + private static readonly _fallback: ElementDisplay = { + isVisible: true, + staffPlacement: StaffPlacement.AllStaves, + systemDisplay: SystemDisplay.AllSystems + }; + + /** + * Per-axis fall-through: first defined value walking + * per-bar → per-staff → score-wide → {@link _fallback}. + */ + public static merge( + perBar: ElementDisplay | undefined, + perStaff: ElementDisplay | undefined, + stylesheet: ElementDisplay | undefined + ): ElementDisplay { + const fallback = StaffDisplayResolver._fallback; + return { + isVisible: + perBar?.isVisible ?? perStaff?.isVisible ?? stylesheet?.isVisible ?? fallback.isVisible, + staffPlacement: + perBar?.staffPlacement ?? + perStaff?.staffPlacement ?? + stylesheet?.staffPlacement ?? + fallback.staffPlacement, + systemDisplay: + perBar?.systemDisplay ?? + perStaff?.systemDisplay ?? + stylesheet?.systemDisplay ?? + fallback.systemDisplay + }; + } + + public static isPrimaryForElement(staff: IStaffDisplayContext, display: ElementDisplay): boolean { + if (display.isVisible === false) { + return false; + } + if (display.systemDisplay === SystemDisplay.FirstSystemOnly && staff.systemIndex !== 0) { + return false; + } + switch (display.staffPlacement) { + case StaffPlacement.AllStaves: + return true; + case StaffPlacement.Primary: + return staff.isCascadePrimary; + } + return true; + } + + public static computeCascadePrimary(staff: IStaffDisplayContext): boolean { + const modelStaff = staff.modelStaff; + let primary = staff; + for (const sibling of staff.cascadeSiblings) { + if (sibling === staff || sibling.modelStaff !== modelStaff) { + continue; + } + if (sibling.cascadePriority < primary.cascadePriority) { + primary = sibling; + } + } + return primary === staff; + } +} diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-all-four.png b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-all-four.png new file mode 100644 index 000000000..44e99706f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-all-four.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-numbered-only.png b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-numbered-only.png new file mode 100644 index 000000000..2e57b3549 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-numbered-only.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-score-tab.png b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-score-tab.png new file mode 100644 index 000000000..832f2df5f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-score-tab.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-slash-numbered.png b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-slash-numbered.png new file mode 100644 index 000000000..c03abfd62 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-slash-numbered.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-tab-only.png b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-tab-only.png new file mode 100644 index 000000000..8915ddbe0 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-tab-only.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-tab-slash.png b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-tab-slash.png new file mode 100644 index 000000000..c67e03ad6 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/cascade-tab-slash.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/override-l1-ks-change-hide.png b/packages/alphatab/test-data/visual-tests/staff-display-config/override-l1-ks-change-hide.png new file mode 100644 index 000000000..1aa8cddf2 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/override-l1-ks-change-hide.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/override-l1-ts-hide-firstbar.png b/packages/alphatab/test-data/visual-tests/staff-display-config/override-l1-ts-hide-firstbar.png new file mode 100644 index 000000000..81e2634f5 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/override-l1-ts-hide-firstbar.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/override-l2-score-clef-hide.png b/packages/alphatab/test-data/visual-tests/staff-display-config/override-l2-score-clef-hide.png new file mode 100644 index 000000000..2567af499 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/override-l2-score-clef-hide.png differ diff --git a/packages/alphatab/test-data/visual-tests/staff-display-config/override-l2-tab-ts-hide.png b/packages/alphatab/test-data/visual-tests/staff-display-config/override-l2-tab-ts-hide.png new file mode 100644 index 000000000..656578704 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/staff-display-config/override-l2-tab-ts-hide.png differ diff --git a/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap index 11e20342c..88982a96c 100644 --- a/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap @@ -794,6 +794,18 @@ Map { }, ], "barnumberdisplay" => 2, + "scoredisplay" => Map { + "barnumber" => 2, + }, + "tabdisplay" => Map { + "barnumber" => 2, + }, + "slashdisplay" => Map { + "barnumber" => 2, + }, + "numbereddisplay" => Map { + "barnumber" => 2, + }, }, Map { "__kind" => "Bar", @@ -852,6 +864,18 @@ Map { }, ], "barnumberdisplay" => 2, + "scoredisplay" => Map { + "barnumber" => 2, + }, + "tabdisplay" => Map { + "barnumber" => 2, + }, + "slashdisplay" => Map { + "barnumber" => 2, + }, + "numbereddisplay" => Map { + "barnumber" => 2, + }, }, Map { "__kind" => "Bar", @@ -918,6 +942,18 @@ Map { }, ], "barnumberdisplay" => 2, + "scoredisplay" => Map { + "barnumber" => 2, + }, + "tabdisplay" => Map { + "barnumber" => 2, + }, + "slashdisplay" => Map { + "barnumber" => 2, + }, + "numbereddisplay" => Map { + "barnumber" => 2, + }, }, Map { "__kind" => "Bar", @@ -976,6 +1012,18 @@ Map { }, ], "barnumberdisplay" => 2, + "scoredisplay" => Map { + "barnumber" => 2, + }, + "tabdisplay" => Map { + "barnumber" => 2, + }, + "slashdisplay" => Map { + "barnumber" => 2, + }, + "numbereddisplay" => Map { + "barnumber" => 2, + }, }, Map { "__kind" => "Bar", @@ -3036,6 +3084,18 @@ Map { }, ], "barnumberdisplay" => 2, + "scoredisplay" => Map { + "barnumber" => 2, + }, + "tabdisplay" => Map { + "barnumber" => 2, + }, + "slashdisplay" => Map { + "barnumber" => 2, + }, + "numbereddisplay" => Map { + "barnumber" => 2, + }, }, Map { "__kind" => "Bar", @@ -3094,6 +3154,18 @@ Map { }, ], "barnumberdisplay" => 2, + "scoredisplay" => Map { + "barnumber" => 2, + }, + "tabdisplay" => Map { + "barnumber" => 2, + }, + "slashdisplay" => Map { + "barnumber" => 2, + }, + "numbereddisplay" => Map { + "barnumber" => 2, + }, }, Map { "__kind" => "Bar", @@ -3160,6 +3232,18 @@ Map { }, ], "barnumberdisplay" => 2, + "scoredisplay" => Map { + "barnumber" => 2, + }, + "tabdisplay" => Map { + "barnumber" => 2, + }, + "slashdisplay" => Map { + "barnumber" => 2, + }, + "numbereddisplay" => Map { + "barnumber" => 2, + }, }, Map { "__kind" => "Bar", @@ -3218,6 +3302,18 @@ Map { }, ], "barnumberdisplay" => 2, + "scoredisplay" => Map { + "barnumber" => 2, + }, + "tabdisplay" => Map { + "barnumber" => 2, + }, + "slashdisplay" => Map { + "barnumber" => 2, + }, + "numbereddisplay" => Map { + "barnumber" => 2, + }, }, Map { "__kind" => "Bar", diff --git a/packages/alphatab/test/model/StaffConfiguration.test.ts b/packages/alphatab/test/model/StaffConfiguration.test.ts new file mode 100644 index 000000000..308819829 --- /dev/null +++ b/packages/alphatab/test/model/StaffConfiguration.test.ts @@ -0,0 +1,246 @@ +import { describe, expect, it } from 'vitest'; +import { BarSerializer } from '@coderline/alphatab/generated/model/BarSerializer'; +import { RenderStylesheetSerializer } from '@coderline/alphatab/generated/model/RenderStylesheetSerializer'; +import { StaffSerializer } from '@coderline/alphatab/generated/model/StaffSerializer'; +import { Bar } from '@coderline/alphatab/model/Bar'; +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { BarNumberDisplay, RenderStylesheet } from '@coderline/alphatab/model/RenderStylesheet'; +import { Staff } from '@coderline/alphatab/model/Staff'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; + +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; + +/** + * @internal + */ +function expectElementDisplay( + actual: ElementDisplay | undefined, + isVisible: boolean | undefined, + staffPlacement: StaffPlacement | undefined, + systemDisplay: SystemDisplay | undefined +): void { + expect(actual).not.toBeUndefined(); + expect(actual!.isVisible).toBe(isVisible); + expect(actual!.staffPlacement).toBe(staffPlacement); + expect(actual!.systemDisplay).toBe(systemDisplay); +} + +describe('RenderStylesheet L3 historical defaults', () => { + it('scoreConfig defaults', () => { + const rs = new RenderStylesheet(); + expectElementDisplay(rs.scoreConfig.clef, true, StaffPlacement.AllStaves, SystemDisplay.AllSystems); + expectElementDisplay(rs.scoreConfig.keySignature, true, StaffPlacement.AllStaves, SystemDisplay.AllSystems); + expectElementDisplay(rs.scoreConfig.timeSignature, true, StaffPlacement.AllStaves, SystemDisplay.AllSystems); + expect(rs.scoreConfig.barNumber).toBe(BarNumberDisplay.AllBars); + }); + + it('tabConfig defaults', () => { + const rs = new RenderStylesheet(); + expectElementDisplay(rs.tabConfig.clef, true, StaffPlacement.AllStaves, SystemDisplay.AllSystems); + expectElementDisplay(rs.tabConfig.timeSignature, true, StaffPlacement.Primary, SystemDisplay.AllSystems); + expect(rs.tabConfig.barNumber).toBe(BarNumberDisplay.AllBars); + expect(rs.tabConfig.rhythm).toBe(TabRhythmMode.Automatic); + expectElementDisplay(rs.tabConfig.rests, true, StaffPlacement.Primary, undefined); + }); + + it('slashConfig defaults', () => { + const rs = new RenderStylesheet(); + expectElementDisplay(rs.slashConfig.keySignature, false, undefined, undefined); + expectElementDisplay(rs.slashConfig.timeSignature, true, StaffPlacement.Primary, SystemDisplay.AllSystems); + expect(rs.slashConfig.barNumber).toBe(BarNumberDisplay.AllBars); + }); + + it('numberedConfig defaults', () => { + const rs = new RenderStylesheet(); + expectElementDisplay(rs.numberedConfig.timeSignature, true, StaffPlacement.Primary, SystemDisplay.AllSystems); + expect(rs.numberedConfig.barNumber).toBe(BarNumberDisplay.AllBars); + }); +}); + +describe('Staff L2 fields default to undefined', () => { + it('all four *Config fields are undefined on a fresh Staff', () => { + const staff = new Staff(); + expect(staff.scoreConfig).toBeUndefined(); + expect(staff.tabConfig).toBeUndefined(); + expect(staff.slashConfig).toBeUndefined(); + expect(staff.numberedConfig).toBeUndefined(); + }); +}); + +describe('Bar L1 fields default to undefined', () => { + it('all four *Display fields are undefined on a fresh Bar', () => { + const bar = new Bar(); + expect(bar.scoreDisplay).toBeUndefined(); + expect(bar.tabDisplay).toBeUndefined(); + expect(bar.slashDisplay).toBeUndefined(); + expect(bar.numberedDisplay).toBeUndefined(); + }); +}); + +describe('RenderStylesheet.barNumberDisplay shim (ADR-006 §1)', () => { + it('getter reads from scoreConfig.barNumber', () => { + const rs = new RenderStylesheet(); + expect(rs.barNumberDisplay).toBe(BarNumberDisplay.AllBars); + rs.scoreConfig.barNumber = BarNumberDisplay.Hide; + expect(rs.barNumberDisplay).toBe(BarNumberDisplay.Hide); + }); + + it('setter broadcasts to all four staff-type L3 entries', () => { + const rs = new RenderStylesheet(); + rs.barNumberDisplay = BarNumberDisplay.FirstOfSystem; + expect(rs.scoreConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(rs.tabConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(rs.slashConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(rs.numberedConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + }); +}); + +describe('Bar.barNumberDisplay shim (ADR-006 §2)', () => { + it('getter returns undefined when no scoreDisplay override exists', () => { + const bar = new Bar(); + expect(bar.barNumberDisplay).toBeUndefined(); + }); + + it('getter returns scoreDisplay.barNumber when present', () => { + const bar = new Bar(); + bar.scoreDisplay = { barNumber: BarNumberDisplay.Hide }; + expect(bar.barNumberDisplay).toBe(BarNumberDisplay.Hide); + }); + + it('setter with concrete value lazy-creates each *Display bag and broadcasts', () => { + const bar = new Bar(); + bar.barNumberDisplay = BarNumberDisplay.FirstOfSystem; + expect(bar.scoreDisplay).not.toBeUndefined(); + expect(bar.scoreDisplay!.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(bar.tabDisplay).not.toBeUndefined(); + expect(bar.tabDisplay!.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(bar.slashDisplay).not.toBeUndefined(); + expect(bar.slashDisplay!.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(bar.numberedDisplay).not.toBeUndefined(); + expect(bar.numberedDisplay!.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + }); + + it('setter with undefined clears barNumber on each existing bag without deleting the bag', () => { + const bar = new Bar(); + bar.barNumberDisplay = BarNumberDisplay.Hide; + bar.barNumberDisplay = undefined; + expect(bar.scoreDisplay).not.toBeUndefined(); + expect(bar.scoreDisplay!.barNumber).toBeUndefined(); + expect(bar.tabDisplay).not.toBeUndefined(); + expect(bar.tabDisplay!.barNumber).toBeUndefined(); + expect(bar.slashDisplay).not.toBeUndefined(); + expect(bar.slashDisplay!.barNumber).toBeUndefined(); + expect(bar.numberedDisplay).not.toBeUndefined(); + expect(bar.numberedDisplay!.barNumber).toBeUndefined(); + }); + + it('setter with undefined preserves other L1 element overrides on the same bag', () => { + const bar = new Bar(); + bar.scoreDisplay = { timeSignature: { isVisible: false } }; + bar.barNumberDisplay = BarNumberDisplay.AllBars; + bar.barNumberDisplay = undefined; + expect(bar.scoreDisplay).not.toBeUndefined(); + expect(bar.scoreDisplay!.barNumber).toBeUndefined(); + expectElementDisplay(bar.scoreDisplay!.timeSignature, false, undefined, undefined); + }); +}); + +describe('JSON round-trip for the new staff-config surface', () => { + function roundtripStylesheet(rs: RenderStylesheet): RenderStylesheet { + const out = RenderStylesheetSerializer.toJson(rs)!; + const result = new RenderStylesheet(); + RenderStylesheetSerializer.fromJson(result, out); + return result; + } + + function roundtripStaff(staff: Staff): Staff { + const out = StaffSerializer.toJson(staff)!; + const result = new Staff(); + StaffSerializer.fromJson(result, out); + return result; + } + + function roundtripBar(bar: Bar): Bar { + const out = BarSerializer.toJson(bar)!; + const result = new Bar(); + BarSerializer.fromJson(result, out); + return result; + } + + it('preserves RenderStylesheet L3 historical defaults', () => { + const rs = roundtripStylesheet(new RenderStylesheet()); + expectElementDisplay(rs.scoreConfig.clef, true, StaffPlacement.AllStaves, SystemDisplay.AllSystems); + expect(rs.tabConfig.rhythm).toBe(TabRhythmMode.Automatic); + expectElementDisplay(rs.tabConfig.rests, true, StaffPlacement.Primary, undefined); + expectElementDisplay(rs.numberedConfig.timeSignature, true, StaffPlacement.Primary, SystemDisplay.AllSystems); + expect(rs.scoreConfig.barNumber).toBe(BarNumberDisplay.AllBars); + }); + + it('preserves RenderStylesheet L3 author overrides', () => { + const rs = new RenderStylesheet(); + rs.tabConfig = { + clef: { isVisible: false }, + timeSignature: { systemDisplay: SystemDisplay.FirstSystemOnly }, + barNumber: BarNumberDisplay.FirstOfSystem, + rhythm: TabRhythmMode.ShowWithBeams, + rests: { isVisible: false } + }; + const out = roundtripStylesheet(rs); + expectElementDisplay(out.tabConfig.clef, false, undefined, undefined); + expectElementDisplay(out.tabConfig.timeSignature, undefined, undefined, SystemDisplay.FirstSystemOnly); + expect(out.tabConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(out.tabConfig.rhythm).toBe(TabRhythmMode.ShowWithBeams); + expectElementDisplay(out.tabConfig.rests, false, undefined, undefined); + }); + + it('preserves Staff L2 overrides; undefined L2 stays undefined', () => { + const staff = new Staff(); + staff.tabConfig = { clef: { isVisible: false } }; + const out = roundtripStaff(staff); + expect(out.tabConfig).not.toBeUndefined(); + expectElementDisplay(out.tabConfig!.clef, false, undefined, undefined); + expect(out.scoreConfig).toBeUndefined(); + expect(out.slashConfig).toBeUndefined(); + expect(out.numberedConfig).toBeUndefined(); + }); + + it('preserves Bar L1 overrides; undefined L1 stays undefined', () => { + const bar = new Bar(); + bar.scoreDisplay = { timeSignature: { isVisible: false } }; + const out = roundtripBar(bar); + expect(out.scoreDisplay).not.toBeUndefined(); + expectElementDisplay(out.scoreDisplay!.timeSignature, false, undefined, undefined); + expect(out.tabDisplay).toBeUndefined(); + expect(out.slashDisplay).toBeUndefined(); + expect(out.numberedDisplay).toBeUndefined(); + }); + + it('preserves per-axis sparseness in ElementDisplay', () => { + const rs = new RenderStylesheet(); + rs.scoreConfig.clef = { isVisible: true }; + const out = roundtripStylesheet(rs); + expectElementDisplay(out.scoreConfig.clef, true, undefined, undefined); + }); + + it('preserves RenderStylesheet.barNumberDisplay shim through round-trip', () => { + const rs = new RenderStylesheet(); + rs.barNumberDisplay = BarNumberDisplay.FirstOfSystem; + const out = roundtripStylesheet(rs); + expect(out.barNumberDisplay).toBe(BarNumberDisplay.FirstOfSystem); + expect(out.scoreConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(out.tabConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(out.slashConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + expect(out.numberedConfig.barNumber).toBe(BarNumberDisplay.FirstOfSystem); + }); + + it('preserves Bar.barNumberDisplay shim through round-trip', () => { + const bar = new Bar(); + bar.barNumberDisplay = BarNumberDisplay.Hide; + const out = roundtripBar(bar); + expect(out.barNumberDisplay).toBe(BarNumberDisplay.Hide); + expect(out.scoreDisplay!.barNumber).toBe(BarNumberDisplay.Hide); + expect(out.tabDisplay!.barNumber).toBe(BarNumberDisplay.Hide); + expect(out.slashDisplay!.barNumber).toBe(BarNumberDisplay.Hide); + expect(out.numberedDisplay!.barNumber).toBe(BarNumberDisplay.Hide); + }); +}); diff --git a/packages/alphatab/test/rendering/StaffDisplayResolver.test.ts b/packages/alphatab/test/rendering/StaffDisplayResolver.test.ts new file mode 100644 index 000000000..23352deb4 --- /dev/null +++ b/packages/alphatab/test/rendering/StaffDisplayResolver.test.ts @@ -0,0 +1,187 @@ +import type { ElementDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { Staff } from '@coderline/alphatab/model/Staff'; +import { type IStaffDisplayContext, StaffDisplayResolver } from '@coderline/alphatab/rendering/staves/StaffDisplayResolver'; +import { describe, expect, it } from 'vitest'; + +/** + * Lightweight {@link IStaffDisplayContext} implementation used to drive + * {@link StaffDisplayResolver} without a full render pipeline. Sibling + * arrays are shared by reference across peers in the same group. + * @internal + */ +class StaffDisplayContextStub implements IStaffDisplayContext { + public modelStaff: Staff; + public cascadePriority: number; + public systemIndex: number; + + private _siblings: IStaffDisplayContext[] = []; + private _cachedPrimary: boolean = false; + private _cachedPrimaryComputed: boolean = false; + + public constructor(modelStaff: Staff, cascadePriority: number, systemIndex: number = 0) { + this.modelStaff = modelStaff; + this.cascadePriority = cascadePriority; + this.systemIndex = systemIndex; + } + + public get cascadeSiblings(): Iterable { + return this._siblings; + } + + public setSiblings(siblings: IStaffDisplayContext[]): void { + this._siblings = siblings; + } + + public get isCascadePrimary(): boolean { + if (!this._cachedPrimaryComputed) { + this._cachedPrimary = StaffDisplayResolver.computeCascadePrimary(this); + this._cachedPrimaryComputed = true; + } + return this._cachedPrimary; + } +} + +/** + * @internal + */ +class StaffDisplayContextSpec { + public cascadePriority: number; + public modelStaff: Staff; + public systemIndex: number; + + public constructor(cascadePriority: number, modelStaff: Staff, systemIndex: number = 0) { + this.cascadePriority = cascadePriority; + this.modelStaff = modelStaff; + this.systemIndex = systemIndex; + } +} + +/** + * @internal + */ +class StaffDisplayContextFixtures { + /** + * Build a {@link IStaffDisplayContext}-shaped stub group. Each entry in + * `siblings` becomes a peer sharing the same sibling array; the + * focused stub is returned at `siblings[focusIndex]`. + */ + public static makeGroup(focusIndex: number, siblings: StaffDisplayContextSpec[]): IStaffDisplayContext { + const staves: StaffDisplayContextStub[] = []; + for (const s of siblings) { + staves.push(new StaffDisplayContextStub(s.modelStaff, s.cascadePriority, s.systemIndex)); + } + const sharedSiblings: IStaffDisplayContext[] = []; + for (const staff of staves) { + sharedSiblings.push(staff); + } + for (const staff of staves) { + staff.setSiblings(sharedSiblings); + } + return staves[focusIndex]; + } +} + +describe('StaffDisplayResolver.merge', () => { + it('returns fallback when every layer leaves all axes undefined', () => { + const display: ElementDisplay = StaffDisplayResolver.merge(undefined, undefined, undefined); + expect(display.isVisible).toBe(true); + expect(display.staffPlacement).toBe(StaffPlacement.AllStaves); + expect(display.systemDisplay).toBe(SystemDisplay.AllSystems); + }); + + it('walks per-bar → per-staff → stylesheet → fallback per-axis', () => { + const perBar: ElementDisplay = { isVisible: false }; + const perStaff: ElementDisplay = { staffPlacement: StaffPlacement.Primary }; + const stylesheet: ElementDisplay = { systemDisplay: SystemDisplay.FirstSystemOnly }; + const display: ElementDisplay = StaffDisplayResolver.merge(perBar, perStaff, stylesheet); + expect(display.isVisible).toBe(false); + expect(display.staffPlacement).toBe(StaffPlacement.Primary); + expect(display.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); + }); + + it('earlier defined value wins over later layers', () => { + const perBar: ElementDisplay = { isVisible: false }; + const perStaff: ElementDisplay = { isVisible: true, staffPlacement: StaffPlacement.Primary }; + const stylesheet: ElementDisplay = { isVisible: true, staffPlacement: StaffPlacement.AllStaves }; + const display: ElementDisplay = StaffDisplayResolver.merge(perBar, perStaff, stylesheet); + expect(display.isVisible).toBe(false); + expect(display.staffPlacement).toBe(StaffPlacement.Primary); + }); +}); + +describe('StaffDisplayResolver.isPrimaryForElement', () => { + const modelStaffA: Staff = new Staff(); + const modelStaffB: Staff = new Staff(); + + function scoreStub(focusIndex: number): IStaffDisplayContext { + return StaffDisplayContextFixtures.makeGroup(focusIndex, [ + new StaffDisplayContextSpec(0, modelStaffA), + new StaffDisplayContextSpec(1, modelStaffA) + ]); + } + + it('returns false when isVisible is false', () => { + const staff: IStaffDisplayContext = scoreStub(0); + const display: ElementDisplay = { isVisible: false }; + expect(StaffDisplayResolver.isPrimaryForElement(staff, display)).toBe(false); + }); + + it('suppresses paint on systems with index != 0 when systemDisplay is FirstSystemOnly', () => { + const focus: IStaffDisplayContext = StaffDisplayContextFixtures.makeGroup(0, [ + new StaffDisplayContextSpec(0, modelStaffA, 1) + ]); + const display: ElementDisplay = { + isVisible: true, + staffPlacement: StaffPlacement.AllStaves, + systemDisplay: SystemDisplay.FirstSystemOnly + }; + expect(StaffDisplayResolver.isPrimaryForElement(focus, display)).toBe(false); + }); + + it('AllStaves paints on every staff regardless of cascade winner', () => { + const display: ElementDisplay = { + isVisible: true, + staffPlacement: StaffPlacement.AllStaves, + systemDisplay: SystemDisplay.AllSystems + }; + expect(StaffDisplayResolver.isPrimaryForElement(scoreStub(0), display)).toBe(true); + expect(StaffDisplayResolver.isPrimaryForElement(scoreStub(1), display)).toBe(true); + }); + + it('Primary paints only on the cascade winner among siblings sharing the model Staff', () => { + const display: ElementDisplay = { + isVisible: true, + staffPlacement: StaffPlacement.Primary, + systemDisplay: SystemDisplay.AllSystems + }; + expect(StaffDisplayResolver.isPrimaryForElement(scoreStub(0), display)).toBe(true); + expect(StaffDisplayResolver.isPrimaryForElement(scoreStub(1), display)).toBe(false); + }); + + it('cascade evaluates per model Staff — different model staves elect independent primaries', () => { + const display: ElementDisplay = { + isVisible: true, + staffPlacement: StaffPlacement.Primary, + systemDisplay: SystemDisplay.AllSystems + }; + const group: StaffDisplayContextSpec[] = [ + new StaffDisplayContextSpec(0, modelStaffA), + new StaffDisplayContextSpec(1, modelStaffA), + new StaffDisplayContextSpec(0, modelStaffB), + new StaffDisplayContextSpec(1, modelStaffB) + ]; + expect(StaffDisplayResolver.isPrimaryForElement(StaffDisplayContextFixtures.makeGroup(0, group), display)).toBe( + true + ); + expect(StaffDisplayResolver.isPrimaryForElement(StaffDisplayContextFixtures.makeGroup(1, group), display)).toBe( + false + ); + expect(StaffDisplayResolver.isPrimaryForElement(StaffDisplayContextFixtures.makeGroup(2, group), display)).toBe( + true + ); + expect(StaffDisplayResolver.isPrimaryForElement(StaffDisplayContextFixtures.makeGroup(3, group), display)).toBe( + false + ); + }); +}); diff --git a/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts b/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts new file mode 100644 index 000000000..191261850 --- /dev/null +++ b/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts @@ -0,0 +1,159 @@ +import { describe, it } from 'vitest'; + +import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; + +describe('StaffDisplayConfig', () => { + describe('Primary cascade — multi-notation single-track stacks', () => { + it('cascade-score-tab', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {score tabs} + \\ks D + 3.3.4 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/cascade-score-tab.png' + ); + }); + + it('cascade-tab-only', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {tabs} + 3.3.4 3.3 r.4 3.3 | + 3.3 -.3 3.3 r.4 | + `, + 'test-data/visual-tests/staff-display-config/cascade-tab-only.png' + ); + }); + + it('cascade-tab-slash', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {tabs slash} + 3.3.4 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/cascade-tab-slash.png' + ); + }); + + it('cascade-slash-numbered', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {slash numbered} + \\ks G + 3.3.4 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/cascade-slash-numbered.png' + ); + }); + + it('cascade-numbered-only', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {numbered} + \\ks G + 3.3.4 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/cascade-numbered-only.png' + ); + }); + + it('cascade-all-four', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {score tabs slash numbered} + \\ks F + 3.3.4 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/cascade-all-four.png' + ); + }); + }); + + describe('Per-staff (L2) overrides', () => { + it('override-l2-tab-ts-hide', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {tabs} + 3.3.4 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/override-l2-tab-ts-hide.png', + undefined, + o => { + const staff = o.score.tracks[0].staves[0]; + staff.tabConfig = { timeSignature: { isVisible: false } }; + } + ); + }); + + it('override-l2-score-clef-hide', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {score} + \\ks D + 3.3.4 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/override-l2-score-clef-hide.png', + undefined, + o => { + const staff = o.score.tracks[0].staves[0]; + staff.scoreConfig = { clef: { isVisible: false } }; + } + ); + }); + }); + + describe('Per-bar (L1) overrides', () => { + it('override-l1-ks-change-hide', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {score} + \\ks D + 3.3.4 3.3 3.3 3.3 | + \\ks A + 3.3.4 3.3 3.3 3.3 | + 3.3.4 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/override-l1-ks-change-hide.png', + undefined, + o => { + const bars = o.score.tracks[0].staves[0].bars; + bars[1].scoreDisplay = { keySignature: { isVisible: false } }; + } + ); + }); + + it('override-l1-ts-hide-firstbar', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + \\staff {score tabs} + 3.3.4 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 | + `, + 'test-data/visual-tests/staff-display-config/override-l1-ts-hide-firstbar.png', + undefined, + o => { + const bars = o.score.tracks[0].staves[0].bars; + bars[0].scoreDisplay = { timeSignature: { isVisible: false } }; + } + ); + }); + }); +}); diff --git a/packages/csharp/src/AlphaTab.Test/Test/Globals.cs b/packages/csharp/src/AlphaTab.Test/Test/Globals.cs index 369cd32cd..1e7cf0f8c 100644 --- a/packages/csharp/src/AlphaTab.Test/Test/Globals.cs +++ b/packages/csharp/src/AlphaTab.Test/Test/Globals.cs @@ -374,6 +374,53 @@ public void ToBeUndefined() Assert.IsNull(_actual, _message); } + public void ToBeDefined() + { + Assert.IsNotNull(_actual, _message); + } + + public void ToEqual(object? expected, string? message = null) + { + if (expected is null && _actual is null) + { + return; + } + if (expected is null || _actual is null) + { + Assert.Fail(message ?? _message ?? $"Expected {(expected is null ? "null" : expected.ToString())}, got {((object?)_actual is null ? "null" : _actual!.ToString())}"); + return; + } + + var expectedType = expected.GetType(); + var actualType = _actual.GetType(); + + if (expectedType == actualType) + { + Assert.AreEqual(expected, _actual, message ?? _message); + return; + } + + // Structural comparison: walk expected's properties (e.g. an anonymous object from a + // TS object-literal `expect(x).toEqual({...})`) and match against the actual instance's + // properties case-insensitively (TS source uses camelCase, C# properties are PascalCase). + var expectedProps = expectedType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + foreach (var prop in expectedProps) + { + var actualProp = actualType.GetProperty( + prop.Name, + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase + ); + if (actualProp is null) + { + Assert.Fail(message ?? _message ?? $"Property '{prop.Name}' not found on {actualType.Name}"); + return; + } + var expectedValue = prop.GetValue(expected); + var actualValue = actualProp.GetValue(_actual); + Assert.AreEqual(expectedValue, actualValue, $"Property '{prop.Name}'"); + } + } + public void ToThrow(Type expected) { Throw(expected);