Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ export const CONFIG = {
'LAST_SYSTEM_WIDTH_STRETCH_THRESHOLD is the total width fraction that the measures must exceed to stretch the ' +
'measures in the last system.',
}),
CONTINUATION_MEASURE_WIDTH_THRESHOLD: t.number({
defaultValue: null,
help:
'CONTINUATION_MEASURE_WIDTH_THRESHOLD is the calculated measure width (in pixels) above which an eligible ' +
'measure is split into continuation pieces. Pieces flow across systems via the normal bin-packer, with the ' +
'inner pieces having no start or end barlines as the continuation cue. Only supported by DefaultFormatter ' +
'(requires WIDTH to be set). When this is null, no fragmentation occurs.',
}),
PART_LABEL_FONT_FAMILY: t.string({
defaultValue: 'Arial',
help: 'PART_LABEL_FONT_FAMILY is the font family for part names.',
Expand Down
13 changes: 13 additions & 0 deletions src/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ export type Measure = {
startBarlineStyle: BarlineStyle | null;
endBarlineStyle: BarlineStyle | null;
repetitionSymbols: RepetitionSymbol[];
/**
* Non-null when this measure is part of a continuation chain (a measure that was too wide and got split into
* pieces). Otherwise null.
*/
continuation: Continuation | null;
};

export type Continuation = {
type: 'continuation';
/** 0-based position of this piece in its continuation chain. */
index: number;
/** Total number of pieces in the chain (>= 2). */
total: number;
};

export type Jump =
Expand Down
8 changes: 8 additions & 0 deletions src/elements/measure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export class Measure {
return this.measureRender.absoluteIndex;
}

/**
* Returns the continuation metadata if this measure is part of a continuation chain (a measure that was too wide
* and got fragmented into pieces). Returns null otherwise.
*/
getContinuation(): data.Continuation | null {
return this.measureRender.continuation;
}

/**
* Sometimes document measures are folded into one (e.g. multimeasure rest). This method returns the [start, end]
* _absolute_ index range that the measure covers.
Expand Down
106 changes: 106 additions & 0 deletions src/formatting/continuationpass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as data from '@/data';
import * as rendering from '@/rendering';
import * as util from '@/util';
import { Config } from '@/config';
import { Logger } from '@/debug';
import { Rect } from '@/spatial';
import { MeasureSplitter } from './measuresplitter';

export type ContinuationPassResult = {
/** Flattened post-split measures across all systems. */
measures: data.Measure[];
/**
* Synthesized {@link rendering.MeasureRender}s — one per piece — with widths suitable for downstream bin-packing.
* Most fields beyond `rect.w` and `absoluteIndex` are stubs; the bin-packer only reads those two.
*/
measureRenders: rendering.MeasureRender[];
};

/**
* Mutates `document.score.systems` to replace eligible too-wide measures with continuation pieces. Returns the flat
* post-split measures list paired with synthesized panoramic-width renders for the bin-packer.
*
* Caller is expected to pass a cloned document — this function will mutate it.
*/
export function applyContinuationSplit(
document: data.Document,
panoramicScoreRender: rendering.ScoreRender,
config: Config,
log: Logger
): ContinuationPassResult {
if (config.CONTINUATION_MEASURE_WIDTH_THRESHOLD === null) {
return {
measures: document.score.systems.flatMap((s) => s.measures),
measureRenders: panoramicScoreRender.systemRenders.flatMap((s) => s.measureRenders),
};
}

const splitter = new MeasureSplitter(config, log);

const flatOriginalRenders = panoramicScoreRender.systemRenders.flatMap((s) => s.measureRenders);
const flatOriginalMeasures = document.score.systems.flatMap((s) => s.measures);
util.assert(
flatOriginalRenders.length === flatOriginalMeasures.length,
'panoramic render must have one MeasureRender per data.Measure'
);

const splitResults: Array<{ pieces: data.Measure[]; pieceWidths: number[]; reference: rendering.MeasureRender }> = [];

let renderCursor = 0;
const newSystems: data.System[] = [];
for (const system of document.score.systems) {
const newMeasures: data.Measure[] = [];
for (const measure of system.measures) {
const reference = flatOriginalRenders[renderCursor++];
const result = splitter.split(measure, reference);
newMeasures.push(...result.pieces);
splitResults.push({ ...result, reference });
}
newSystems.push({ type: 'system', measures: newMeasures });
}
document.score.systems = newSystems;

const measures = newSystems.flatMap((s) => s.measures);
const measureRenders: rendering.MeasureRender[] = [];
let absoluteIndex = 0;
for (const result of splitResults) {
if (result.pieces.length === 1) {
measureRenders.push({ ...result.reference, absoluteIndex });
absoluteIndex++;
continue;
}
for (let pieceIndex = 0; pieceIndex < result.pieces.length; pieceIndex++) {
measureRenders.push(
synthesizePieceRender(
result.reference,
result.pieceWidths[pieceIndex],
absoluteIndex,
pieceIndex,
result.pieces.length
)
);
absoluteIndex++;
}
}

return { measures, measureRenders };
}

function synthesizePieceRender(
reference: rendering.MeasureRender,
pieceWidth: number,
absoluteIndex: number,
pieceIndex: number,
total: number
): rendering.MeasureRender {
return {
type: 'measure',
key: reference.key,
rect: new Rect(reference.rect.x, reference.rect.y, pieceWidth, reference.rect.h),
absoluteIndex,
fragmentRenders: [],
multiRestCount: 0,
jumps: [],
continuation: { type: 'continuation', index: pieceIndex, total },
};
}
51 changes: 28 additions & 23 deletions src/formatting/defaultformatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Config, DEFAULT_CONFIG } from '@/config';
import { Logger, NoopLogger } from '@/debug';
import { Formatter } from './types';
import { PanoramicFormatter } from './panoramicformatter';
import { applyContinuationSplit } from './continuationpass';

