UNPKG

satie

Version:

A sheet music renderer for the web

440 lines (439 loc) 23.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/>. */ "use strict"; var __assign = (this && this.__assign) || Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; /** * @file engine/processors/measure.ts provides functions for validating and laying out measures */ var invariant = require("invariant"); var lodash_1 = require("lodash"); var document_1 = require("./document"); var implAttributes_attributesData_1 = require("./implAttributes_attributesData"); var private_combinedLayout_1 = require("./private_combinedLayout"); var private_cursor_1 = require("./private_cursor"); var private_part_1 = require("./private_part"); var private_util_1 = require("./private_util"); var private_chordUtil_1 = require("./private_chordUtil"); var engine_divisionOverflowException_1 = require("./engine_divisionOverflowException"); /** * Given a bunch of segments and the context (measure, line), returns information needed to lay the * models out. Note that the order of the output is arbitrary and may not correspond to the order * of the input segments. * * @segments Models to lay out or validate. * @measure Model to which the model belongs to. * @line Line context * * Complexity: O(staff-voice pairs) */ function refreshMeasure(spec) { var gMeasure = spec.measure; invariant(!!spec.attributes, "Attributes must be defined"); var gInitialAttribs = private_util_1.cloneObject(spec.attributes); var gPrint = spec.print; var gMaxDivisions = 0; if (!spec.document.cleanlinessTracking.measures[spec.measure.uuid]) { spec.document.cleanlinessTracking.measures[spec.measure.uuid] = { clean: null, x: {}, layout: null, }; } // Cleanliness is also part-owned by the line processor. The layout in // cleanliness is not the output of this function -- it also has been // treated by postprocessors. This function sets "x" and uses the clean-state // to avoid unnecessary work. var cleanliness = spec.document.cleanlinessTracking.measures[spec.measure.uuid]; var oldLayout = cleanliness.layout; invariant(spec.segments.length >= 1, "_processMeasure expects at least one segment."); Object.keys(spec.measure.parts).forEach(function (part) { cleanliness.x[part] = cleanliness.x[part] || {}; spec.measure.parts[part].voices.forEach(function (voice) { if (!voice) { return; } cleanliness.x[part][voice.owner] = cleanliness.x[part][voice.owner] || { voiceX: [], staffX: spec.measure.parts[part].staves.reduce(function (memo, staff) { if (staff) { memo[staff.owner] = []; } return memo; }, {}), }; }); }); var gStaffMeasure = lodash_1.keyBy(lodash_1.filter(spec.segments, function (seg) { return seg.ownerType === "staff"; }), function (seg) { return seg.part + "_" + seg.owner; }); var gVoiceMeasure = lodash_1.keyBy(lodash_1.filter(spec.segments, function (seg) { return seg.ownerType === "voice"; }), function (seg) { return seg.part + "_" + seg.owner; }); var gStaffLayouts = {}; var gMaxXInMeasure = spec.measureX; var gMaxPaddingTopInMeasure = []; var gMaxPaddingBottomInMeasure = []; var gDivOverflow = null; var lastPrint = spec.print; var vCursor; function fixup(operations) { var localSegment = vCursor.segmentInstance; var restartRequired = lodash_1.some(operations, function (op) { if (op.p[0] === "divisions") { return true; } invariant(String(op.p[0]) === String(spec.measure.uuid), "Unexpected fixup for a measure " + op.p[0] + " " + ("other than the current " + spec.measure.uuid)); invariant(op.p[1] === "parts", "Expected p[1] to be parts"); invariant(op.p[2] === localSegment.part, "Expected part " + op.p[2] + " to be " + localSegment.part); if (localSegment.ownerType === "voice") { if (typeof op.p[4] === "string") { op.p[4] = parseInt(op.p[4], 10); } invariant(op.p[3] === "voices", "We are in a voice, so we can only patch the voice"); invariant(op.p[4] === localSegment.owner, "Expected voice owner " + localSegment.owner + ", got " + op.p[4]); return op.p.length === 6 && (op.p[5] <= vCursor.segmentPosition) || op.p[5] < vCursor.segmentPosition; } else if (localSegment.ownerType === "staff") { invariant(op.p[3] === "staves", "We are in a staff, so we can only patch the staff"); invariant(op.p[4] === localSegment.owner, "Expected staff owner " + localSegment.owner + ", got " + op.p[4]); return op.p.length === 6 && (op.p[5] <= vCursor.segmentPosition) || op.p[5] < vCursor.segmentPosition; } invariant(false, "Invalid segment owner type " + localSegment.ownerType); }); spec.fixup(localSegment, operations, restartRequired); } ; var gVoiceLayouts = lodash_1.map(gVoiceMeasure, function (voiceSegment) { var part = voiceSegment.part; gInitialAttribs[part] = gInitialAttribs[part] || []; var voiceStaves = {}; var staffContexts = {}; var xPerStaff = []; var measureIsLast = gMeasure.uuid === lodash_1.last(spec.document.measures).uuid; vCursor = new private_cursor_1.ValidationCursor(__assign({}, spec, { measureInstance: gMeasure, measureIsLast: measureIsLast, page: 1, print: lastPrint, segment: voiceSegment, staffAccidentals: null, staffAttributes: null, staffIdx: NaN, fixup: fixup })); var lCursor = new private_cursor_1.LayoutCursor(__assign({}, spec, { validationCursor: vCursor, x: spec.measureX })); /** * Processes a staff model within this voice's context. */ function pushStaffSegment(staffIdx, model, catchUp) { if (!model) { staffContexts[staffIdx].division = vCursor.segmentDivision + 1; return; } var oldDivision = vCursor.segmentDivision; var oldSegment = vCursor.segmentInstance; var oldIdx = vCursor.segmentPosition; vCursor.segmentDivision = staffContexts[staffIdx].division; vCursor.staffAccidentals = staffContexts[staffIdx].accidentals; vCursor.staffAttributes = staffContexts[staffIdx].attributes; if (catchUp) { lCursor.segmentX = xPerStaff[staffIdx]; } vCursor.segmentInstance = gStaffMeasure[part + "_" + staffIdx]; vCursor.segmentPosition = voiceStaves[staffIdx].length; var layout; model.key = "SATIE" + vCursor.measureInstance.uuid + "_parts_" + vCursor.segmentInstance.part + "_staves_" + vCursor.segmentInstance.owner + "_" + vCursor.segmentPosition; model.staffIdx = vCursor.staffIdx; if (vCursor.factory.modelHasType(model, document_1.Type.Barline)) { var totalDivisions = private_chordUtil_1.barDivisions(vCursor.staffAttributes); var divsToAdvance = totalDivisions - vCursor.segmentDivision; if (divsToAdvance > 0) { vCursor.advance(divsToAdvance); } } if (spec.mode === RefreshMode.RefreshModel) { model.refresh(vCursor.const()); } if (vCursor.factory.modelHasType(model, document_1.Type.Attributes)) { vCursor.staffAttributes = model._snapshot; vCursor.staffAccidentals = implAttributes_attributesData_1.getNativeKeyAccidentals(model._snapshot.keySignature); } if (vCursor.factory.modelHasType(model, document_1.Type.Print)) { vCursor.print = model; } if (spec.mode === RefreshMode.RefreshLayout) { layout = model.getLayout(lCursor); layout.part = part; layout.key = model.key; if (spec.preview) { lCursor.segmentX = cleanliness.x[part][voiceSegment.owner].staffX[staffIdx][lCursor.segmentPosition]; } cleanliness.x[part][voiceSegment.owner].staffX[staffIdx][lCursor.segmentPosition] = lCursor.segmentX; } invariant(isFinite(model.divCount), "%s should be a constant division count", model.divCount); vCursor.segmentDivision += model.divCount; if (vCursor.staffAttributes) { var totalDivisions = private_chordUtil_1.barDivisions(vCursor.staffAttributes); if (vCursor.segmentDivision > totalDivisions && !!gDivOverflow) { if (!gDivOverflow) { gDivOverflow = new engine_divisionOverflowException_1.default(totalDivisions, spec.measure, vCursor.staffAttributes); } invariant(totalDivisions === gDivOverflow.maxDiv, "Divisions are not consistent. Found %s but expected %s", totalDivisions, gDivOverflow.maxDiv); } } else { invariant(vCursor.segmentDivision === 0, "Expected attributes to be set on cursor"); } staffContexts[staffIdx].division = vCursor.segmentDivision; staffContexts[staffIdx].accidentals = vCursor.staffAccidentals; staffContexts[staffIdx].attributes = vCursor.staffAttributes; xPerStaff[staffIdx] = lCursor.segmentX; vCursor.segmentDivision = oldDivision; vCursor.segmentInstance = oldSegment; vCursor.segmentPosition = oldIdx; if (spec.mode === RefreshMode.RefreshLayout) { invariant(!!layout, "%s must be a valid layout", layout); } voiceStaves[staffIdx].push(layout); } var segmentLayout = []; for (var i = 0; i < voiceSegment.length; ++i) { var model = voiceSegment[i]; var atEnd = i + 1 === voiceSegment.length; var staffIdx = model.staffIdx; invariant(isFinite(model.staffIdx), "%s must be finite", model.staffIdx); if (!lCursor.lineMaxPaddingTopByStaff[model.staffIdx]) { lCursor.lineMaxPaddingTopByStaff[model.staffIdx] = 0; } if (!lCursor.lineMaxPaddingBottomByStaff[model.staffIdx]) { lCursor.lineMaxPaddingBottomByStaff[model.staffIdx] = 0; } if (!staffContexts[staffIdx]) { staffContexts[staffIdx] = { accidentals: null, attributes: gInitialAttribs[part][staffIdx], division: 0, }; } // Create a voice-staff pair if needed. We'll later merge all the // voice staff pairs. if (!voiceStaves[staffIdx]) { voiceStaves[staffIdx] = []; gStaffLayouts[part + "_" + staffIdx] = gStaffLayouts[part + "_" + staffIdx] || []; gStaffLayouts[part + "_" + staffIdx].push(voiceStaves[staffIdx]); xPerStaff[staffIdx] = 0; } vCursor.segmentPosition = i; vCursor.staffAccidentals = staffContexts[staffIdx].accidentals; vCursor.staffAttributes = staffContexts[staffIdx].attributes; vCursor.staffIdx = staffIdx; while (staffContexts[staffIdx].division <= vCursor.segmentDivision) { var nextStaffEl = gStaffMeasure[part + "_" + staffIdx][voiceStaves[staffIdx].length]; // We can mostly ignore priorities here, since except for barlines, // staff segments are more important than voice segments. var nextIsBarline = spec.factory.modelHasType(nextStaffEl, document_1.Type.Barline); if (nextIsBarline && staffContexts[staffIdx].division === vCursor.segmentDivision) { break; } // Process a staff model within a voice context. var catchUp = staffContexts[staffIdx].division < vCursor.segmentDivision; pushStaffSegment(staffIdx, nextStaffEl, catchUp); invariant(isFinite(staffContexts[staffIdx].division), "divisionPerStaff is supposed " + "to be a number, got %s", staffContexts[staffIdx].division); } // All layout that can be controlled by the model is done here. var layout = void 0; model.key = "SATIE" + vCursor.measureInstance.uuid + "_parts_" + vCursor.segmentInstance.part + "_voices_" + vCursor.segmentInstance.owner + "_" + vCursor.segmentPosition; model.staffIdx = vCursor.staffIdx; if (!vCursor.staffAccidentals) { vCursor.staffAccidentals = implAttributes_attributesData_1.getNativeKeyAccidentals(vCursor.staffAttributes.keySignature); } if (spec.mode === RefreshMode.RefreshModel) { model.refresh(vCursor.const()); } if (vCursor.factory.modelHasType(model, document_1.Type.Chord)) { lodash_1.forEach(model, function (note) { if (note.rest) { return; } var pitch = note.pitch; if ((vCursor.staffAccidentals[pitch.step + pitch.octave] || 0) !== (pitch.alter || 0) || (vCursor.staffAccidentals[pitch.step] || 0) !== (pitch.alter || 0)) { vCursor.staffAccidentals[pitch.step + pitch.octave] = pitch.alter || 0; if ((vCursor.staffAccidentals[pitch.step] || 0) !== (pitch.alter || 0)) { vCursor.staffAccidentals[pitch.step] = private_chordUtil_1.InvalidAccidental; } } }); } if (spec.mode === RefreshMode.RefreshLayout) { layout = model.getLayout(lCursor); if (spec.preview) { lCursor.segmentX = cleanliness.x[part][voiceSegment.owner].voiceX[lCursor.segmentPosition]; } cleanliness.x[part][voiceSegment.owner].voiceX[lCursor.segmentPosition] = lCursor.segmentX; layout.part = part; layout.key = model.key; } vCursor.segmentDivision += model.divCount; gMaxDivisions = Math.max(gMaxDivisions, vCursor.segmentDivision); var totalDivisions = private_chordUtil_1.barDivisions(vCursor.staffAttributes); if (vCursor.segmentDivision > totalDivisions && !spec.preview) { // Note: unfortunate copy-pasta. if (!gDivOverflow) { gDivOverflow = new engine_divisionOverflowException_1.default(totalDivisions, spec.measure, vCursor.staffAttributes); } invariant(totalDivisions === gDivOverflow.maxDiv, "Divisions are not consistent. Found %s but expected %s", totalDivisions, gDivOverflow.maxDiv); invariant(!!voiceSegment.part, "Part must be defined -- is this spec from Engine.validate$?"); } if (atEnd) { // Finalize. lodash_1.forEach(gStaffMeasure, function (staff, idx) { var pIdx = idx.lastIndexOf("_"); var staffMeasurePart = idx.substr(0, pIdx); if (staffMeasurePart !== part) { return; } var nidx = parseInt(idx.substr(pIdx + 1), 10); var voiceStaff = voiceStaves[nidx]; if (!!staff && !!voiceStaff) { while (voiceStaff.length < staff.length) { pushStaffSegment(nidx, staff[voiceStaff.length], false); } } }); } var previousAttribs = vCursor.staffAttributes; gInitialAttribs[voiceSegment.part][model.staffIdx] = previousAttribs; gPrint = vCursor.print; gMaxXInMeasure = Math.max(lCursor.segmentX, gMaxXInMeasure); gMaxPaddingTopInMeasure[model.staffIdx] = Math.max(lCursor.lineMaxPaddingTopByStaff[model.staffIdx], gMaxPaddingTopInMeasure[model.staffIdx] || 0); gMaxPaddingBottomInMeasure[model.staffIdx] = Math.max(lCursor.lineMaxPaddingBottomByStaff[model.staffIdx], gMaxPaddingBottomInMeasure[model.staffIdx] || 0); segmentLayout.push(layout); } lastPrint = spec.print; return segmentLayout; }); if (gDivOverflow) { throw gDivOverflow; } // Get an ideal voice layout for each voice-staff combination var gStaffLayoutsUnkeyed = lodash_1.values(gStaffLayouts); var gStaffLayoutsCombined = lodash_1.flatten(gStaffLayoutsUnkeyed); // Create a layout that satisfies the constraints in every single voice. // IModel.mergeSegmentsInPlace requires two passes to fully merge the layouts. // We do the second pass once we filter unneeded staff segments. var gAllLayouts = gStaffLayoutsCombined.concat(gVoiceLayouts); // We have a staff layout for every single voice-staff combination. // They will be merged, so it doesn't matter which one we pick. // Pick the first. var gStaffLayoutsUnique = lodash_1.map(gStaffLayoutsUnkeyed, function (layouts) { return layouts[0]; }); if (!spec.noAlign) { // Calculate and finish applying the master layout. // Two passes is always sufficient. var masterLayout = lodash_1.reduce(gAllLayouts, private_combinedLayout_1.mergeSegmentsInPlace, []); // Avoid lining up different divisions lodash_1.reduce(masterLayout, function (_a, layout) { var prevDivision = _a.prevDivision, min = _a.min; var newMin = layout.x; if (min >= layout.x && layout.division !== prevDivision && layout.renderClass !== document_1.Type.Spacer && layout.renderClass !== document_1.Type.Barline) { layout.x = min + 20; } return { prevDivision: layout.division, min: newMin }; }, { prevDivision: -1, min: -10 }); lodash_1.reduce(gVoiceLayouts, private_combinedLayout_1.mergeSegmentsInPlace, masterLayout); // Merge in the staves lodash_1.reduce(gStaffLayoutsUnique, private_combinedLayout_1.mergeSegmentsInPlace, masterLayout); } var gPadding = gMaxXInMeasure === spec.measureX || spec.lineBarOnLine + 1 === spec.lineTotalBarsOnLine ? 0 : 15; var newLayout; if (spec.mode === RefreshMode.RefreshLayout && spec.preview) { newLayout = { attributes: oldLayout.attributes, print: oldLayout.print, elements: oldLayout.elements, width: oldLayout.width, maxDivisions: oldLayout.maxDivisions, originX: spec.measureX, originY: {}, paddingTop: oldLayout.paddingTop, paddingBottom: oldLayout.paddingBottom, getVersion: function () { return gMeasure.version; }, uuid: gMeasure.uuid, }; } else { newLayout = { attributes: gInitialAttribs, print: gPrint, elements: gStaffLayoutsUnique.concat(gVoiceLayouts), width: gMaxXInMeasure + gPadding - spec.measureX, maxDivisions: gMaxDivisions, originX: spec.measureX, originY: {}, paddingTop: gMaxPaddingTopInMeasure, paddingBottom: gMaxPaddingBottomInMeasure, getVersion: function () { return gMeasure.version; }, uuid: gMeasure.uuid, }; } if (spec.mode === RefreshMode.RefreshLayout && !spec.preview) { cleanliness.clean = newLayout; } return newLayout; } exports.refreshMeasure = refreshMeasure; var RefreshMode; (function (RefreshMode) { RefreshMode[RefreshMode["RefreshModel"] = 0] = "RefreshModel"; RefreshMode[RefreshMode["RefreshLayout"] = 1] = "RefreshLayout"; })(RefreshMode = exports.RefreshMode || (exports.RefreshMode = {})); ; /** * Given the context and constraints given, creates a possible layout for items within a measure. * * @param opts structure with __normalized__ voices and staves * @returns an array of staff and voice layouts with an undefined order */ function layoutMeasure(_a) { var document = _a.document, header = _a.header, print = _a.print, measure = _a.measure, factory = _a.factory, x = _a.x, singleLineMode = _a.singleLineMode, preview = _a.preview, fixup = _a.fixup, lineShortest = _a.lineShortest, lineBarOnLine = _a.lineBarOnLine, lineTotalBarsOnLine = _a.lineTotalBarsOnLine, lineIndex = _a.lineIndex, lineCount = _a.lineCount, attributes = _a.attributes; var parts = lodash_1.map(private_part_1.scoreParts(header.partList), function (part) { return part.id; }); var staves = lodash_1.flatten(lodash_1.map(parts, function (partId) { return measure.parts[partId].staves; })); var voices = lodash_1.flatten(lodash_1.map(parts, function (partId) { return measure.parts[partId].voices; })); var segments = lodash_1.filter(voices.concat(staves), function (s) { return !!s; }); var status = refreshMeasure({ document: document, factory: factory, print: print, header: header, measure: measure, measureX: x, segments: segments, lineShortest: lineShortest, lineBarOnLine: lineBarOnLine, lineTotalBarsOnLine: lineTotalBarsOnLine, lineIndex: lineIndex, lineCount: lineCount, mode: RefreshMode.RefreshLayout, singleLineMode: singleLineMode, preview: preview, fixup: fixup, attributes: attributes, }); return status; } exports.layoutMeasure = layoutMeasure;