UNPKG

satie

Version:

A sheet music renderer for the web

219 lines (190 loc) 9.25 kB
/** * 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, }; }