smoosic
Version:
<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i
791 lines (779 loc) • 29.7 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
import { SmoScore } from '../../smo/data/score';
import { SmoMeasure } from '../../smo/data/measure';
import { SmoTextGroup } from '../../smo/data/scoreText';
import { SmoGraceNote } from '../../smo/data/noteModifiers';
import { SmoMusic } from '../../smo/data/music';
import { SmoSystemStaff } from '../../smo/data/systemStaff';
import { SmoPartInfo } from '../../smo/data/partInfo';
import { StaffModifierBase } from '../../smo/data/staffModifiers';
import { SmoSelection, SmoSelector } from '../../smo/xform/selections';
import { UndoBuffer, copyUndo } from '../../smo/xform/undo';
import { PasteBuffer } from '../../smo/xform/copypaste';
import { SuiScroller } from './scroller';
import { SvgHelpers } from './svgHelpers';
import { SuiTracker } from './tracker';
import { createTopDomContainer } from '../../common/htmlHelpers';
import { SmoRenderConfiguration } from './configuration';
import { SuiRenderState, scoreChangeEvent } from './renderState';
import { ScoreRenderParams } from './scoreRender';
import { SmoOperation } from '../../smo/xform/operations';
import { SuiAudioPlayer } from '../audio/player';
import { SuiAudioAnimationParams } from '../audio/musicCursor';
import { SmoTempoText } from '../../smo/data/measureModifiers';
import { TimeSignature } from '../../smo/data/measureModifiers';
declare var $: any;
/**
* Indicates a stave is/is not displayed in the score
* @category SuiRender
*/
export interface ViewMapEntry {
show: boolean;
}
export type updateSelectionFunc = (score: SmoScore, selections: SmoSelection[]) => void;
export type updateSingleSelectionFunc = (score: SmoScore, selection: SmoSelection) => void;
export type updateStaffModifierFunc = (score: SmoScore, fromSelection: SmoSelection, toSelection: SmoSelection) => void;
/**
* Base class for all operations on the rendered score. The base class handles the following:
* 1. Undo and recording actions for the operation
* 2. Maintain/change which staves in the score are displayed (staff map)
* 3. Mapping between the displayed score and the data representation
* @category SuiRender
*/
export abstract class SuiScoreView {
static Instance: SuiScoreView | null = null;
score: SmoScore; // The score that is displayed
storeScore: SmoScore; // the full score, including invisible staves
staffMap: number[]; // mapping the 2 things above
storeUndo: UndoBuffer; // undo buffer for operations to above
tracker: SuiTracker; // UI selections
renderer: SuiRenderState;
scroller: SuiScroller;
storePaste: PasteBuffer;
config: SmoRenderConfiguration;
audioAnimation: SuiAudioAnimationParams;
constructor(config: SmoRenderConfiguration, svgContainer: HTMLElement, score: SmoScore, scrollSelector: HTMLElement, undoBuffer: UndoBuffer) {
this.score = score;
const renderParams: ScoreRenderParams = {
elementId: svgContainer,
score,
config,
undoBuffer
};
this.audioAnimation = config.audioAnimation;
this.renderer = new SuiRenderState(renderParams);
this.config = config;
const scoreJson = score.serialize({ skipStaves: false, useDictionary: false, preserveStaffIds: true });
this.scroller = new SuiScroller(scrollSelector, this.renderer.renderer.vexContainers);
this.storePaste = new PasteBuffer();
this.tracker = new SuiTracker(this.renderer, this.scroller);
this.renderer.setMeasureMapper(this.tracker);
this.storeScore = SmoScore.deserialize(JSON.stringify(scoreJson));
this.score.synchronizeTextGroups(this.storeScore.textGroups);
this.storeUndo = new UndoBuffer();
this.staffMap = this.defaultStaffMap;
SuiScoreView.Instance = this; // for debugging
this.setMappedStaffIds();
createTopDomContainer('.saveLink'); // for file upload
}
/**
* Await on the full update of the score
* @returns
*/
async renderPromise(): Promise<any> {
return this.renderer.renderPromise();
}
/**
* Await on the partial update of the score in the view
* @returns
*/
async updatePromise(): Promise<any> {
return this.renderer.updatePromise();
}
async awaitRender(): Promise<any> {
this.renderer.rerenderAll();
return this.renderer.updatePromise();
}
/**
* await on the full update of the score, also resetting the viewport (to reflect layout changes)
* @returns
*/
async refreshViewport(): Promise<any> {
this.renderer.preserveScroll();
this.renderer.setViewport();
this.renderer.setRefresh();
await this.renderer.renderPromise();
}
handleScrollEvent(scrollLeft: number, scrollTop: number) {
this.tracker.scroller.handleScroll(scrollLeft, scrollTop);
}
getPartMap(): { keys: number[], partMap: Record<number, SmoPartInfo> } {
let keepNext = false;
let partCount = 0;
let partMap: Record<number, SmoPartInfo> = {};
const keys: number[] = [];
this.storeScore.staves.forEach((staff) => {
const partInfo = staff.partInfo;
partInfo.associatedStaff = staff.staffId;
if (!keepNext) {
partMap[partCount] = partInfo;
keys.push(partCount);
partCount += 1;
if (partInfo.stavesAfter > 0) {
keepNext = true;
}
} else {
keepNext = false;
}
});
return { keys, partMap };
}
/**
* Any method that modifies a set of selections can call this to update
* the score view and the backing score.
* @param actor
* @param selections
*/
async modifyCurrentSelections(label: string, actor: updateSelectionFunc) {
const altSelections = this._getEquivalentSelections(this.tracker.selections);
this.undoTrackerMeasureSelections(label);
actor(this.score, this.tracker.selections);
actor(this.storeScore, altSelections);
this._renderChangedMeasures(SmoSelection.getMeasureList(this.tracker.selections));
await this.updatePromise();
}
/**
* Any method that modifies a set of selections can call this to update
* the score view and the backing score.
* @param actor
* @param selections
*/
async modifySelection(label: string, selection: SmoSelection, actor: updateSelectionFunc) {
const altSelection = this._getEquivalentSelection(selection);
this.undoTrackerMeasureSelections(label);
actor(this.score, [selection]);
if (altSelection) {
actor(this.storeScore, [altSelection]);
}
this._renderChangedMeasures(SmoSelection.getMeasureList([selection]));
await this.updatePromise();
}
/**
* Any method that modifies a set of selections can call this to update
* the score view and the backing score.
* @param actor
* @param selections
*/
async modifySelectionNoWait(label: string, selection: SmoSelection, actor: updateSingleSelectionFunc) {
const altSelection = this._getEquivalentSelection(selection);
this.undoTrackerMeasureSelections(label);
actor(this.score, selection);
if (altSelection) {
actor(this.storeScore, altSelection);
}
this._renderChangedMeasures(SmoSelection.getMeasureList([selection]));
}
/**
* Modifiy a set of columns, e.g. tempo, time, key. This has different undo behavior, don't
* pend on the result because there may be a combination of operations.
* @param label
* @param selections
* @param actor
*/
modifyColumnsSelectionsNoWait(label: string, selections: SmoSelection[], actor: updateSingleSelectionFunc) {
this.undoColumnRange(label, selections);
selections.forEach((selection) => {
const altSelection = this._getEquivalentSelection(selection);
actor(this.score, selection);
if (altSelection) {
actor(this.storeScore, altSelection);
}
});
this._renderChangedMeasures(selections);
}
/**
* This is used in some Smoosic demos and pens.
* @param action any action, but most usefully a SuiScoreView method
* @param repetition number of times to repeat, waiting on render promise between
* if not specified, defaults to 1
* @returns promise, resolved action has been completed and score is updated.
*/
async waitableAction(action: () => void, repetition?: number) {
const rep = repetition ?? 1;
const self = this;
const promise = new Promise((resolve: any) => {
const fc = async (count: number) => {
if (count > 0) {
action();
await self.renderer.updatePromise();
fc(count - 1);
} else {
resolve();
}
};
fc(rep);
});
return promise;
}
/**
* The plural form of _getEquivalentSelection
* @param selections
* @returns
*/
_getEquivalentSelections(selections: SmoSelection[]): SmoSelection[] {
const rv: SmoSelection[] = [];
selections.forEach((selection) => {
const sel = this._getEquivalentSelection(selection);
if (sel !== null) {
rv.push(sel);
}
});
return rv;
}
// Get a long enough list of measures to paste into
getPasteMeasureList() {
// The length of the paste buffer, in ticks
const ticksToPaste = this.storePaste.getCopyBufferTickCount();
const selections: SmoSelection[] = SmoSelection.getMeasureList(this.tracker.selections);
if (!selections.length) {
return [];
}
const destination = selections[0];
selections.splice(0);
selections.push(destination);
const voice = destination.selector.voice;
const tm = destination.measure.tickmapForVoice(voice);
// length of first selected measure, in ticks
const measureTicks = destination.measure.getTicksFromVoice(voice);
// remaining ticks after first selection. This is our starting point.
let startTick = measureTicks - tm.durationMap[this.tracker.selections[0].selector.tick];
let currentMeasure = destination.selector.measure + 1;
// if we are short, and there are measures left, add them to the selection list
while (startTick < ticksToPaste && destination.staff.measures.length > currentMeasure) {
const newSel = SmoSelection.measureSelection(this.score,
destination.selector.staff, currentMeasure);
if (newSel) {
selections.push(newSel);
startTick += newSel.measure.getTicksFromThisOrAnyVoice(voice);
}
currentMeasure += 1;
}
return selections;
}
/**
* A staff modifier has changed, create undo operations for the measures affected
* @param label
* @param staffModifier
* @param subtype
*/
undoStaffModifier(label: string, staffModifier: StaffModifierBase, subtype: number) {
const copy = StaffModifierBase.deserialize(staffModifier.serialize());
copy.startSelector = this._getEquivalentSelector(copy.startSelector);
copy.endSelector = this._getEquivalentSelector(copy.endSelector);
const copySer = copy.serialize();
// Copy ID so we can undo properly
copySer.attrs = JSON.parse(JSON.stringify(staffModifier.attrs));
this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.STAFF_MODIFIER, SmoSelector.default,
copySer, subtype);
}
/**
* Return the index of the page that is in the center of the client screen.
*/
getFocusedPage(): number {
if (this.score.layoutManager === undefined) {
return 0;
}
const scrollAvg = this.tracker.scroller.netScroll.y + (this.tracker.scroller.viewport.height / 2);
const midY = scrollAvg;
const layoutManager = this.score.layoutManager.getGlobalLayout();
const lh = layoutManager.pageHeight / layoutManager.svgScale;
const lw = layoutManager.pageWidth / layoutManager.svgScale;
const pt = this.renderer.pageMap.svgToClient(SvgHelpers.smoBox({ x: lw, y: lh }));
return Math.round(midY / pt.y);
}
/**
* Create a rectangle undo, like a multiple columns but not necessarily the whole
* score.
*/
_undoColumn(label: string, measureIndex: number) {
this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.COLUMN, SmoSelector.default,
{ score: this.storeScore, measureIndex }, UndoBuffer.bufferSubtypes.NONE);
}
/**
* Score preferences don't affect the display, but they do have an undo
* @param label
*/
_undoScorePreferences(label: string) {
this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.SCORE_ATTRIBUTES, SmoSelector.default, this.storeScore, UndoBuffer.bufferSubtypes.NONE);
}
undoColumnRange(label: string, measureSelections: SmoSelection[]) {
const checked: Record<number, boolean> = {};
measureSelections.forEach((measureSelection) => {
if (!checked[measureSelection.selector.measure]) {
checked[measureSelection.selector.measure] = true;
this._undoColumn(label, measureSelection.selector.measure);
}
});
}
undoMeasureRange(label: string, measureSelections: SmoSelection[]) {
measureSelections.forEach((measureSelection) => {
const equiv = this._getEquivalentSelection(measureSelection);
if (equiv !== null) {
this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.MEASURE, equiv.selector, equiv.measure,
UndoBuffer.bufferSubtypes.NONE);
}
});
return measureSelections;
}
/**
* Add to the undo buffer the current set of measures selected.
* @param label
* @returns
*/
undoTrackerMeasureSelections(label: string): SmoSelection[] {
const measureSelections = SmoSelection.getMeasureList(this.tracker.selections);
return this.undoMeasureRange(label, measureSelections);
}
/**
* operation that only affects the first selection. Setup undo for the measure
*/
_undoFirstMeasureSelection(label: string): SmoSelection {
const sel = this.tracker.selections[0];
const equiv = this._getEquivalentSelection(sel);
if (equiv !== null) {
this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.MEASURE, equiv.selector, equiv.measure,
UndoBuffer.bufferSubtypes.NONE);
}
return sel;
}
/**
* Add the selection to the undo buffer
* @param label
* @param selection
*/
_undoSelection(label: string, selection: SmoSelection) {
const equiv = this._getEquivalentSelection(selection);
if (equiv !== null) {
this.storeUndo.addBuffer(label,
UndoBuffer.bufferTypes.MEASURE, equiv.selector, equiv.measure,
UndoBuffer.bufferSubtypes.NONE);
}
}
/**
* Add multiple selections to the undo buffer as a group
* @param label
* @param selections
*/
_undoSelections(label: string, selections: SmoSelection[]) {
this.storeUndo.grouping = true;
selections.forEach((selection) => {
this._undoSelection(label, selection);
});
this.storeUndo.grouping = false;
}
/**
* Update renderer for measures that have changed
*/
_renderChangedMeasures(measureSelections: SmoSelection[]) {
if (!Array.isArray(measureSelections)) {
measureSelections = [measureSelections];
}
measureSelections.forEach((measureSelection) => {
this.renderer.addToReplaceQueue(measureSelection);
});
}
/**
* Update renderer for some columns
* @param fromSelector
* @param toSelector
*/
_renderRectangle(fromSelector: SmoSelector, toSelector: SmoSelector) {
this._getRectangleSelections(fromSelector, toSelector).forEach((s) => {
this.renderer.addToReplaceQueue(s);
});
}
/**
* Setup undo for operation that affects the whole score
* @param label
*/
_undoScore(label: string) {
this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.SCORE, SmoSelector.default, this.storeScore,
UndoBuffer.bufferSubtypes.NONE);
}
/**
* Get the selector from this.storeScore that maps to the displayed selector from this.score
* @param selector
* @returns
*/
_getEquivalentSelector(selector: SmoSelector) {
const rv = JSON.parse(JSON.stringify(selector));
rv.staff = this.staffMap[selector.staff];
return rv;
}
/**
* Get the equivalent staff id from this.storeScore that maps to the displayed selector from this.score
* @param staffId
* @returns
*/
_getEquivalentStaff(staffId: number) {
return this.staffMap[staffId];
}
/**
* Get the equivalent selection from this.storeScore that maps to the displayed selection from this.score
* @param selection
* @returns
*/
_getEquivalentSelection(selection: SmoSelection): SmoSelection | null {
try {
if (typeof (selection.selector.tick) === 'undefined') {
return SmoSelection.measureSelection(this.storeScore, this.staffMap[selection.selector.staff], selection.selector.measure);
}
if (typeof (selection.selector.pitches) === 'undefined') {
return SmoSelection.noteSelection(this.storeScore, this.staffMap[selection.selector.staff], selection.selector.measure, selection.selector.voice,
selection.selector.tick);
}
return SmoSelection.pitchSelection(this.storeScore, this.staffMap[selection.selector.staff], selection.selector.measure, selection.selector.voice,
selection.selector.tick, selection.selector.pitches);
} catch (ex) {
console.warn(ex);
return null;
}
}
/**
* Get the equivalent selection from this.storeScore that maps to the displayed selection from this.score
* @param selection
* @returns
*/
_getEquivalentGraceNote(selection: SmoSelection, gn: SmoGraceNote): SmoGraceNote {
if (selection.note !== null) {
const rv = selection.note.getGraceNotes().find((gg) => gg.attrs.id === gn.attrs.id);
if (rv) {
return rv;
}
}
return gn;
}
/**
* Get the rectangle of selections indicated by the parameters from the score
* @param startSelector
* @param endSelector
* @param score
* @returns
*/
_getRectangleSelections(startSelector: SmoSelector, endSelector: SmoSelector): SmoSelection[] {
const rv: SmoSelection[] = [];
let i = 0;
let j = 0;
for (i = startSelector.staff; i <= endSelector.staff; i++) {
for (j = startSelector.measure; j <= endSelector.measure; j++) {
const target = SmoSelection.measureSelection(this.score, i, j);
if (target !== null) {
rv.push(target);
}
}
}
return rv;
}
/**
* set the grouping flag for undo operations
* @param val
*/
groupUndo(val: boolean) {
this.storeUndo.grouping = val;
}
/**
* Show all staves, 1:1 mapping of view score staff to stored score staff
*/
get defaultStaffMap(): number[] {
let i = 0;
const rv: number[] = [];
for (i = 0; i < this.storeScore.staves.length; ++i) {
rv.push(i);
}
return rv;
}
/**
* Bootstrapping function, creates the renderer and associated timers
*/
startRenderingEngine() {
if (!this.renderer.score) {
// If there is only one part, display the part.
if (this.storeScore.isPartExposed()) {
this.exposePart(this.score.staves[0]);
}
// If the score is transposing, hide the instrument xpose settings
this._setTransposing();
this.renderer.score = this.score;
this.renderer.setViewport();
}
this.renderer.startDemon();
}
/**
* Gets the current mapping of displayed staves to score staves (this.storeScore)
* @returns
*/
getView(): ViewMapEntry[] {
const rv = [];
let i = 0;
for (i = 0; i < this.storeScore.staves.length; ++i) {
const show = this.staffMap.indexOf(i) >= 0;
rv.push({ show });
}
return rv;
}
/**
* Update the staff ID when the view changes
*/
setMappedStaffIds() {
this.score.staves.forEach((staff) => {
if (!this.isPartExposed()) {
staff.partInfo.displayCues = staff.partInfo.cueInScore;
} else {
staff.partInfo.displayCues = false;
}
staff.setMappedStaffId(this.staffMap[staff.staffId]);
});
}
resetPartView() {
if (this.staffMap.length === 1) {
const staff = this.storeScore.staves[this.staffMap[0]];
this.exposePart(staff);
}
}
/**
* Exposes a part: hides non-part staves, shows part staves.
* Note this will reset the view. After this operation, staff 0 will
* be the selected part.
* @param staff
*/
exposePart(staff: SmoSystemStaff) {
let i = 0;
const exposeMap: ViewMapEntry[] = [];
let pushNext = false;
for (i = 0; i < this.storeScore.staves.length; ++i) {
const tS = this.storeScore.staves[i];
const show = tS.staffId === staff.staffId;
if (pushNext) {
exposeMap.push({ show: true });
pushNext = false;
} else {
exposeMap.push({ show });
if (tS.partInfo.stavesAfter > 0 && show) {
pushNext = true;
}
}
}
this.setView(exposeMap);
}
/**
* Indicates if the score is displaying in part-mode vs. score mode.
* @returns
*/
isPartExposed(): boolean {
return this.score.isPartExposed();
}
/**
* Parts have different formatting options from the parent score, indluding layout. Reset
* them when exposing a part.
*/
_mapPartFormatting() {
this.score.layoutManager = this.score.staves[0].partInfo.layoutManager;
let replacedText = false;
this.score.staves.forEach((staff) => {
staff.updateMeasureFormatsForPart();
if (staff.partInfo.preserveTextGroups && !replacedText) {
const tga: SmoTextGroup[] = [];
replacedText = true;
staff.partInfo.textGroups.forEach((tg) => {
tga.push(tg);
});
this.score.textGroups = tga;
}
});
}
/**
* Update the list of staves in the score that are displayed.
*/
setView(rows: ViewMapEntry[]) {
let i = 0;
const any = rows.find((row) => row.show === true);
if (!any) {
return;
}
const nscore = SmoScore.deserialize(JSON.stringify(this.storeScore.serialize(
{ skipStaves: true, useDictionary: false, preserveStaffIds: false })));
const staffMap = [];
for (i = 0; i < rows.length; ++i) {
const row = rows[i];
if (row.show) {
const srcStave = this.storeScore.staves[i];
const jsonObj = srcStave.serialize({ skipMaps: false, preserveIds: true });
jsonObj.staffId = staffMap.length;
const nStave = SmoSystemStaff.deserialize(jsonObj);
nStave.mapStaffFromTo(i, nscore.staves.length);
nscore.staves.push(nStave);
if (srcStave.keySignatureMap) {
nStave.keySignatureMap = JSON.parse(JSON.stringify(srcStave.keySignatureMap));
}
nStave.measures.forEach((measure: SmoMeasure, ix) => {
const srcMeasure = srcStave.measures[ix];
measure.tempo = new SmoTempoText(srcMeasure.tempo.serialize());
measure.timeSignature = new TimeSignature(srcMeasure.timeSignature);
measure.keySignature = srcMeasure.keySignature;
});
staffMap.push(i);
}
}
nscore.numberStaves();
this.staffMap = staffMap;
this.score = nscore;
// Indicate which score staff view staves are mapped to, to decide to display
// modifiers.
this.setMappedStaffIds();
// TODO: add part-specific measure formatting, etc.
this._setTransposing();
this.renderer.score = nscore;
// If this current view is a part, show the part layout
if (this.isPartExposed()) {
this._mapPartFormatting();
this.score.staves.forEach((staff) => {
staff.partInfo.displayCues = false;
});
SmoOperation.computeMultipartRest(nscore);
} else {
this.score.staves.forEach((staff) => {
staff.partInfo.displayCues = staff.partInfo.cueInScore;
});
}
window.dispatchEvent(new CustomEvent(scoreChangeEvent, { detail: { view: this } }));
this.renderer.setViewport();
}
/**
* view all the staffs in score mode.
*/
viewAll() {
this.score = SmoScore.deserialize(JSON.stringify(
this.storeScore.serialize({ skipStaves: false, useDictionary: false, preserveStaffIds: true })));
this.staffMap = this.defaultStaffMap;
this.setMappedStaffIds();
this._setTransposing();
this.score.synchronizeTextGroups(this.storeScore.textGroups);
this.renderer.score = this.score;
window.dispatchEvent(new CustomEvent(scoreChangeEvent, { detail: { view: this } }));
this.renderer.setViewport();
}
/**
* Update score based on transposing flag.
*/
_setTransposing() {
if (!this.isPartExposed()) {
const xpose = this.score.preferences?.transposingScore;
if (xpose) {
this.score.setTransposing();
}
}
}
/**
* Update the view after loading or restoring a completely new score
* @param score
* @returns
*/
async changeScore(score: SmoScore) {
this.storeUndo.reset();
SuiAudioPlayer.stopPlayer();
this.renderer.score = score;
this.renderer.setViewport();
this.storeScore = SmoScore.deserialize(JSON.stringify(
score.serialize({ skipStaves: false, useDictionary: false, preserveStaffIds: true })));
this.score = score;
// If the score is non-transposing, hide the instrument xpose settings
this._setTransposing();
this.staffMap = this.defaultStaffMap;
this.setMappedStaffIds();
this.score.synchronizeTextGroups(this.storeScore.textGroups);
if (this.storeScore.isPartExposed()) {
this.exposePart(this.score.staves[0]);
}
const rv = await this.awaitRender();
window.dispatchEvent(new CustomEvent(scoreChangeEvent, { detail: { view: this } }));
return rv;
}
replaceMeasureView(measureRange: number[]) {
for (let i = measureRange[0]; i <= measureRange[1]; ++i) {
this.score.staves.forEach((staff) => {
const staffId = staff.staffId;
const altStaff = this.storeScore.staves[this._getEquivalentStaff(staffId)];
if (altStaff) {
staff.syncStaffModifiers(i, altStaff);
}
// Get a copy of the backing score, and map it to the score stave. this.score may have fewer staves
// than this.storeScore
const svg = JSON.parse(JSON.stringify(staff.measures[i].svg));
const serialized = UndoBuffer.serializeMeasure(this.storeScore.staves[this.staffMap[staffId]].measures[i]);
serialized.measureNumber.staffId = staffId;
const xpose = serialized.transposeIndex ?? 0;
const concertKey = SmoMusic.vexKeySigWithOffset(serialized.keySignature ?? 'c', -1 * xpose);
serialized.keySignature = concertKey;
const rmeasure = SmoMeasure.deserialize(serialized);
rmeasure.svg = svg;
// If this is a tranposed score, the displayed score needs to be in 'C'.
// We do this step last since serialize/unserialize work in a pitch transposed
// for the instrument
if (this.score.preferences.transposingScore && !this.score.isPartExposed()) {
rmeasure.transposeToOffset(-1 * xpose, 'c');
rmeasure.keySignature = 'c';
rmeasure.transposeIndex = 0;
}
const selector: SmoSelector = { staff: staffId, measure: i, voice: 0, tick: 0, pitches: [] };
this.score.replaceMeasure(selector, rmeasure);
});
this.renderer.addColumnToReplaceQueue(i);
}
}
/**
* for the view score, the renderer decides what to render
* depending on what is undone.
* @returns
*/
async undo() {
if (!this.renderer.score) {
return;
}
if (!this.storeUndo.buffersAvailable()) {
return;
}
const staffMap: Record<number, number> = {};
const identityMap: Record<number, number> = {};
this.defaultStaffMap.forEach((nn) => identityMap[nn] = nn);
this.staffMap.forEach((mm, ix) => staffMap[mm] = ix);
// A score-level undo might have changed the score.
const fullScore = this.storeUndo.undoScorePeek();
// text undo is handled differently since there is usually not
// an associated measure.
const scoreText = this.storeUndo.undoScoreTextGroupPeek();
const partText = this.storeUndo.undoPartTextGroupPeek();
if (scoreText || partText) {
await this.renderer.unrenderTextGroups();
}
const measureRange = this.storeUndo.getMeasureRange();
if (!(fullScore || scoreText || partText)) {
for (let i = measureRange[0]; i <= measureRange[1]; ++i) {
this.renderer.unrenderColumn(this.score.staves[0].measures[i]);
}
}
this.storeScore = this.storeUndo.undo(this.storeScore, identityMap, true);
if (fullScore) {
this.viewAll();
this.renderer.setRefresh();
} else if (partText) {
this.setView(this.getView());
} else if (scoreText) {
this.score.synchronizeTextGroups(this.storeScore.textGroups);
this.renderer.rerenderTextGroups();
} else {
this.replaceMeasureView(measureRange);
}
await this.renderer.updatePromise();
}
}