type SystemSlice = {
from: number;
Expand Down Expand Up @@ -35,50 +36,54 @@ export class DefaultFormatter implements Formatter {

// First, ensure the document is formatted for infinite x-scrolling. This will allow us to measure the width of the
// measures and make decisions on how to group them into systems.
const panoramicConfig = { ...this.config, WIDTH: null, HEIGHT: null };
const panoramicConfig = {
...this.config,
WIDTH: null,
HEIGHT: null,
CONTINUATION_MEASURE_WIDTH_THRESHOLD: null,
};
const panoramicFormatter = new PanoramicFormatter({ config: panoramicConfig });
const panoramicDocument = new rendering.Document(panoramicFormatter.format(document));
const panoramicScoreRender = new rendering.Score(panoramicConfig, this.log, panoramicDocument, null).render();

const slices = this.getSystemSlices(this.config, panoramicScoreRender);
const { measures, measureRenders } = applyContinuationSplit(clone, panoramicScoreRender, this.config, this.log);

this.applySystemSlices(clone, slices);
const slices = this.getSystemSlices(this.config, measureRenders);

this.applySystemSlices(clone, slices, measures);

return clone;
}

private getSystemSlices(config: Config, scoreRender: rendering.ScoreRender): SystemSlice[] {
const slices = [{ from: 0, to: 0 }];

let remaining = config.WIDTH!;
let count = 0;

const measureRenders = scoreRender.systemRenders.flatMap((systemRender) => systemRender.measureRenders);

for (let measureIndex = 0; measureIndex < measureRenders.length; measureIndex++) {
const measure = measureRenders[measureIndex];
private getSystemSlices(config: Config, measureRenders: rendering.MeasureRender[]): SystemSlice[] {
const slices: SystemSlice[] = [];
let remaining = 0;
// Continuation pieces occupy their own system, and the system following a continuation piece must start fresh.
let lockedFromContinuation = false;

for (const measure of measureRenders) {
const required = measure.rect.w;
const isContinuationPiece = measure.continuation !== null;
const currentSlice = slices.at(-1);

if (required > remaining && count > 0) {
const needNewSlice = !currentSlice || isContinuationPiece || lockedFromContinuation || required > remaining;

if (needNewSlice) {
slices.push({ from: measure.absoluteIndex, to: measure.absoluteIndex });
remaining = config.WIDTH!;
count = 0;
remaining = config.WIDTH! - required;
} else {
currentSlice!.to = measure.absoluteIndex;
remaining -= required;
}

slices.at(-1)!.to = measure.absoluteIndex;
remaining -= required;
count++;
lockedFromContinuation = isContinuationPiece;
}

this.log.debug(`grouped ${measureRenders.length} measures into ${slices.length} system(s)`);

return slices;
}

private applySystemSlices(document: data.Document, slices: SystemSlice[]): void {
const measures = document.score.systems.flatMap((s) => s.measures);

private applySystemSlices(document: data.Document, slices: SystemSlice[], measures: data.Measure[]): void {
document.score.systems = [];

for (const slice of slices) {
Expand Down
Loading
Loading