Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
14 changes: 9 additions & 5 deletions packages/alphatab/scripts/JsonDeclarationEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function cloneJsDoc<T extends ts.Node>(node: T, source: ts.Node, additionalTags:

function createJsonMember(
program: ts.Program,
input: ts.PropertyDeclaration,
input: ts.PropertyDeclaration | ts.GetAccessorDeclaration,
importer: (name: string, module: string) => void
): ts.TypeElement {
const typeInfo = getTypeWithNullableInfo(program, input.type ?? program.getTypeChecker().getTypeAtLocation(input.name), true, false, undefined);
Expand All @@ -150,14 +150,18 @@ function createJsonMembers(
input: ts.ClassDeclaration,
importer: (name: string, module: string) => void
): ts.TypeElement[] {
const hasMatchingSetter = (name: string): boolean =>
input.members.some(m => ts.isSetAccessorDeclaration(m) && (m.name as ts.Identifier).text === name);

return input.members
.filter(
m =>
ts.isPropertyDeclaration(m) &&
m.modifiers &&
!m.modifiers.find(m => m.kind === ts.SyntaxKind.StaticKeyword)
(ts.isPropertyDeclaration(m) || ts.isGetAccessorDeclaration(m)) &&
!m.modifiers?.find(m => m.kind === ts.SyntaxKind.StaticKeyword) &&
!ts.getJSDocTags(m).find(t => t.tagName.text === 'json_ignore') &&
(!ts.isGetAccessorDeclaration(m) || hasMatchingSetter((m.name as ts.Identifier).text))
)
.map(m => createJsonMember(program, m as ts.PropertyDeclaration, importer));
.map(m => createJsonMember(program, m as ts.PropertyDeclaration | ts.GetAccessorDeclaration, importer));
}

let allJsonTypes: Map<string, string> = new Map<string, string>();
Expand Down
45 changes: 39 additions & 6 deletions packages/alphatab/scripts/TypeSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,27 @@ export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration)
hasSetPropertyExtension: false
};

