UNPKG

satie

Version:

A sheet music renderer for the web

277 lines (243 loc) 11.1 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 {reduce, forEach, flatten, filter, find, map, toPairs, last} from "lodash"; import * as invariant from "invariant"; import {Print, BarStyleType} from "musicxml-interfaces"; import {IAny} from "musicxml-interfaces/operations"; import {Type, ISegment} from "./document"; import {IAttributesSnapshot} from "./private_attributesSnapshot"; import {ILayoutOptions, IFixupFn} from "./private_layoutOptions"; import createPatch from "./engine_createPatch"; import applyOp from "./engine_applyOp"; import {normalizeDivisionsInPlace} from "./engine_divisions"; import DivisionOverflowException from "./engine_divisionOverflowException"; import {refreshMeasure, RefreshMode} from "./engine_processors_measure"; /** * Reducer for a collection of functions, calling each one. */ function call<T>(memo: T, fn: (t: T) => T) { return fn(memo); } /** * Exception that indicates the measure must be validated again. * This can occur when a measure was modified in a position before the cursor. */ class RestartMeasureValidation { stack: string; constructor() { this.stack = new Error().stack; } } /** * Validate the measure. */ export default function validate(options: ILayoutOptions): void { options.measures = <any> reduce(options.preprocessors, call, options.measures); let shouldTryAgain: boolean; /** * The operations that have been applied while validating. * This is for debug output when we get stuck in a loop. * This is reset every measure. */ let debugFixupOperations: IAny[][] = []; /** * This function applies a patch as part of validation. * * A fixup function may have been passed in (if we are in an editor). If not, we just * mutate the song in-place. Note that this implementation does not allow for undo/redo. */ function rootFixup(segment: ISegment, operations: IAny[], restartRequired: boolean) { debugFixupOperations.push(operations); if (options.fixup) { options.fixup(segment, operations); } else { forEach(operations, operation => { applyOp(options.preview, options.measures, options.modelFactory, operation, options.document, () => options.preview = false); }); } if (restartRequired) { throw new RestartMeasureValidation(); } } let rootFixupOpts = { debugFixupOperations, rootFixup, }; do { shouldTryAgain = false; try { tryValidate(options, rootFixupOpts); } catch (err) { if (err instanceof DivisionOverflowException) { const ops = (<DivisionOverflowException>err).getOperations(); // The restartRequired flag is false because we restart manually. rootFixup(null, createPatch(false, options.document, ops), false); shouldTryAgain = true; } else { throw err; } } } while (shouldTryAgain); } function tryValidate(options: ILayoutOptions, rootFixupOpts: {debugFixupOperations: IAny[][], rootFixup: IFixupFn}): void { let factory = options.modelFactory; let search = factory.search.bind(factory); let lastAttribs: {[part: string]: IAttributesSnapshot[]} = {}; let lastPrint: Print = options.print; function withPart(segments: ISegment[], partID: string): ISegment[] { forEach(segments, segment => { if (segment) { segment.part = partID; } }); return segments; } // Normalize divisions on a line: let allSegments: ISegment[] = []; forEach(options.measures, function validateMeasure(measure) { let voiceSegments = <ISegment[]> flatten(map(toPairs(measure.parts), partx => withPart(partx[1].voices, partx[0]))); let staffSegments = <ISegment[]> flatten(map(toPairs(measure.parts), partx => withPart(partx[1].staves, partx[0]))); allSegments = allSegments.concat(filter(voiceSegments.concat(staffSegments), s => !!s)); }); normalizeDivisionsInPlace(factory, allSegments, 0); // TODO: check if a measure hence becomes dirty? let tries = 0; forEach(options.measures, function validateMeasure(measure) { let cleanliness = options.document.cleanlinessTracking.measures[measure.uuid]; if (cleanliness && cleanliness.clean) { lastAttribs = cleanliness.clean.attributes; lastPrint = cleanliness.clean.print; return; } rootFixupOpts.debugFixupOperations = []; // Fixups can require multiple passes. for (let tryAgain = true; tryAgain;) { if (++tries > 100) { console.warn("-------------- too many fixups: aborting -------------- "); console.warn(rootFixupOpts.debugFixupOperations); throw new Error("Internal Satie Error: fixup loop!"); } tryAgain = false; try { let voiceSegments = <ISegment[]> flatten(map(toPairs(measure.parts), partx => withPart(partx[1].voices, partx[0]))); let staffSegments = <ISegment[]> flatten(map(toPairs(measure.parts), partx => withPart(partx[1].staves, partx[0]))); let segments = filter(voiceSegments.concat(staffSegments), s => !!s); forEach(staffSegments, function(segment, idx) { if (!segment) { return; } invariant(segment.ownerType === "staff", "Expected staff segment"); lastAttribs[segment.part] = lastAttribs[segment.part] || []; function ensureHeader(type: Type) { if (!search(segment, 0, type).length) { if (segment.owner === 1) { rootFixupOpts.rootFixup(segment, [{ p: [ String(measure.uuid), "parts", segment.part, "staves", segment.owner, 0 ], li: { _class: Type[type] } }], false); } else { let proxy = factory.create(Type.Proxy); let proxiedSegment: ISegment = find(staffSegments, potentialProxied => potentialProxied && potentialProxied.part === segment.part && potentialProxied.owner === 1); let target = search(proxiedSegment, 0, type)[0]; (<any>proxy).target = target; (<any>proxy).staffIdx = idx; let tidx = -1; for (let i = 0; i < proxiedSegment.length; ++i) { if (proxiedSegment[i] === target) { tidx = i; break; } } invariant(tidx !== -1, "Could not find required model."); // Warning: without fixup. // STOPSHIP: Also add ability to remove/retarget proxy segment.splice(tidx, 0, proxy); } } } ensureHeader(Type.Print); ensureHeader(Type.Attributes); if (!search(segment, segment.length - 1, Type.Barline).length) { // Make sure the barline ends up at the end. const patches = createPatch(false, options.document, measure.uuid, segment.part, part => part.staff( segment.owner, staff => staff .insertBarline(barline => barline .barStyle({ data: measure.uuid === last(options.document.measures).uuid ? BarStyleType.LightHeavy : BarStyleType.Regular, }) ), segment.length ) ); rootFixupOpts.rootFixup(segment, patches, false); } }); let outcome = refreshMeasure({ noAlign: true, mode: RefreshMode.RefreshModel, document: options.document, factory: factory, fixup: rootFixupOpts.rootFixup, header: options.header, lineBarOnLine: NaN, lineCount: NaN, lineIndex: NaN, lineShortest: NaN, lineTotalBarsOnLine: NaN, measure: measure, measureX: 0, preview: options.preview, print: lastPrint, segments: segments, attributes: lastAttribs, singleLineMode: options.singleLineMode, }); lastAttribs = outcome.attributes; lastPrint = outcome.print; } catch (ex) { if (ex instanceof RestartMeasureValidation) { tryAgain = true; } else { throw ex; } } } }); }