satie
Version:
A sheet music renderer for the web
219 lines (190 loc) • 9.25 kB
text/typescript
/**
* This file is part of Satie music engraver <https://github.com/jnetterf/satie>.
* Copyright (C) Joshua Netterfield <joshua.ca> 2015 - present.
*
* Satie is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Satie is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Satie. If not, see <http://www.gnu.org/licenses/>.
*/
import {createFactory, ReactElement} from "react";
import {renderToStaticMarkup} from "react-dom/server";
import {ScoreHeader, Print, Grouping, FiguredBass, Attributes, Sound, Direction,
Harmony, Barline} from "musicxml-interfaces";
import {IAny} from "musicxml-interfaces/operations";
import {find} from "lodash";
import ProxyExports from "./implProxy_proxyModel";
import SpacerExports from "./implSpacer_spacerModel";
import VisualCursorExports from "./implVisualCursor_visualCursorModel";
import {IFactory} from "./private_factory";
import {IAttributesSnapshot} from "./private_attributesSnapshot";
import {ILayoutOptions} from "./private_layoutOptions";
import {IChord} from "./private_chordUtil";
import validate from "./engine_processors_validate";
import layoutSong, {ILinePlacementHint} from "./engine_processors_layout";
import PageView from "./implPage_pageView";
import {IMeasure} from "./document_measure";
import {IModel} from "./document_model";
import Type from "./document_types";
const $PageView = createFactory(PageView);
export {ISegment, IMeasurePart, IMeasure, getMeasureSegments, reduceToShortestInSegments} from "./document_measure";
export {IModel, generateModelKey, ILayout, detach} from "./document_model";
export {IMouseEvent, IProps, IPatchSpec, specIsRaw, specIsDocBuilder, specIsPartBuilder, ISong,
ISongClass} from "./document_song";
export {default as Type} from "./document_types";
import {IMeasureLayout} from "./private_measureLayout";
export interface ICleanlinessTracking {
measures: {[measure: string]: {
/** x is cleared whenever rerendering a dirty measure not in preview mode */
x: {
[part: string]: {
[voice: number]: {
voiceX: number[];
staffX: {[staff: number]: number[]};
};
}
};
clean: IMeasureLayout;
layout: IMeasureLayout;
}};
lines: number[][];
linePlacementHints: ReadonlyArray<ILinePlacementHint>;
}
/**
* Models a document in a certain state. Songs wrap documents to support change tracking.
* Songs should not be mutated by user code.
*/
export class Document {
error: Error;
header: ScoreHeader;
measures: IMeasure[];
parts: string[];
_visualCursor: any;
private _factory: IFactory;
modelHasType(model: IModel, modelType: Type.Chord): model is (IChord & IModel);
modelHasType(model: IModel, modelType: Type.Print): model is (Print & IModel);
modelHasType(model: IModel, modelType: Type.Grouping): model is (Grouping & IModel);
modelHasType(model: IModel, modelType: Type.FiguredBass): model is (FiguredBass & IModel);
modelHasType(model: IModel, modelType: Type.Attributes): model is (Attributes & IModel & {_snapshot: IAttributesSnapshot});
modelHasType(model: IModel, modelType: Type.Sound): model is (Sound & IModel);
modelHasType(model: IModel, modelType: Type.Direction): model is (Direction & IModel);
modelHasType(model: IModel, modelType: Type.Harmony): model is (Harmony & IModel);
modelHasType(model: IModel, modelType: Type.Proxy): model is ProxyExports.IProxyModel;
modelHasType(model: IModel, modelType: Type.Spacer): model is SpacerExports.ISpacerModel;
modelHasType(model: IModel, modelType: Type.VisualCursor):
model is VisualCursorExports.IVisualCursorModel;
modelHasType(model: IModel, modelType: Type.Barline): model is (Barline & IModel);
modelHasType(model: IModel, ...modelTypes: Type[]): boolean;
modelHasType(model: IModel, ...modelTypes: Type[]): boolean {
return this._factory.modelHasType(model, ...modelTypes);
}
search(models: IModel[], idx: number, modelType: Type.Chord): (IChord & IModel)[];
search(models: IModel[], idx: number, modelType: Type.Print): (Print & IModel)[];
search(models: IModel[], idx: number, modelType: Type.Grouping): (Grouping & IModel)[];
search(models: IModel[], idx: number, modelType: Type.FiguredBass): (FiguredBass & IModel)[];
search(models: IModel[], idx: number, modelType: Type.Attributes): (Attributes & IModel)[];
search(models: IModel[], idx: number, modelType: Type.Sound): (Sound & IModel)[];
search(models: IModel[], idx: number, modelType: Type.Direction): (Direction & IModel)[];
search(models: IModel[], idx: number, modelType: Type.Harmony): (Harmony & IModel)[];
search(models: IModel[], idx: number, modelType: Type.Proxy): ProxyExports.IProxyModel[];
search(models: IModel[], idx: number, modelType: Type.Spacer): SpacerExports.ISpacerModel[];
search(models: IModel[], idx: number, modelType: Type.VisualCursor):
VisualCursorExports.IVisualCursorModel[];
search(models: IModel[], idx: number, ...types: Type[]): IModel[];
search(models: IModel[], idx: number, ...types: Type[]): IModel[] {
return this._factory.search(models, idx, ...types);
}
getPrint(startMeasure: number): Print {
let firstMeasure = this.measures[startMeasure];
if (!firstMeasure) {
throw new Error("No such measure " + startMeasure);
}
let partWithPrint = find(firstMeasure.parts, part => !!part.staves[1] &&
this.search(part.staves[1], 0, Type.Print).length);
if (partWithPrint) {
return this.search(partWithPrint.staves[1], 0, Type.Print)[0]._snapshot;
}
throw new Error("Part does not contain a Print element at division 0. Is it validated?");
}
renderToStaticMarkup(startMeasure: number): string {
const core = renderToStaticMarkup(
this.__getPage(startMeasure, false, "svg-export", null, false));
return `xml version="1.0" encoding="UTF-8" standalone="no"${
core.replace("<svg", "<svg xmlns=\"http://www.w3.org/2000/svg\"")
.replace(/class="tn_"/g, "font-family='Alegreya'")
.replace(/class="mmn_"/g, "font-family='Alegreya' " +
"font-style='italic' stroke='#7a7a7a'")
.replace(/class="bn_"/g, "font-family='Alegreya' " +
"font-style='italic' text-anchor='end' stroke='#7a7a7a'")
.replace(/<noscript><\/noscript>/g, "")}`;
}
/**
* INTERNAL. Renders a page. Instead, use renderToStaticMarkup() or the
* functions provided in Song.
*
* Invariant: document must be validated.
*/
__getPage(startMeasure: number, preview: boolean,
renderTarget?: "svg-web" | "svg-export", pageClassName?: string, singleLineMode?: boolean,
fixedMeasureWidth?: number,
onOperationsAppended?: (ops: IAny[]) => void,
ref?: (svg: SVGSVGElement) => void,
onPageHeightChanged?: (height: number) => void): ReactElement<any> {
let opts: ILayoutOptions = {
document: this,
attributes: {},
debug: true,
header: this.header,
lineCount: NaN, // YYY
lineIndex: NaN, // YYY
measures: this.measures,
modelFactory: this._factory,
postprocessors: this._factory.postprocessors,
preprocessors: [],
print: this.getPrint(startMeasure),
preview,
singleLineMode,
fixedMeasureWidth,
fixup: onOperationsAppended ? (segment, patch) => {
onOperationsAppended(patch);
} : null,
};
validate(opts);
// Print snapshot may have been changed.
opts.print = this.getPrint(startMeasure);
const lineLayouts = layoutSong(opts);
return $PageView({
className: pageClassName,
lineLayouts: lineLayouts,
print: opts.print,
renderTarget: renderTarget,
scoreHeader: this.header,
singleLineMode,
svgRef: ref,
onPageHeightChanged: onPageHeightChanged,
});
}
constructor(header: ScoreHeader, measures: IMeasure[], parts: string[],
internalFactory: IFactory, error?: Error) {
if (error) {
this.error = error;
return;
};
this.header = header;
this.measures = measures;
this._factory = internalFactory;
}
cleanlinessTracking: ICleanlinessTracking = {
measures: {},
lines: [],
linePlacementHints: null,
};
}