const accessorHasSetter = (cls: ts.ClassDeclaration, 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],
typeArgumentMapping: Map<string, ts.Type> | undefined
) => {
if (ts.isPropertyDeclaration(member)) {
const propertyDeclaration = member as ts.PropertyDeclaration;
if (ts.isPropertyDeclaration(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
// assigned to and writing it via toJson would just duplicate underlying state.
if (
ts.isGetAccessorDeclaration(member) &&
!accessorHasSetter(cls, (member.name as ts.Identifier).text)
) {
return;
}
if (
!propertyDeclaration.modifiers!.find(
!member.modifiers?.find(
m => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.PrivateKeyword
)
) {
Expand All @@ -57,20 +70,40 @@ export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration)
if (!jsDoc.find(t => t.tagName.text === 'json_ignore')) {
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;

// Heuristic: a deprecated getter+setter pair is almost always a
// backwards-compat alias for a canonical property — round-tripping it via
// toJson would duplicate state. Surface a warning so a human can decide
// whether to add @json_read_only (input-only) or @json_ignore (drop entirely).
if (
isAccessor &&
!isReadonly &&
jsDoc.some(t => t.tagName.text === 'deprecated')
) {
const sourceFile = member.getSourceFile();
const { line } = sourceFile.getLineAndCharacterOfPosition(member.getStart());
console.warn(
`[serializer] ${sourceFile.fileName}:${line + 1} - deprecated accessor ${(member.name as ts.Identifier).text}: consider @json_read_only (input-only legacy alias) or @json_ignore (drop from JSON entirely).`
);
}
schema.properties.push({
jsonNames: jsonNames,
asRaw,
partialNames: !!jsDoc.find(t => t.tagName.text === 'json_partial_names'),
target: jsDoc.find(t => t.tagName.text === 'target')?.comment as string,
isJsonReadOnly: isReadonly,
isReadOnly: propertyDeclaration.modifiers!.some(m => m.kind == ts.SyntaxKind.ReadonlyKeyword),
isReadOnly: isAccessor
? false
: member.modifiers!.some(m => m.kind == ts.SyntaxKind.ReadonlyKeyword),
name: (member.name as ts.Identifier).text,
jsDocTags: jsDoc,
type: getTypeWithNullableInfo(
program,
member.type ?? program.getTypeChecker().getTypeAtLocation(member.name),
asRaw || isReadonly,
!!member.questionToken,
isOptional,
typeArgumentMapping
)
});
Expand All @@ -93,7 +126,7 @@ export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration)
const checker = program.getTypeChecker();
while (hierarchy) {
for (const x of hierarchy.members) {
handleMember(x, typeArgumentMapping);
handleMember(hierarchy, x, typeArgumentMapping);
}

const extendsClause = hierarchy.heritageClauses?.find(c => c.token === ts.SyntaxKind.ExtendsKeyword);
Expand Down
83 changes: 79 additions & 4 deletions packages/alphatab/src/DisplaySettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,35 @@ export class DisplaySettings {
*/
public stretchForce: number = 1.0;

/**
* The proportional spacing ratio between successive note durations.
* @since 1.9.0
* @category Display
* @defaultValue `Math.SQRT2` (≈ 1.414, matches Dorico's default)
* @remarks
* Controls the *shape* of the horizontal spacing curve - how much wider a note of duration `2d` is rendered relative to a note of duration `d`.
* AlphaTab uses a power-law spacing model (the same approach used by Dorico, MuseScore and Finale): doubling the note duration multiplies its
* allocated horizontal space by `spacingRatio`.
*
* Reference values for cross-application comparison:
*
* | Application / Style | Ratio | Character |
* |----------------------------|----------------------|------------------------------------|
* | Dorico default | √2 ≈ 1.414 | Tight, efficient, orchestral |
* | MuseScore default | 1.5 | Balanced, general-purpose |
* | Finale default (Fibonacci) | φ ≈ 1.618 | Loose, traditional engraving |
*
* AlphaTab defaults to `√2` (Dorico's value). This produces tighter spacing at long
* durations than the alternatives, which matters for guitar tablature where rest bars and
* whole notes are common - looser ratios make those bars dominate system width.
*
* This setting is orthogonal to {@link stretchForce}: `spacingRatio` controls the *shape* of the spacing (proportions between durations),
* `stretchForce` controls the overall *density* (how tightly or loosely the music is packed). Both can be adjusted independently.
*
* Values are clamped to the range `[1.2, 2.0]`. A value of `1.0` would produce equal spacing for all durations and is rejected.
*/
public spacingRatio: number = Math.SQRT2;

/**
* The layouting mode used to arrange the the notation.
* @remarks
Expand Down Expand Up @@ -123,19 +152,65 @@ export class DisplaySettings {
*/
public barCountPerPartial: number = 10;

/**
* The minimum fullness ratio at which the last system in a flow is justified to fill the
* available staff width.
* @since 1.9.0
* @category Display
* @defaultValue `1`
* @remarks
* The "fullness" of a system is its natural unjustified width divided by the available
* staff width. The last system is stretched to full width only when its fullness is
* **greater than or equal to** this threshold; otherwise it renders at its natural width.
*
* Following industry convention (Dorico, MuseScore), a sparsely populated final system
* looks better compact than spread across the full page. The threshold lets users tune
* where that boundary sits.
*
* Common values:
*
* - `1` (default) — never justify the last system. Equivalent to the legacy
* `justifyLastSystem = false` behaviour.
* - `0` — always justify the last system, even when sparse. Equivalent to the legacy
* `justifyLastSystem = true` behaviour.
* - `0.4`–`0.7` — Dorico/MuseScore-style: justify when the last system is reasonably full,
* leave compact when only a few bars trail.
*
* The threshold is bypassed when the last system is naturally wider than the available
* staff width - in that case the system still compresses to fit, since otherwise content
* would overflow horizontally.
*
* Values outside `[0, 1]` are clamped.
*/
public lastSystemFillThreshold: number = 1;

/**
* Whether to justify also the last system in page layouts.
*
* @remarks
* Setting this option to `true` tells alphaTab to also justify the last system (row) like it
* already does for the systems which are full.
* @deprecated Use {@link lastSystemFillThreshold} for fine-grained control over when the
* last system is justified. This property is now a thin wrapper:
*
* - **Get** returns `true` when {@link lastSystemFillThreshold} is less than `1` (i.e. some
* degree of last-system justification is enabled), `false` otherwise.
* - **Set** to `true` writes `lastSystemFillThreshold = 0` (always justify regardless of
* fullness). Set to `false` writes `lastSystemFillThreshold = 1` (never justify).
*
* | Justification Disabled | Justification Enabled |
* |--------------------------------------------------------------|-------------------------------------------------------|
* | ![Disabled](https://alphatab.net/img/reference/property/justify-last-system-false.png) | ![Enabled](https://alphatab.net/img/reference/property/justify-last-system-true.png) |
*
* @since 1.3.0
* @category Display
* @defaultValue `false`
*/
public justifyLastSystem: boolean = false;
* @json_read_only
*/
public get justifyLastSystem(): boolean {
return this.lastSystemFillThreshold < 1;
}
public set justifyLastSystem(value: boolean) {
this.lastSystemFillThreshold = value ? 0 : 1;
}

/**
* Allows adjusting of the used fonts and colors for rendering.
Expand Down
9 changes: 9 additions & 0 deletions packages/alphatab/src/RenderingResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class RenderingResources {
* @defaultValue `bold 12px Arial, sans-serif`
* @since 0.9.6
* @deprecated use {@link elementFonts} with {@link NotationElement.ScoreCopyright}
* @json_read_only
*/
public get copyrightFont(): Font {
return this.elementFonts.get(NotationElement.ScoreCopyright)!;
Expand All @@ -103,6 +104,7 @@ export class RenderingResources {
* @defaultValue `32px Georgia, serif`
* @since 0.9.6
* @deprecated use {@link elementFonts} with {@link NotationElement.ScoreTitle}
* @json_read_only
*/
public get titleFont(): Font {
return this.elementFonts.get(NotationElement.ScoreTitle)!;
Expand All @@ -120,6 +122,7 @@ export class RenderingResources {
* @defaultValue `20px Georgia, serif`
* @since 0.9.6
* @deprecated use {@link elementFonts} with {@link NotationElement.ScoreSubTitle}
* @json_read_only
*/
public get subTitleFont(): Font {
return this.elementFonts.get(NotationElement.ScoreSubTitle)!;
Expand All @@ -137,6 +140,7 @@ export class RenderingResources {
* @defaultValue `15px Arial, sans-serif`
* @since 0.9.6
* @deprecated use {@link elementFonts} with {@link NotationElement.ScoreWords}
* @json_read_only
*/
public get wordsFont(): Font {
return this.elementFonts.get(NotationElement.ScoreWords)!;
Expand All @@ -154,6 +158,7 @@ export class RenderingResources {
* @defaultValue `12px Georgia, serif`
* @since 1.4.0
* @deprecated use {@link elementFonts} with {@link NotationElement.EffectBeatTimer}
* @json_read_only
*/
public get timerFont(): Font {
return this.elementFonts.get(NotationElement.EffectBeatTimer)!;
Expand All @@ -171,6 +176,7 @@ export class RenderingResources {
* @defaultValue `14px Georgia, serif`
* @since 1.4.0
* @deprecated use {@link elementFonts} with {@link NotationElement.EffectDirections}
* @json_read_only
*/
public get directionsFont(): Font {
return this.elementFonts.get(NotationElement.EffectDirections)!;
Expand All @@ -188,6 +194,7 @@ export class RenderingResources {
* @defaultValue `11px Arial, sans-serif`
* @since 0.9.6
* @deprecated use {@link elementFonts} with {@link NotationElement.ChordDiagramFretboardNumbers}
* @json_read_only
*/
public get fretboardNumberFont(): Font {
return this.elementFonts.get(NotationElement.ChordDiagramFretboardNumbers)!;
Expand Down Expand Up @@ -223,6 +230,7 @@ export class RenderingResources {
* @defaultValue `bold 14px Georgia, serif`
* @since 0.9.6
* @deprecated use {@link elementFonts} with {@link NotationElement.EffectMarker}
* @json_read_only
*/
public get markerFont(): Font {
return this.elementFonts.get(NotationElement.EffectMarker)!;
Expand All @@ -249,6 +257,7 @@ export class RenderingResources {
* @defaultValue `11px Arial, sans-serif`
* @since 0.9.6
* @deprecated use {@link elementFonts} with {@link NotationElement.BarNumber}
* @json_read_only
*/
public get barNumberFont(): Font {
return this.elementFonts.get(NotationElement.BarNumber)!;
Expand Down
Loading
Loading