UNPKG

satie

Version:

A sheet music renderer for the web

235 lines (204 loc) 9.24 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 {Print} from "musicxml-interfaces"; import {map, reduce, find, last} from "lodash"; import * as invariant from "invariant"; import {IMeasure, getMeasureSegments, Type, reduceToShortestInSegments} from "./document"; import {ILayoutOptions} from "./private_layoutOptions"; import {calculateLineBounds} from "./private_lineBounds"; import {IMeasureLayout} from "./private_measureLayout"; import {layoutLine} from "./engine_processors_line"; const SQUISHINESS = 0.8; export interface ILinePlacementHint { widthByShortest: {[key: number]: number}; shortestCount: number; attributesWidthStart: number; attributesWidthEnd: number; } interface IReduceOptsMemo { options: ILayoutOptions; opts: ILayoutOptions[]; remainingWidth: number; startingWidth: number; thisPrint: Print; widthAllocatedForEnd: number; widthAllocatedForStart: number; shortest: number; } function findPrint(options: ILayoutOptions, measure: IMeasure): Print { const partWithPrint = find(measure.parts, part => !!part.staves[1] && options.modelFactory.search(part.staves[1], 0, Type.Print).length); if (partWithPrint) { return options.modelFactory.search(partWithPrint.staves[1], 0, Type.Print)[0]._snapshot as any; } return null; } /** * Reducer that puts measures into lines. */ function assignLinesReducer(memo: IReduceOptsMemo, measureInfo: ILinePlacementHint, idx: number, all: ILinePlacementHint[]): IReduceOptsMemo { let options = memo.options; let measures = options.measures; memo.thisPrint = findPrint(options, measures[idx]) || memo.thisPrint; if (!last(memo.opts).print) { last(memo.opts).print = memo.thisPrint; } invariant(!!memo.thisPrint, "No print found"); if (!memo.options.singleLineMode) { if (measureInfo.attributesWidthStart > memo.widthAllocatedForStart) { memo.remainingWidth -= measureInfo.attributesWidthStart - memo.widthAllocatedForStart; memo.widthAllocatedForStart = measureInfo.attributesWidthStart; } if (measureInfo.attributesWidthEnd > memo.widthAllocatedForEnd) { memo.remainingWidth -= measureInfo.attributesWidthEnd - memo.widthAllocatedForEnd; memo.widthAllocatedForEnd = measureInfo.attributesWidthEnd; } let retroactiveIncrease = 0; if (memo.shortest > measureInfo.shortestCount) { const measuresOnLine = last(memo.opts).measures.length; const measuresInfo = all.slice(idx - measuresOnLine, idx); retroactiveIncrease = measuresInfo.reduce((increase, measure) => { return measure.widthByShortest[measureInfo.shortestCount] - measure.widthByShortest[memo.shortest]; }, 0); memo.shortest = measureInfo.shortestCount; } const measureWidth = measureInfo.widthByShortest[memo.shortest]; const totalIncrease = retroactiveIncrease + measureWidth; if (memo.remainingWidth > totalIncrease) { memo.remainingWidth -= totalIncrease; } else { memo.opts.push(createEmptyLayout(options, memo.thisPrint)); memo.remainingWidth = memo.startingWidth - measureWidth - measureInfo.attributesWidthStart - measureInfo.attributesWidthEnd; memo.widthAllocatedForStart = measureInfo.attributesWidthStart; memo.widthAllocatedForEnd = measureInfo.attributesWidthEnd; } } last(memo.opts).measures.push(measures[idx]); return memo; } function createEmptyLayout(options: ILayoutOptions, print: Print): ILayoutOptions { return { ...options, attributes: null, measures: [], print: print, }; } export function getApproximateMeasureWidth(measure: IMeasure, shortest: number) { return Object.keys(measure.parts).reduce((pwidth, partName) => { const vwidth = measure.parts[partName].voices.reduce((vwidth, voice) => { if (!voice) { return vwidth; } return voice.reduce((swidth, el) => swidth + el.calcWidth(shortest), vwidth); }, 0); return Math.max(vwidth, pwidth); }, 0); } function getLinePlacementHints(measures: IMeasure[]): ReadonlyArray<ILinePlacementHint> { const shortestByMeasure: ReadonlyArray<number> = measures.map(measure => { const segments = getMeasureSegments(measure); return reduce(segments, reduceToShortestInSegments, Number.MAX_VALUE); }); const shortestsObj: {readonly [key: number]: boolean} = shortestByMeasure.reduce((shortests, shortest) => { shortests[shortest] = true; return shortests; }, {} as {[key: number]: boolean}); const shortests = Object.keys(shortestsObj).map(str => parseInt(str, 10)); return map(measures, function layoutMeasure(measure, idx): ILinePlacementHint { const shortestInMeasure = shortestByMeasure[idx]; let specifiedWidth = measure.width; const numericMeasureWidth = !isNaN(measure.width) && measure.width !== null; if (numericMeasureWidth && (measure.width <= 0 || !isFinite(measure.width))) { console.warn("Bad measure width %s. Ignoring", measure.width); specifiedWidth = undefined; } let widthByShortest = shortests.reduce((shortests, shortest) => { if (shortest <= shortestInMeasure) { shortests[shortest] = getApproximateMeasureWidth(measure, shortest); } return shortests; }, {} as {[key: number]: number}); // XXX: multiple rests return { widthByShortest, shortestCount: shortestInMeasure, attributesWidthStart: 150, // XXX attributesWidthEnd: 50, // XXX }; }); } export default function layoutSong(options: ILayoutOptions): IMeasureLayout[][] { invariant(!!options.print, "Print not defined"); invariant(!options.print._snapshot, "Pass a snapshot of Print to layoutSong, not the actual model!"); const page = 1; // XXX const scaling = options.document.header.defaults.scaling; // Estimate the width of each measure, and the space available for each line. const boundsGuess = calculateLineBounds(options.print, page, scaling); const lineWidth = (boundsGuess.right - boundsGuess.left) / SQUISHINESS; const linePlacementHints = options.preview ? options.document.cleanlinessTracking.linePlacementHints : getLinePlacementHints(options.measures); options.document.cleanlinessTracking.linePlacementHints = linePlacementHints; // Assign measures to lines. const layoutOpts = reduce(linePlacementHints, assignLinesReducer, { options: options, opts: <ILayoutOptions[]>[createEmptyLayout(options, options.print)], remainingWidth: lineWidth, shortest: Number.MAX_VALUE, startingWidth: lineWidth, thisPrint: options.print, widthAllocatedForEnd: 0, widthAllocatedForStart: 0, }).opts; layoutOpts.forEach((line, idx) => { line.lineIndex = idx; line.lineCount = layoutOpts.length; line.attributes = {}; }); if (!options.preview) { const oldLineCleanliness = options.document.cleanlinessTracking.lines || []; const newLineCleanliness = layoutOpts.map(line => line.measures.map(measure => measure.uuid)) || []; for (let i = 0; i < oldLineCleanliness.length || i < newLineCleanliness.length; ++i) { const oldLine = oldLineCleanliness[i] || []; const newLine = newLineCleanliness[i] || []; const isDirty = !oldLine || !newLine || oldLine.length !== newLine.length || oldLine.some((m, k) => newLine[k] !== m); if (isDirty) { oldLine.concat(newLine).forEach(m => { options.document.cleanlinessTracking.measures[m] = null; }); } } options.document.cleanlinessTracking.lines = newLineCleanliness; } // Create the final layout const memo = { y: calculateLineBounds(layoutOpts[0].print, page, scaling).top, attributes: {}, }; return layoutOpts.map(lineOpt => layoutLine( lineOpt, calculateLineBounds(lineOpt.print, page, scaling), memo, )); }