');\r\n $('body').append(dbgDiv); \r\n }\r\n if (layoutDebug.mask & layoutDebug.values.play) {\r\n const dbgDiv = $('
');\r\n $('body').append(dbgDiv); \r\n }\r\n }\r\n static updateScrollDebug(point: SvgPoint) {\r\n const displayString = 'X: ' + point.x + ' Y: ' + point.y;\r\n $('.scroll-box-debug').text(displayString);\r\n $('.scroll-box-debug').css('left', '2%').css('top', '20px');\r\n }\r\n static updateMouseDebug(client: SvgPoint, logical: SvgPoint, offset: SvgPoint) {\r\n const displayString = `clientX: ${client.x} clientY: ${client.y} svg: (${logical.x},${logical.y}) offset (${offset.x}, ${offset.y})`;\r\n $('.mouse-debug').text(displayString);\r\n $('.mouse-debug').css('left', '2%').css('top', '60px').css('position','absolute').css('font-size','11px');\r\n }\r\n static updateDragDebug(client: SvgPoint, logical: SvgPoint, state: string) {\r\n const displayString = `clientX: ${client.x} clientY: ${client.y} svg: (${logical.x},${logical.y}) state ${state})`;\r\n $('.drag-debug').text(displayString);\r\n $('.drag-debug').css('left', '2%').css('top', '80px').css('position','absolute').css('font-size','11px');\r\n }\r\n static updatePlayDebug(selector: SmoSelector, logical: SvgBox) {\r\n const displayString = `mm: ${selector.measure} tick: ${selector.tick} svg: (${logical.x},${logical.y}, ${logical.width}, ${logical.height})`;\r\n $('.play-debug').text(displayString);\r\n $('.play-debug').css('left', '2%').css('top', '100px').css('position','absolute').css('font-size','11px');\r\n }\r\n\r\n static addTextDebug(value: number) {\r\n layoutDebug._textDebug.push(value);\r\n //console.log(value);\r\n }\r\n\r\n static addDialogDebug(value: string) {\r\n layoutDebug._dialogEvents.push(value);\r\n // console.log(value);\r\n }\r\n\r\n static measureHistory(measure: SmoMeasure, oldVal: string, newVal: any, description: string) {\r\n if (layoutDebug.flagSet(layoutDebug.values.measureHistory)) {\r\n var oldExp = (typeof ((measure as any).svg[oldVal]) == 'object') ?\r\n JSON.stringify((measure as any).svg[oldVal]).replace(/\"/g, '') : (measure as any).svg[oldVal];\r\n var newExp = (typeof (newVal) == 'object') ? JSON.stringify(newVal).replace(/\"/g, '') : newVal;\r\n measure.svg.history.push(oldVal + ': ' + oldExp + '=> ' + newExp + ' ' + description);\r\n }\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SmoSelector, SmoSelection, ModifierTab } from '../../smo/xform/selections';\r\nimport { OutlineInfo, SvgHelpers } from './svgHelpers';\r\nimport { layoutDebug } from './layoutDebug';\r\nimport { SuiScroller } from './scroller';\r\nimport { SmoSystemStaff } from '../../smo/data/systemStaff';\r\nimport { SmoMeasure, SmoVoice } from '../../smo/data/measure';\r\nimport { PasteBuffer } from '../../smo/xform/copypaste';\r\nimport { SmoNoteModifierBase, SmoLyric } from '../../smo/data/noteModifiers';\r\nimport { SvgBox } from '../../smo/data/common';\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { SmoScore, SmoModifier } from '../../smo/data/score';\r\nimport { SvgPageMap } from './svgPageMap';\r\n\r\n/**\r\n * DI information about renderer, so we can notify renderer and it can contain\r\n * a tracker object\r\n * @param pageMap {@link SvgPageMap}: SvgPageMap - container of SVG elements and vex renderers\r\n * @param score {@link SmoScore}\r\n * @param dirty lets the caller know the display needs update\r\n * @param passState state machine in rendering part/all of the score\r\n * @param renderPromise awaits on render all\r\n * @param addToReplaceQueue adds a measure to the quick update queue\r\n * @param renderElement a little redundant with svg\r\n * @category SuiRender\r\n */\r\nexport interface SuiRendererBase {\r\n pageMap: SvgPageMap,\r\n score: SmoScore | null,\r\n dirty: boolean,\r\n passState: number,\r\n renderPromise(): Promise
,\r\n addToReplaceQueue(mm: SmoSelection[]): void,\r\n renderElement: Element\r\n}\r\n// used to perform highlights in the backgroundd\r\nexport interface HighlightQueue {\r\n selectionCount: number, deferred: boolean\r\n}\r\n/**\r\n * Map the notes in the svg so they can respond to events and interact\r\n * with the mouse/keyboard\r\n * @category SuiRender\r\n */\r\nexport abstract class SuiMapper {\r\n renderer: SuiRendererBase;\r\n scroller: SuiScroller;\r\n // measure to selector map\r\n measureNoteMap: Record = {};\r\n // notes currently selected. Something is always selected\r\n // modifiers (text etc.) that have been selected\r\n modifierSelections: ModifierTab[] = [];\r\n selections: SmoSelection[] = [];\r\n // The list of modifiers near the current selection\r\n localModifiers: ModifierTab[] = [];\r\n modifierIndex: number = -1;\r\n modifierSuggestion: ModifierTab | null = null;\r\n pitchIndex: number = -1;\r\n // By default, defer highlights for performance.\r\n deferHighlightMode: boolean = true;\r\n suggestion: SmoSelection | null = null;\r\n pasteBuffer: PasteBuffer;\r\n highlightQueue: HighlightQueue;\r\n mouseHintBox: OutlineInfo | null = null;\r\n selectionRects: Record = {};\r\n outlines: Record = {};\r\n mapping: boolean = false;\r\n constructor(renderer: SuiRendererBase, scroller: SuiScroller, pasteBuffer: PasteBuffer) {\r\n // renderer renders the music when it changes\r\n this.renderer = renderer;\r\n this.scroller = scroller;\r\n this.modifierIndex = -1;\r\n this.localModifiers = [];\r\n // index if a single pitch of a chord is selected\r\n this.pitchIndex = -1;\r\n // the current selection, which is also the copy/paste destination\r\n this.pasteBuffer = pasteBuffer;\r\n this.highlightQueue = { selectionCount: 0, deferred: false };\r\n }\r\n\r\n abstract highlightSelection(): void;\r\n abstract _growSelectionRight(hold?: boolean): number; \r\n abstract _setModifierAsSuggestion(sel: ModifierTab): void;\r\n abstract _setArtifactAsSuggestion(sel: SmoSelection): void;\r\n abstract getIdleTime(): number;\r\n updateHighlight() {\r\n const self = this;\r\n if (this.selections.length === 0) {\r\n this.highlightQueue.deferred = false;\r\n this.highlightQueue.selectionCount = 0;\r\n return;\r\n }\r\n if (this.highlightQueue.selectionCount === this.selections.length) {\r\n this.highlightSelection();\r\n this.highlightQueue.deferred = false;\r\n } else {\r\n this.highlightQueue.selectionCount = this.selections.length;\r\n setTimeout(() => {\r\n self.updateHighlight();\r\n }, 50);\r\n }\r\n }\r\n deferHighlight() {\r\n if (!this.deferHighlightMode) {\r\n this.highlightSelection();\r\n }\r\n const self = this;\r\n if (!this.highlightQueue.deferred) {\r\n this.highlightQueue.deferred = true;\r\n setTimeout(() => {\r\n self.updateHighlight();\r\n }, 50);\r\n }\r\n }\r\n _createLocalModifiersList() {\r\n this.localModifiers = [];\r\n let index = 0;\r\n this.selections.forEach((sel) => {\r\n sel.note?.getGraceNotes().forEach((gg) => {\r\n this.localModifiers.push({ index, selection: sel, modifier: gg, box: gg.logicalBox ?? SvgBox.default });\r\n index += 1;\r\n });\r\n sel.note?.getModifiers('SmoDynamicText').forEach((dyn) => {\r\n this.localModifiers.push({ index, selection: sel, modifier: dyn, box: dyn.logicalBox ?? SvgBox.default });\r\n index += 1;\r\n });\r\n sel.measure.getModifiersByType('SmoVolta').forEach((volta) => {\r\n this.localModifiers.push({ index, selection: sel, modifier: volta, box: volta.logicalBox ?? SvgBox.default });\r\n index += 1;\r\n });\r\n sel.measure.getModifiersByType('SmoTempoText').forEach((tempo) => {\r\n this.localModifiers.push({ index, selection: sel, modifier: tempo, box: tempo.logicalBox ?? SvgBox.default });\r\n index += 1;\r\n });\r\n sel.staff.renderableModifiers.forEach((mod) => {\r\n if (SmoSelector.gteq(sel.selector, mod.startSelector) &&\r\n SmoSelector.lteq(sel.selector, mod.endSelector) && mod.logicalBox) {\r\n const exists = this.localModifiers.find((mm) => mm.modifier.ctor === mod.ctor);\r\n if (!exists) {\r\n this.localModifiers.push({ index, selection: sel, modifier: mod, box: mod.logicalBox });\r\n index += 1;\r\n }\r\n }\r\n });\r\n });\r\n }\r\n /**\r\n * When a modifier is selected graphically, update the selection list\r\n * and create a local modifier list\r\n * @param modifierTabs \r\n */\r\n createLocalModifiersFromModifierTabs(modifierTabs: ModifierTab[]) {\r\n const selections: SmoSelection[] = [];\r\n const modMap: Record = {};\r\n modifierTabs.forEach((mt) => {\r\n if (mt.selection) {\r\n const key = SmoSelector.getNoteKey(mt.selection.selector);\r\n if (!modMap[key]) {\r\n selections.push(mt.selection);\r\n modMap[key] = true;\r\n }\r\n }\r\n });\r\n if (selections.length) {\r\n this.selections = selections;\r\n this._createLocalModifiersList();\r\n this.deferHighlight();\r\n }\r\n }\r\n // used by remove dialogs to clear removed thing\r\n clearModifierSelections() {\r\n this.modifierSelections = [];\r\n this._createLocalModifiersList();\r\n this.modifierIndex = -1;\r\n if (this.outlines['staffModifier'] && this.outlines['staffModifier'].element) {\r\n this.outlines['staffModifier'].element.remove();\r\n this.outlines['staffModifier'].element = undefined;\r\n }\r\n // this.eraseRect('staffModifier'); not sure where this should go\r\n }\r\n // ### loadScore\r\n // We are loading a new score. clear the maps so we can rebuild them after\r\n // rendering\r\n loadScore() {\r\n this.measureNoteMap = {};\r\n this.clearModifierSelections();\r\n this.selections = [];\r\n this.highlightQueue = { selectionCount: 0, deferred: false };\r\n }\r\n\r\n // ### _clearMeasureArtifacts\r\n // clear the measure from the measure and note maps so we can rebuild it.\r\n clearMeasureMap(measure: SmoMeasure) {\r\n const selector = { staff: measure.measureNumber.staffId, measure: measure.measureNumber.measureIndex, voice: 0, tick: 0, pitches: [] };\r\n\r\n // Unselect selections in this measure so we can reselect them when re-tracked\r\n const ar: SmoSelection[] = [];\r\n this.selections.forEach((selection) => {\r\n if (selection.selector.staff !== selector.staff || selection.selector.measure !== selector.measure) {\r\n ar.push(selection);\r\n }\r\n });\r\n this.selections = ar;\r\n }\r\n\r\n _copySelectionsByMeasure(staffIndex: number, measureIndex: number) {\r\n const rv = this.selections.filter((sel) => sel.selector.staff === staffIndex && sel.selector.measure === measureIndex);\r\n const ticks = rv.length < 1 ? 0 : rv.map((sel) => (sel.note as SmoNote).tickCount).reduce((a, b) => a + b);\r\n const selectors: SmoSelector[] = [];\r\n rv.forEach((sel) => {\r\n const nsel = JSON.parse(JSON.stringify(sel.selector));\r\n if (!nsel.pitches) {\r\n nsel.pitches = [];\r\n }\r\n selectors.push(nsel);\r\n });\r\n return { ticks, selectors };\r\n }\r\n deleteMeasure(selection: SmoSelection) {\r\n const selCopy = this._copySelectionsByMeasure(selection.selector.staff, selection.selector.measure)\r\n .selectors;\r\n this.clearMeasureMap(selection.measure);\r\n if (selCopy.length) {\r\n selCopy.forEach((selector) => {\r\n const nsel = JSON.parse(JSON.stringify(selector));\r\n if (selector.measure === 0) {\r\n nsel.measure += 1;\r\n } else {\r\n nsel.measure -= 1;\r\n }\r\n this.selections.push(this._getClosestTick(nsel));\r\n });\r\n }\r\n }\r\n _updateNoteModifier(selection: SmoSelection, modMap: Record, modifier: SmoNoteModifierBase, ix: number) {\r\n if (!modMap[modifier.attrs.id] && modifier.logicalBox) {\r\n this.renderer.pageMap.addModifierTab(\r\n {\r\n modifier,\r\n selection,\r\n box: modifier.logicalBox,\r\n index: ix\r\n }\r\n );\r\n ix += 1;\r\n const context = this.renderer.pageMap.getRendererFromModifier(modifier);\r\n modMap[modifier.attrs.id] = true;\r\n }\r\n return ix;\r\n }\r\n\r\n _updateModifiers() {\r\n let ix = 0;\r\n const modMap: Record = {};\r\n if (!this.renderer.score) {\r\n return;\r\n }\r\n this.renderer.score.textGroups.forEach((modifier) => {\r\n if (!modMap[modifier.attrs.id] && modifier.logicalBox) {\r\n this.renderer.pageMap.addModifierTab({\r\n modifier,\r\n selection: null,\r\n box: modifier.logicalBox,\r\n index: ix\r\n });\r\n ix += 1;\r\n }\r\n });\r\n const keys = Object.keys(this.measureNoteMap); \r\n keys.forEach((selKey) => {\r\n const selection = this.measureNoteMap[selKey];\r\n selection.staff.renderableModifiers.forEach((modifier) => {\r\n if (SmoSelector.contains(selection.selector, modifier.startSelector, modifier.endSelector)) {\r\n if (!modMap[modifier.attrs.id]) {\r\n if (modifier.logicalBox) {\r\n this.renderer.pageMap.addModifierTab({\r\n modifier,\r\n selection,\r\n box: modifier.logicalBox,\r\n index: ix\r\n });\r\n ix += 1;\r\n modMap[modifier.attrs.id] = true;\r\n }\r\n }\r\n }\r\n });\r\n selection.measure.modifiers.forEach((modifier) => {\r\n if (modifier.attrs.id\r\n && !modMap[modifier.attrs.id]\r\n && modifier.logicalBox) {\r\n this.renderer.pageMap.addModifierTab({\r\n modifier,\r\n selection,\r\n box: SvgHelpers.smoBox(modifier.logicalBox),\r\n index: ix\r\n });\r\n ix += 1;\r\n modMap[modifier.attrs.id] = true;\r\n }\r\n });\r\n selection.note?.textModifiers.forEach((modifier) => {\r\n if (modifier.logicalBox) {\r\n ix = this._updateNoteModifier(selection, modMap, modifier, ix);\r\n }\r\n });\r\n\r\n selection.note?.graceNotes.forEach((modifier) => {\r\n ix = this._updateNoteModifier(selection, modMap, modifier, ix);\r\n });\r\n });\r\n }\r\n // ### _getClosestTick\r\n // given a musical selector, find the note artifact that is closest to it,\r\n // if an exact match is not available\r\n _getClosestTick(selector: SmoSelector): SmoSelection {\r\n let tickKey: string | undefined = '';\r\n const measureKey = Object.keys(this.measureNoteMap).find((k) =>\r\n SmoSelector.sameMeasure(this.measureNoteMap[k].selector, selector)\r\n && this.measureNoteMap[k].selector.tick === 0);\r\n tickKey = Object.keys(this.measureNoteMap).find((k) =>\r\n SmoSelector.sameNote(this.measureNoteMap[k].selector, selector));\r\n const firstObj = this.measureNoteMap[Object.keys(this.measureNoteMap)[0]];\r\n\r\n if (tickKey) {\r\n return this.measureNoteMap[tickKey];\r\n }\r\n if (measureKey) {\r\n return this.measureNoteMap[measureKey];\r\n }\r\n return firstObj;\r\n }\r\n\r\n // ### _setModifierBoxes\r\n // Create the DOM modifiers for the lyrics and other modifiers\r\n _setModifierBoxes(measure: SmoMeasure) {\r\n const context = this.renderer.pageMap.getRenderer(measure.svg.logicalBox);\r\n measure.voices.forEach((voice: SmoVoice) => {\r\n voice.notes.forEach((smoNote: SmoNote) => {\r\n if (context) {\r\n const el = context.svg.getElementById(smoNote.renderId as string);\r\n if (el) {\r\n SvgHelpers.updateArtifactBox(context, (el as any), smoNote);\r\n // TODO: fix this, only works on the first line.\r\n smoNote.getModifiers('SmoLyric').forEach((lyrict: SmoNoteModifierBase) => {\r\n const lyric: SmoLyric = lyrict as SmoLyric;\r\n if (lyric.getText().length || lyric.isHyphenated()) {\r\n const lyricElement = context.svg.getElementById('vf-' + lyric.attrs.id) as SVGSVGElement;\r\n if (lyricElement) {\r\n SvgHelpers.updateArtifactBox(context, lyricElement, lyric as any);\r\n }\r\n }\r\n });\r\n }\r\n smoNote.graceNotes.forEach((g) => {\r\n if (g.element) {\r\n }\r\n var gel = context.svg.getElementById('vf-' + g.renderId) as SVGSVGElement;\r\n SvgHelpers.updateArtifactBox(context, gel, g);\r\n });\r\n smoNote.textModifiers.forEach((modifier) => {\r\n if (modifier.logicalBox && modifier.element) {\r\n SvgHelpers.updateArtifactBox(context, modifier.element, modifier as any);\r\n }\r\n });\r\n }\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * returns true of the selections are adjacent\r\n * @param s1 a selections\r\n * @param s2 another election\r\n * @returns \r\n */\r\n isAdjacentSelection(s1: SmoSelection, s2: SmoSelection) {\r\n if (!this.renderer.score) {\r\n return false;\r\n }\r\n const nextSel = SmoSelection.advanceTicks(this.renderer.score, s1, 1);\r\n if (!nextSel) {\r\n return false;\r\n }\r\n return SmoSelector.eq(nextSel.selector, s2.selector);\r\n }\r\n areSelectionsAdjacent() {\r\n let selectionIx = 0;\r\n for (selectionIx = 0; this.selections.length > 1 && selectionIx < this.selections.length - 1; ++selectionIx) {\r\n if (!this.isAdjacentSelection(this.selections[selectionIx], this.selections[selectionIx + 1])) {\r\n return false;\r\n }\r\n }\r\n return true; \r\n }\r\n // ### updateMeasure\r\n // A measure has changed. Update the music geometry for it\r\n mapMeasure(staff: SmoSystemStaff, measure: SmoMeasure, printing: boolean) {\r\n let voiceIx = 0;\r\n let selectedTicks = 0;\r\n\r\n // We try to restore block selections. If all the selections in this block are not adjacent, only restore individual selections\r\n // if possible\r\n let adjacentSels = this.areSelectionsAdjacent();\r\n const lastResortSelection: SmoSelection[] = [];\r\n let selectionChanged = false;\r\n let vix = 0;\r\n let replacedSelectors = 0;\r\n if (!measure.svg.logicalBox) {\r\n return;\r\n }\r\n this._setModifierBoxes(measure);\r\n const timestamp = new Date().valueOf();\r\n // Keep track of any current selections in this measure, we will try to restore them.\r\n const sels = this._copySelectionsByMeasure(staff.staffId, measure.measureNumber.measureIndex);\r\n this.clearMeasureMap(measure);\r\n if (sels.selectors.length) {\r\n vix = sels.selectors[0].voice;\r\n }\r\n sels.selectors.forEach((sel) => {\r\n sel.voice = vix;\r\n });\r\n\r\n measure.voices.forEach((voice) => {\r\n let tick = 0;\r\n voice.notes.forEach((note) => {\r\n const selector = {\r\n staff: staff.staffId,\r\n measure: measure.measureNumber.measureIndex,\r\n voice: voiceIx,\r\n tick,\r\n pitches: []\r\n };\r\n if (typeof(note.logicalBox) === 'undefined') {\r\n console.warn('note has no box');\r\n }\r\n // create a selection for the newly rendered note\r\n const selection = new SmoSelection({\r\n selector,\r\n _staff: staff,\r\n _measure: measure,\r\n _note: note,\r\n _pitches: [],\r\n box: SvgHelpers.smoBox(SvgHelpers.smoBox(note.logicalBox)),\r\n type: 'rendered'\r\n });\r\n // and add it to the map\r\n this._updateMeasureNoteMap(selection, printing);\r\n\r\n // If this note is the same location as something that was selected, reselect it\r\n if (replacedSelectors < sels.selectors.length && selection.selector.tick === sels.selectors[replacedSelectors].tick &&\r\n selection.selector.voice === vix) {\r\n this.selections.push(selection);\r\n // Reselect any pitches.\r\n if (sels.selectors[replacedSelectors].pitches.length > 0) {\r\n sels.selectors[replacedSelectors].pitches.forEach((pitchIx) => {\r\n if (selection.note && selection.note.pitches.length > pitchIx) {\r\n selection.selector.pitches.push(pitchIx);\r\n }\r\n });\r\n }\r\n const note = selection.note as SmoNote;\r\n selectedTicks += note.tickCount;\r\n replacedSelectors += 1;\r\n selectionChanged = true;\r\n } else if (adjacentSels && selectedTicks > 0 && selectedTicks < sels.ticks && selection.selector.voice === vix) {\r\n // try to select the same length of music as was previously selected. So a 1/4 to 2 1/8, both\r\n // are selected\r\n replacedSelectors += 1;\r\n this.selections.push(selection);\r\n selectedTicks += note.tickCount;\r\n } else if (this.selections.length === 0 && sels.selectors.length === 0 && lastResortSelection.length === 0) {\r\n lastResortSelection.push(selection);\r\n }\r\n tick += 1;\r\n });\r\n voiceIx += 1;\r\n });\r\n // We deleted all the notes that were selected, select something else\r\n if (this.selections.length === 0) {\r\n selectionChanged = true;\r\n this.selections = lastResortSelection;\r\n }\r\n // If there were selections on this measure, highlight them.\r\n if (selectionChanged) {\r\n this.deferHighlight();\r\n }\r\n layoutDebug.setTimestamp(layoutDebug.codeRegions.MAP, new Date().valueOf() - timestamp);\r\n }\r\n\r\n _getTicksFromSelections(): number {\r\n let rv = 0;\r\n this.selections.forEach((sel) => {\r\n if (sel.note) {\r\n rv += sel.note.tickCount;\r\n }\r\n });\r\n return rv;\r\n }\r\n _copySelections(): SmoSelector[] {\r\n const rv: SmoSelector[] = [];\r\n this.selections.forEach((sel) => {\r\n rv.push(sel.selector);\r\n });\r\n return rv;\r\n }\r\n // ### getExtremeSelection\r\n // Get the rightmost (1) or leftmost (-1) selection\r\n getExtremeSelection(sign: number): SmoSelection {\r\n let i = 0;\r\n let rv = this.selections[0];\r\n for (i = 1; i < this.selections.length; ++i) {\r\n const sa = this.selections[i].selector;\r\n if (sa.measure * sign > rv.selector.measure * sign) {\r\n rv = this.selections[i];\r\n } else if (sa.measure === rv.selector.measure && sa.tick * sign > rv.selector.tick * sign) {\r\n rv = this.selections[i];\r\n }\r\n }\r\n return rv;\r\n }\r\n _selectClosest(selector: SmoSelector) {\r\n var artifact = this._getClosestTick(selector);\r\n if (!artifact) {\r\n return;\r\n }\r\n if (this.selections.find((sel) => JSON.stringify(sel.selector)\r\n === JSON.stringify(artifact.selector))) {\r\n return;\r\n }\r\n const note = artifact.note as SmoNote;\r\n if (selector.pitches && selector.pitches.length && selector.pitches.length <= note.pitches.length) {\r\n // If the old selection had only a single pitch, try to recreate that.\r\n artifact.selector.pitches = JSON.parse(JSON.stringify(selector.pitches));\r\n }\r\n this.selections.push(artifact);\r\n }\r\n // ### updateMap\r\n // This should be called after rendering the score. It updates the score to\r\n // graphics map and selects the first object.\r\n updateMap() {\r\n const ts = new Date().valueOf();\r\n this.mapping = true;\r\n let tickSelected = 0;\r\n const selCopy = this._copySelections();\r\n const ticksSelectedCopy = this._getTicksFromSelections();\r\n const firstSelection = this.getExtremeSelection(-1);\r\n this._updateModifiers();\r\n\r\n // Try to restore selection. If there were none, just select the fist\r\n // thing in the score\r\n const firstKey = SmoSelector.getNoteKey(SmoSelector.default);\r\n if (!selCopy.length && this.renderer.score) {\r\n // If there is nothing rendered, don't update tracker\r\n if (typeof(this.measureNoteMap[firstKey]) !== 'undefined' && !firstSelection)\r\n this.selections = [this.measureNoteMap[firstKey]];\r\n } else if (this.areSelectionsAdjacent() && this.selections.length > 1) {\r\n // If there are adjacent selections, restore selections to the ticks that are in the score now\r\n if (!firstSelection) {\r\n layoutDebug.setTimestamp(layoutDebug.codeRegions.UPDATE_MAP, new Date().valueOf() - ts);\r\n return;\r\n }\r\n this.selections = [];\r\n this._selectClosest(firstSelection.selector);\r\n const first = this.selections[0];\r\n tickSelected = (first.note as SmoNote).tickCount ?? 0;\r\n while (tickSelected < ticksSelectedCopy && first) {\r\n let delta: number = this._growSelectionRight(true);\r\n if (!delta) {\r\n break;\r\n }\r\n tickSelected += delta;\r\n }\r\n }\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n this.mapping = false;\r\n layoutDebug.setTimestamp(layoutDebug.codeRegions.UPDATE_MAP, new Date().valueOf() - ts);\r\n }\r\n createMousePositionBox(logicalBox: SvgBox) {\r\n const pageMap = this.renderer.pageMap;\r\n const page = pageMap.getRendererFromPoint(logicalBox);\r\n if (page) {\r\n const cof = (pageMap.zoomScale * pageMap.renderScale); \r\n const debugBox = SvgHelpers.smoBox(logicalBox);\r\n debugBox.y -= (page.box.y + 5 / cof);\r\n debugBox.x -= (page.box.x + 5 / cof)\r\n debugBox.width = 10 / cof;\r\n debugBox.height = 10 / cof;\r\n if (!this.mouseHintBox) {\r\n this.mouseHintBox = {\r\n stroke: SvgPageMap.strokes['debug-mouse-box'],\r\n classes: 'hide-print',\r\n box: debugBox,\r\n scroll: { x: 0, y: 0 },\r\n context: page,\r\n timeOff: 1000\r\n };\r\n }\r\n this.mouseHintBox.context = page;\r\n this.mouseHintBox.box = debugBox;\r\n SvgHelpers.outlineRect(this.mouseHintBox);\r\n } \r\n }\r\n eraseMousePositionBox() {\r\n if (this.mouseHintBox && this.mouseHintBox.element) {\r\n this.mouseHintBox.element.remove();\r\n this.mouseHintBox.element = undefined;\r\n }\r\n }\r\n /**\r\n * Find any musical elements at the supplied screen coordinates and set them as the selection\r\n * @param bb \r\n * @returns \r\n */\r\n intersectingArtifact(bb: SvgBox) {\r\n const scrollState = this.scroller.scrollState;\r\n bb = SvgHelpers.boxPoints(bb.x + scrollState.x, bb.y + scrollState.y, bb.width ? bb.width : 1, bb.height ? bb.height : 1);\r\n const logicalBox = this.renderer.pageMap.clientToSvg(bb);\r\n const { selections, page } = this.renderer.pageMap.findArtifact(logicalBox);\r\n if (page) {\r\n const artifacts = selections;\r\n // const artifacts = SvgHelpers.findIntersectingArtifactFromMap(bb, this.measureNoteMap, SvgHelpers.smoBox(this.scroller.scrollState.scroll));\r\n // TODO: handle overlapping suggestions\r\n if (!artifacts.length) {\r\n const sel = this.renderer.pageMap.findModifierTabs(logicalBox);\r\n if (sel.length) {\r\n this._setModifierAsSuggestion(sel[0]);\r\n this.eraseMousePositionBox();\r\n } else {\r\n // no intersection, show mouse hint \r\n this.createMousePositionBox(logicalBox);\r\n }\r\n return;\r\n }\r\n const artifact = artifacts[0];\r\n this.eraseMousePositionBox();\r\n this._setArtifactAsSuggestion(artifact);\r\n }\r\n }\r\n _getRectangleChain(selection: SmoSelection) {\r\n const rv: number[] = [];\r\n if (!selection.note) {\r\n return rv;\r\n }\r\n rv.push(selection.measure.svg.pageIndex);\r\n rv.push(selection.measure.svg.lineIndex);\r\n rv.push(selection.measure.measureNumber.measureIndex);\r\n return rv;\r\n }\r\n _updateMeasureNoteMap(artifact: SmoSelection, printing: boolean) {\r\n const note = artifact.note as SmoNote;\r\n const noteKey = SmoSelector.getNoteKey(artifact.selector);\r\n const activeVoice = artifact.measure.getActiveVoice();\r\n // not has not been drawn yet.\r\n if ((!artifact.box) || (!artifact.measure.svg.logicalBox)) {\r\n return;\r\n }\r\n this.measureNoteMap[noteKey] = artifact;\r\n this.renderer.pageMap.addArtifact(artifact);\r\n artifact.scrollBox = { x: artifact.box.x,\r\n y: artifact.measure.svg.logicalBox.y };\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SvgHelpers, SvgBuilder } from \"./svgHelpers\";\r\nimport { buildDom } from \"../../common/htmlHelpers\";\r\nimport { SuiScoreViewOperations } from \"./scoreViewOperations\";\r\nimport { SvgBox, Pitch, PitchLetter } from '../../smo/data/common';\r\ndeclare var $: any;\r\n\r\nexport interface PianoKey {\r\n box: SvgBox,\r\n keyElement: SVGSVGElement\r\n}\r\nexport class SuiPiano {\r\n renderElement: SVGSVGElement | null;\r\n view: SuiScoreViewOperations;\r\n octaveOffset: number = 0;\r\n chordPedal: boolean = false;\r\n objects: PianoKey[] = [];\r\n suggestFadeTimer: NodeJS.Timer | null = null;\r\n elementId: string = 'piano-svg';\r\n constructor(view: SuiScoreViewOperations) {\r\n this.renderElement = (document.getElementById(this.elementId) as any) as SVGSVGElement;\r\n this.view = view;\r\n this.render();\r\n }\r\n\r\n static get dimensions() {\r\n return {\r\n wwidth: 23,\r\n bwidth: 13,\r\n wheight: 120,\r\n bheight: 80,\r\n octaves: 1\r\n };\r\n }\r\n // 7 white keys per octave\r\n static get wkeysPerOctave() {\r\n return 7;\r\n }\r\n static get owidth() {\r\n return SuiPiano.dimensions.wwidth * SuiPiano.wkeysPerOctave;\r\n }\r\n static createAndDisplay() {\r\n // Called by ribbon button.\r\n $('body').trigger('show-piano-event');\r\n $('body').trigger('forceScrollEvent');\r\n }\r\n\r\n _mapKeys() {\r\n this.objects = [];\r\n var keys: SVGSVGElement[] = [].slice.call(this.renderElement!.getElementsByClassName('piano-key'));\r\n keys.forEach((key) => {\r\n var rect = SvgHelpers.smoBox(key.getBoundingClientRect());\r\n var id = key.getAttributeNS('', 'id');\r\n var artifact = {\r\n keyElement: key,\r\n box: rect,\r\n id: id\r\n };\r\n this.objects.push(artifact);\r\n });\r\n }\r\n _removeClass(classes: string) {\r\n Array.from(this.renderElement!.getElementsByClassName('piano-key')).forEach((el) => {\r\n $(el).removeClass(classes);\r\n });\r\n }\r\n _removeGlow() {\r\n this._removeClass('glow-key');\r\n }\r\n _fadeGlow(el: SVGSVGElement) {\r\n if (this.suggestFadeTimer) {\r\n clearTimeout(this.suggestFadeTimer as any);\r\n }\r\n // Make selection fade if there is a selection.\r\n this.suggestFadeTimer = setTimeout(() => {\r\n $(el).removeClass('glow-key');\r\n }, 1000);\r\n }\r\n bind() {\r\n // The menu option to toggle piano state\r\n $('body').off('show-piano-event').on('show-piano-event', () => {\r\n const isVisible = $('body').hasClass('show-piano');\r\n $('body').toggleClass('show-piano');\r\n this._mapKeys();\r\n });\r\n $('#piano-8va-button').off('click').on('click', (ev: any) => {\r\n $('#piano-8vb-button').removeClass('activated');\r\n if (this.octaveOffset === 0) {\r\n $(ev.currentTarget).addClass('activated');\r\n this.octaveOffset = 1;\r\n } else {\r\n $(ev.currentTarget).removeClass('activated');\r\n this.octaveOffset = 0;\r\n }\r\n });\r\n $('#piano-8vb-button').off('click').on('click', (ev: any) => {\r\n $('#piano-8va-button').removeClass('activated');\r\n if (this.octaveOffset === 0) {\r\n $(ev.currentTarget).addClass('activated');\r\n this.octaveOffset = -1;\r\n } else {\r\n $(ev.currentTarget).removeClass('activated');\r\n this.octaveOffset = 0;\r\n }\r\n });\r\n $('#piano-xpose-up').off('click').on('click', () => {\r\n this.view.transposeSelections(1);\r\n });\r\n $('#piano-xpose-down').off('click').on('click', () => {\r\n this.view.transposeSelections(-1);\r\n });\r\n $('#piano-enharmonic').off('click').on('click', () => {\r\n this.view.toggleEnharmonic();\r\n });\r\n $('button.jsLeft').off('click').on('click', () => {\r\n this.view.tracker.moveSelectionLeft();\r\n });\r\n $('button.jsRight').off('click').on('click', () => {\r\n this.view.tracker.moveSelectionRight(this.view.score, null, false);\r\n });\r\n $('button.jsGrowDuration').off('click').on('click', () => {\r\n this.view.batchDurationOperation('doubleDuration');\r\n });\r\n $('button.jsGrowDot').off('click').on('click', () => {\r\n this.view.batchDurationOperation('dotDuration');\r\n });\r\n $('button.jsShrinkDuration').off('click').on('click', () => {\r\n this.view.batchDurationOperation('halveDuration');\r\n });\r\n $('button.jsShrinkDot').off('click').on('click', () => {\r\n this.view.batchDurationOperation('undotDuration');\r\n });\r\n $('button.jsChord').off('click').on('click', (ev: any) => {\r\n $(ev.currentTarget).toggleClass('activated');\r\n this.chordPedal = !this.chordPedal;\r\n });\r\n $(this.renderElement).off('mousemove').on('mousemove', (ev: any) => {\r\n if (Math.abs(this.objects[0].box.x - this.objects[0].keyElement.getBoundingClientRect().x)\r\n > this.objects[0].box.width / 2) {\r\n console.log('remap piano');\r\n this._mapKeys();\r\n }\r\n if (!this.renderElement) {\r\n return;\r\n }\r\n const clientBox = SvgHelpers.smoBox(SvgHelpers.boxPoints(ev.clientX, ev.clientY, 1, 1)); // last param is scroll offset\r\n\r\n var keyPressed = SvgHelpers.findSmallestIntersection(\r\n clientBox, this.objects) as PianoKey;\r\n if (!keyPressed) {\r\n return;\r\n }\r\n const el: SVGSVGElement = this.renderElement!.getElementById(keyPressed.keyElement.id) as SVGSVGElement;\r\n if ($(el).hasClass('glow-key')) {\r\n return;\r\n }\r\n this._removeGlow();\r\n $(el).addClass('glow-key');\r\n this._fadeGlow(el);\r\n });\r\n $(this.renderElement).off('blur').on('blur', () => {\r\n this._removeGlow();\r\n });\r\n $(this.renderElement).off('click').on('click', (ev: any) => {\r\n this._updateSelections(ev);\r\n });\r\n // the close button on piano itself\r\n $('.close-piano').off('click').on('click', () => {\r\n this.view.score.preferences.showPiano = false;\r\n this.view.updateScorePreferences(this.view.score.preferences);\r\n });\r\n }\r\n static hidePiano() {\r\n if ($('body').hasClass('show-piano')) {\r\n $('body').removeClass('show-piano');\r\n }\r\n }\r\n static showPiano() {\r\n if ($('body').hasClass('show-piano') === false) {\r\n $('body').addClass('show-piano');\r\n // resize the work area.\r\n // $('body').trigger('forceResizeEvent');\r\n }\r\n }\r\n static get isShowing() {\r\n return $('body').hasClass('show-piano');\r\n }\r\n _updateSelections(ev: any) {\r\n // fake a scroller (piano scroller w/b cool tho...)\r\n if (!this.renderElement) {\r\n return;\r\n }\r\n const logicalBox = SvgHelpers.smoBox({ x: ev.clientX, y: ev.clientY });\r\n\r\n var keyPressed =\r\n SvgHelpers.findSmallestIntersection(logicalBox, this.objects) as PianoKey;\r\n if (!keyPressed) {\r\n return;\r\n }\r\n if (!ev.shiftKey && !this.chordPedal) {\r\n this._removeClass('glow-key pressed-key');\r\n } else {\r\n var el = this.renderElement!.getElementById(keyPressed.keyElement.id) as SVGSVGElement;\r\n $(el).addClass('pressed-key');\r\n }\r\n const key = keyPressed.keyElement.id.substr(6, keyPressed.keyElement.id.length - 6);\r\n const pitch: Pitch = {\r\n letter: key[0].toLowerCase() as PitchLetter,\r\n octave: this.octaveOffset,\r\n accidental: key.length > 1 ? key[1] : 'n'\r\n };\r\n\r\n this.view.setPitchPiano(pitch, this.chordPedal);\r\n }\r\n _renderControls() {\r\n var b = buildDom;\r\n var r = b('button').classes('icon icon-cross close close-piano');\r\n $('.piano-container .key-right-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl jsGrowDuration').append(b('span').classes('icon icon-duration_grow'));\r\n $('.piano-container .key-right-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl jsShrinkDuration').append(b('span').classes('icon icon-duration_less'));\r\n $('.piano-container .key-right-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl jsGrowDot').append(b('span').classes('icon icon-duration_grow_dot'));\r\n $('.piano-container .key-right-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl jsShrinkDot').append(b('span').classes('icon icon-duration_less_dot'));\r\n $('.piano-container .key-right-ctrl').append(r.dom());\r\n\r\n r = b('button').classes('key-ctrl jsLeft').append(b('span').classes('icon icon-arrow-left'));\r\n $('.piano-container .piano-keys').prepend(r.dom());\r\n r = b('button').classes('key-ctrl jsRight').append(b('span').classes('icon icon-arrow-right'));\r\n $('.piano-container .piano-keys').append(r.dom());\r\n\r\n r = b('button').classes('piano-ctrl').attr('id', 'piano-8va-button').append(\r\n b('span').classes('bold-italic').text('8')).append(\r\n b('sup').classes('italic').text('va'));\r\n $('.piano-container .key-left-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl ').attr('id', 'piano-8vb-button').append(\r\n b('span').classes('bold-italic').text('8')).append(\r\n b('sup').classes('italic').text('vb'));\r\n $('.piano-container .key-left-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl jsXposeUp').attr('id', 'piano-xpose-up').append(\r\n b('span').classes('bold').text('+'));\r\n $('.piano-container .key-left-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl jsXposeDown').attr('id', 'piano-xpose-down').append(\r\n b('span').classes('bold').text('-'));\r\n $('.piano-container .key-left-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl jsEnharmonic').attr('id', 'piano-enharmonic').append(\r\n b('span').classes('bold icon icon-accident'));\r\n\r\n $('.piano-container .key-left-ctrl').append(r.dom());\r\n r = b('button').classes('piano-ctrl jsChord')\r\n .append(b('span').classes('icon icon-chords'));\r\n $('.piano-container .key-left-ctrl').append(r.dom());\r\n }\r\n handleResize() {\r\n this._mapKeys();\r\n }\r\n playNote() {\r\n }\r\n render() {\r\n $('body').addClass('show-piano');\r\n var b = SvgBuilder.b;\r\n var d = SuiPiano.dimensions;\r\n // https://www.mathpages.com/home/kmath043.htm\r\n\r\n // Width of white key at back for C,D,E\r\n var b1off = d.wwidth - (d.bwidth * 2 / 3);\r\n\r\n // Width of other white keys at the back.\r\n var b2off = d.wwidth - (d.bwidth * 3) / 4;\r\n\r\n var xwhite = [{\r\n note: 'C',\r\n x: 0\r\n }, {\r\n note: 'D',\r\n x: d.wwidth\r\n }, {\r\n note: 'E',\r\n x: 2 * d.wwidth\r\n }, {\r\n note: 'F',\r\n x: 3 * d.wwidth\r\n }, {\r\n note: 'G',\r\n x: 4 * d.wwidth\r\n }, {\r\n note: 'A',\r\n x: 5 * d.wwidth\r\n }, {\r\n note: 'B',\r\n x: 6 * d.wwidth\r\n }\r\n ];\r\n var xblack = [{\r\n note: 'Db',\r\n x: b1off\r\n }, {\r\n note: 'Eb',\r\n x: 2 * b1off + d.bwidth\r\n }, {\r\n note: 'Gb',\r\n x: 3 * d.wwidth + b2off\r\n }, {\r\n note: 'Ab',\r\n x: (3 * d.wwidth + b2off) + b2off + d.bwidth\r\n }, {\r\n note: 'Bb',\r\n x: SuiPiano.owidth - (b2off + d.bwidth)\r\n }\r\n ];\r\n var wwidth = d.wwidth;\r\n var bwidth = d.bwidth;\r\n var wheight = d.wheight;\r\n var bheight = d.bheight;\r\n var owidth = SuiPiano.wkeysPerOctave * wwidth;\r\n\r\n // Start on C2 to C6 to reduce space\r\n var octaveOff = 7 - d.octaves;\r\n\r\n var x = 0;\r\n var y = 0;\r\n var r = b('g');\r\n for (var i = 0; i < d.octaves; ++i) {\r\n x = i * owidth;\r\n xwhite.forEach((key) => {\r\n var nt = key.note;\r\n var classes = 'piano-key white-key';\r\n if (nt == 'C4') {\r\n classes += ' middle-c';\r\n }\r\n var rect = b('rect').attr('id', 'keyId-' + nt).rect(x + key.x, y, wwidth, wheight, classes);\r\n r.append(rect);\r\n\r\n var tt = b('text').text(x + key.x + (wwidth / 5), bheight + 16, 'note-text', nt);\r\n r.append(tt);\r\n });\r\n xblack.forEach((key) => {\r\n var nt = key.note;\r\n var classes = 'piano-key black-key';\r\n var rect = b('rect').attr('id', 'keyId-' + nt).attr('fill', 'url(#piano-grad)').rect(x + key.x, 0, bwidth, bheight, classes);\r\n r.append(rect);\r\n });\r\n }\r\n var el = (document.getElementById(this.elementId) as any) as SVGSVGElement;\r\n SvgHelpers.gradient(el, 'piano-grad', 'vertical', [{ color: '#000', offset: '0%', opacity: 1 },\r\n { color: '#777', offset: '50%', opacity: 1 }, { color: '#ddd', offset: '100%', opacity: 1 }]);\r\n el.appendChild(r.dom());\r\n this._renderControls();\r\n this._mapKeys();\r\n this.bind();\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\n/**\r\n * Classes to support the state machine associated with background music rendering.\r\n * @module renderState\r\n */\r\nimport { SmoMeasure } from '../../smo/data/measure';\r\nimport { UndoBuffer, UndoEntry } from '../../smo/xform/undo';\r\nimport { SmoRenderConfiguration } from './configuration';\r\nimport { PromiseHelpers } from '../../common/promiseHelpers';\r\nimport { SmoSelection } from '../../smo/xform/selections';\r\nimport { VxSystem } from '../vex/vxSystem';\r\nimport { SmoScore } from '../../smo/data/score';\r\nimport { SmoTextGroup } from '../../smo/data/scoreText';\r\nimport { SuiMapper } from './mapper';\r\nimport { SmoSystemStaff } from '../../smo/data/systemStaff';\r\nimport { SuiScoreRender, ScoreRenderParams } from './scoreRender';\r\nimport { SuiExceptionHandler } from '../../ui/exceptions';\r\nimport { VexFlow, setFontStack } from '../../common/vex';\r\ndeclare var $: any;\r\n\r\n\r\nexport var scoreChangeEvent = 'smoScoreChangeEvent';\r\n/**\r\n * Manage the state of the score rendering. The score can be rendered either completely,\r\n * or partially for editing. This class works with the RenderDemon to decide when to\r\n * render the score after it has been modified, and keeps track of what the current\r\n * render state is (dirty, etc.)\r\n * @category SuiRender\r\n * */\r\nexport class SuiRenderState {\r\n static debugMask: number = 0;\r\n dirty: boolean;\r\n replaceQ: SmoSelection[];\r\n stateRepCount: 0;\r\n viewportChanged: boolean;\r\n _resetViewport: boolean;\r\n measureMapper: SuiMapper | null;\r\n passState: number = SuiRenderState.passStates.initial;\r\n _score: SmoScore | null = null;\r\n _backupZoomScale: number = 0;\r\n renderer: SuiScoreRender;\r\n idleRedrawTime: number;\r\n idleLayoutTimer: number = 0; // how long the score has been idle\r\n demonPollTime: number;\r\n handlingRedraw: boolean = false;\r\n // signal to render demon that we have suspended background\r\n // rendering because we are recording or playing actions.\r\n suspendRendering: boolean = false;\r\n undoBuffer: UndoBuffer;\r\n undoStatus: number = 0;\r\n\r\n constructor(config: ScoreRenderParams) {\r\n this.dirty = true;\r\n this.replaceQ = [];\r\n this.stateRepCount = 0;\r\n this.setPassState(SuiRenderState.passStates.initial, 'ctor');\r\n this.viewportChanged = false;\r\n this._resetViewport = false;\r\n this.measureMapper = null;\r\n this.renderer = new SuiScoreRender(config);\r\n this.idleRedrawTime = config.config.idleRedrawTime;\r\n this.demonPollTime = config.config.demonPollTime;\r\n this.undoBuffer = config.undoBuffer;\r\n }\r\n get elementId() {\r\n return this.renderer.elementId;\r\n }\r\n get pageMap() {\r\n return this.renderer.vexContainers;\r\n }\r\n // ### setMeasureMapper\r\n // DI/notifier pattern. The measure mapper/tracker is updated when the score is rendered\r\n // so the UI stays in sync with the location of elements in the score.\r\n setMeasureMapper(mapper: SuiMapper) {\r\n this.measureMapper = mapper;\r\n this.renderer.measureMapper = mapper;\r\n }\r\n set stepMode(value: boolean) {\r\n this.suspendRendering = value;\r\n this.renderer.autoAdjustRenderTime = !value;\r\n if (this.measureMapper) {\r\n this.measureMapper.deferHighlightMode = !value;\r\n }\r\n }\r\n\r\n // ### createScoreRenderer\r\n // ### Description;\r\n // to get the score to appear, a div and a score object are required. The layout takes care of creating the\r\n // svg element in the dom and interacting with the vex library.\r\n static createScoreRenderer(config: SmoRenderConfiguration, renderElement: Element, score: SmoScore, undoBuffer: UndoBuffer): SuiRenderState {\r\n const ctorObj: ScoreRenderParams = {\r\n config,\r\n elementId: renderElement,\r\n score,\r\n undoBuffer\r\n };\r\n const renderer = new SuiRenderState(ctorObj);\r\n return renderer;\r\n }\r\n static get passStates(): Record {\r\n return { initial: 0, clean: 2, replace: 3 };\r\n }\r\n get renderElement(): Element {\r\n return this.elementId;\r\n }\r\n notifyFontChange() {\r\n setFontStack(this.score!.engravingFont);\r\n }\r\n addToReplaceQueue(selection: SmoSelection | SmoSelection[]) {\r\n if (this.passState === SuiRenderState.passStates.clean ||\r\n this.passState === SuiRenderState.passStates.replace) { \r\n if (Array.isArray(selection)) {\r\n this.replaceQ = this.replaceQ.concat(selection);\r\n } else {\r\n this.replaceQ.push(selection);\r\n }\r\n this.setDirty();\r\n }\r\n }\r\n\r\n setDirty() {\r\n if (!this.dirty) {\r\n this.dirty = true;\r\n if (this.passState === SuiRenderState.passStates.clean) {\r\n this.setPassState(SuiRenderState.passStates.replace, 'setDirty');\r\n }\r\n }\r\n }\r\n setRefresh() {\r\n this.dirty = true;\r\n this.setPassState(SuiRenderState.passStates.initial, 'setRefresh');\r\n }\r\n rerenderAll() {\r\n this.dirty = true;\r\n this.setPassState(SuiRenderState.passStates.initial, 'rerenderAll');\r\n this._resetViewport = true;\r\n }\r\n clearLine(measure: SmoMeasure) {\r\n const page = measure.svg.pageIndex;\r\n this.renderer.clearRenderedPage(page);\r\n }\r\n get renderStateClean() {\r\n return this.passState === SuiRenderState.passStates.clean && this.renderer.backgroundRender === false;\r\n }\r\n get renderStateRendered() {\r\n return (this.passState === SuiRenderState.passStates.clean && this.renderer.backgroundRender === false) ||\r\n (this.passState === SuiRenderState.passStates.replace && this.replaceQ.length === 0 && this.renderer.backgroundRender === false);\r\n }\r\n /**\r\n * Do a quick re-render of a measure that has changed, defer the whole score.\r\n * @returns \r\n */\r\n replaceMeasures() {\r\n const staffMap: Record = {};\r\n if (this.score === null || this.measureMapper === null) {\r\n return;\r\n }\r\n this.replaceQ.forEach((change) => {\r\n this.renderer.replaceSelection(staffMap, change);\r\n });\r\n Object.keys(staffMap).forEach((key) => {\r\n const obj = staffMap[key];\r\n this.renderer.renderModifiers(obj.staff, obj.system);\r\n obj.system.renderEndings(this.measureMapper!.scroller);\r\n obj.system.updateLyricOffsets();\r\n });\r\n this.replaceQ = [];\r\n }\r\n async preserveScroll() {\r\n const scrollState = this.measureMapper!.scroller.scrollState;\r\n await this.renderPromise();\r\n this.measureMapper!.scroller.restoreScrollState(scrollState);\r\n }\r\n\r\n _renderStatePromise(condition: () => boolean): Promise {\r\n const oldSuspend = this.suspendRendering;\r\n this.suspendRendering = false;\r\n const self = this;\r\n const endAction = () => {\r\n self.suspendRendering = oldSuspend;\r\n };\r\n return PromiseHelpers.makePromise(condition, endAction, null, this.demonPollTime);\r\n }\r\n // ### renderPromise\r\n // return a promise that resolves when the score is in a fully rendered state.\r\n renderPromise(): Promise {\r\n return this._renderStatePromise(() => this.renderStateClean);\r\n }\r\n\r\n // ### renderPromise\r\n // return a promise that resolves when the score is in a fully rendered state.\r\n updatePromise() {\r\n this.replaceMeasures();\r\n return this._renderStatePromise(() => this.renderStateRendered);\r\n }\r\n async handleRedrawTimer() {\r\n if (this.handlingRedraw) {\r\n return;\r\n }\r\n if (this.suspendRendering) {\r\n return;\r\n }\r\n this.handlingRedraw = true;\r\n const redrawTime = Math.max(this.renderer.renderTime, this.idleRedrawTime);\r\n // If there has been a change, redraw the score\r\n if (this.passState === SuiRenderState.passStates.initial) {\r\n this.dirty = true;\r\n this.undoStatus = this.undoBuffer.opCount;\r\n this.idleLayoutTimer = Date.now();\r\n\r\n // indicate the display is 'dirty' and we will be refreshing it.\r\n $('body').addClass('refresh-1');\r\n try {\r\n // Sort of a hack. If the viewport changed, the scroll state is already reset\r\n // so we can't preserver the scroll state.\r\n if (!this.renderer.viewportChanged) {\r\n this.preserveScroll();\r\n }\r\n await this.render();\r\n } catch (ex) {\r\n console.error(ex);\r\n SuiExceptionHandler.instance.exceptionHandler(ex);\r\n this.handlingRedraw = false;\r\n }\r\n } else if (this.passState === SuiRenderState.passStates.replace && this.undoStatus === this.undoBuffer.opCount) {\r\n // Consider navigation as activity when deciding to refresh\r\n this.idleLayoutTimer = Math.max(this.idleLayoutTimer, this.measureMapper!.getIdleTime());\r\n $('body').addClass('refresh-1');\r\n // Do we need to refresh the score?\r\n if (this.renderer.backgroundRender === false && Date.now() - this.idleLayoutTimer > redrawTime) {\r\n this.passState = SuiRenderState.passStates.initial;\r\n if (!this.renderer.viewportChanged) {\r\n this.preserveScroll();\r\n }\r\n this.render();\r\n }\r\n } else {\r\n this.idleLayoutTimer = Date.now();\r\n this.undoStatus = this.undoBuffer.opCount;\r\n if (this.replaceQ.length > 0) {\r\n this.render();\r\n }\r\n }\r\n this.handlingRedraw = false;\r\n }\r\n pollRedraw() {\r\n setTimeout(async () => {\r\n await this.handleRedrawTimer();\r\n this.pollRedraw();\r\n }, this.demonPollTime);\r\n }\r\n\r\n startDemon() {\r\n this.pollRedraw();\r\n }\r\n renderTextGroup(gg: SmoTextGroup) {\r\n this.renderer.renderTextGroup(gg);\r\n }\r\n /**\r\n * Set the SVG viewport\r\n * @param reset whether to re-render the entire SVG DOM\r\n * @returns \r\n */\r\n setViewport() {\r\n if (!this.score || !this.renderer) {\r\n return;\r\n }\r\n this.renderer.setViewport();\r\n this.score!.staves.forEach((staff) => {\r\n staff.measures.forEach((measure) => {\r\n if (measure.svg.logicalBox) {\r\n measure.svg.history = ['reset'];\r\n }\r\n });\r\n });\r\n }\r\n renderForPrintPromise(): Promise {\r\n $('body').addClass('print-render');\r\n const self = this;\r\n if (!this.score) {\r\n return PromiseHelpers.emptyPromise();\r\n }\r\n const layoutMgr = this.score!.layoutManager!;\r\n const layout = layoutMgr.getGlobalLayout();\r\n this._backupZoomScale = layout.zoomScale;\r\n layout.zoomScale = 1.0;\r\n layoutMgr.updateGlobalLayout(layout);\r\n this.setViewport();\r\n this.setRefresh();\r\n\r\n const promise = new Promise((resolve) => {\r\n const poll = () => {\r\n setTimeout(() => {\r\n if (!self.dirty && !self.renderer.backgroundRender) {\r\n // tracker.highlightSelection();\r\n $('body').removeClass('print-render');\r\n $('.vf-selection').remove();\r\n $('body').addClass('printing');\r\n $('.musicRelief').css('height', '');\r\n resolve();\r\n } else {\r\n poll();\r\n }\r\n }, 500);\r\n };\r\n poll();\r\n });\r\n return promise;\r\n }\r\n\r\n restoreLayoutAfterPrint() {\r\n const layout = this.score!.layoutManager!.getGlobalLayout();\r\n layout.zoomScale = this._backupZoomScale;\r\n this.score!.layoutManager!.updateGlobalLayout(layout);\r\n this.setViewport();\r\n this.setRefresh();\r\n }\r\n\r\n setPassState(st: number, location: string) {\r\n const oldState = this.passState;\r\n let msg = '';\r\n if (oldState !== st) {\r\n this.stateRepCount = 0;\r\n } else {\r\n this.stateRepCount += 1;\r\n }\r\n\r\n msg = location + ': passState ' + this.passState + '=>' + st;\r\n if (this.stateRepCount > 0) {\r\n msg += ' (' + this.stateRepCount + ')';\r\n }\r\n if (SuiRenderState.debugMask) {\r\n console.log(msg);\r\n }\r\n this.passState = st;\r\n }\r\n\r\n get score(): SmoScore | null {\r\n return this._score;\r\n }\r\n\r\n // used for debugging and drawing dots.\r\n dbgDrawDot(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise: boolean) {\r\n const context = this.renderer.getRenderer({ x, y });\r\n if (context) {\r\n context.getContext().beginPath();\r\n context.getContext().arc(x, y, radius, startAngle, endAngle, counterclockwise);\r\n context.getContext().closePath();\r\n context.getContext().fill(); \r\n }\r\n }\r\n set score(score: SmoScore | null) {\r\n if (score === null) {\r\n return;\r\n }\r\n /* if (this._score) {\r\n shouldReset = true;\r\n } */\r\n this.setPassState(SuiRenderState.passStates.initial, 'load score');\r\n const font = score.engravingFont;\r\n this.dirty = true;\r\n this._score = score;\r\n this.renderer.score = score;\r\n this.notifyFontChange();\r\n // if (shouldReset) {\r\n this.setViewport();\r\n if (this.measureMapper) {\r\n this.measureMapper.loadScore();\r\n }\r\n }\r\n\r\n // ### undo\r\n // Undo is handled by the render state machine, because the layout has to first\r\n // delete areas of the viewport that may have changed,\r\n // then create the modified score, then render the 'new' score.\r\n undo(undoBuffer: UndoBuffer, staffMap: Record): SmoScore {\r\n let op = 'setDirty';\r\n const buffer = undoBuffer.peek();\r\n // Unrender the modified music because the IDs may change and normal unrender won't work\r\n if (buffer) {\r\n const sel = buffer.selector;\r\n if (buffer.type === UndoBuffer.bufferTypes.MEASURE) { \r\n if (typeof(staffMap[sel.staff]) === 'number') {\r\n const mSelection = SmoSelection.measureSelection(this.score!, staffMap[sel.staff], sel.measure);\r\n if (mSelection !== null) {\r\n this.renderer.unrenderMeasure(mSelection.measure);\r\n }\r\n }\r\n } else if (buffer.type === UndoBuffer.bufferTypes.STAFF) {\r\n if (typeof(staffMap[sel.staff]) === 'number') {\r\n const sSelection = SmoSelection.measureSelection(this.score!, staffMap[sel.staff], 0);\r\n if (sSelection !== null) {\r\n this.renderer.unrenderStaff(sSelection.staff);\r\n }\r\n }\r\n op = 'setRefresh';\r\n } else {\r\n this.renderer.unrenderAll();\r\n op = 'setRefresh';\r\n }\r\n this._score = undoBuffer.undo(this._score!, staffMap, false);\r\n // Broken encapsulation - we need to know if we are 'undoing' an entire score\r\n // so we can change the score pointed to by the renderer.\r\n if (buffer.type === UndoBuffer.bufferTypes.SCORE) {\r\n this.renderer.score = this._score;\r\n }\r\n (this as any)[op]();\r\n }\r\n if (!this._score) {\r\n throw ('no score when undo');\r\n } \r\n return this._score;\r\n }\r\n\r\n\r\n unrenderColumn(measure: SmoMeasure) {\r\n this.score!.staves.forEach((staff) => {\r\n this.renderer.unrenderMeasure(staff.measures[measure.measureNumber.measureIndex]);\r\n });\r\n }\r\n\r\n // ### forceRender\r\n // For unit test applictions that want to render right-away\r\n forceRender() {\r\n this.setRefresh();\r\n this.render();\r\n }\r\n unrenderMeasure(measure: SmoMeasure) {\r\n this.renderer.unrenderMeasure(measure);\r\n }\r\n\r\n async renderScoreModifiers() {\r\n await this.renderer.renderScoreModifiers();\r\n }\r\n async render(): Promise {\r\n if (this._resetViewport) {\r\n this.setViewport();\r\n this._resetViewport = false;\r\n }\r\n try {\r\n if (SuiRenderState.passStates.replace === this.passState) {\r\n this.replaceMeasures();\r\n } else if (SuiRenderState.passStates.initial === this.passState) {\r\n if (this.renderer.backgroundRender) {\r\n return;\r\n }\r\n this.renderer.layout();\r\n this.renderer.drawPageLines();\r\n this.setPassState(SuiRenderState.passStates.clean, 'rs: complete render');\r\n }\r\n } catch (excp) {\r\n console.warn('exception in render: ' + excp);\r\n }\r\n this.dirty = false;\r\n }\r\n}","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SvgBox, SvgPoint } from '../../smo/data/common';\r\nimport { SmoMeasure, SmoVoice } from '../../smo/data/measure';\r\nimport { SmoScore } from '../../smo/data/score';\r\nimport { SmoTextGroup } from '../../smo/data/scoreText';\r\nimport { SmoSelection } from '../../smo/xform/selections';\r\nimport { SmoSystemStaff } from '../../smo/data/systemStaff';\r\nimport { StaffModifierBase } from '../../smo/data/staffModifiers';\r\nimport { VxMeasure } from '../vex/vxMeasure';\r\nimport { SuiMapper } from './mapper';\r\nimport { VxSystem } from '../vex/vxSystem';\r\nimport { SvgHelpers, StrokeInfo } from './svgHelpers';\r\nimport { SuiPiano } from './piano';\r\nimport { SuiLayoutFormatter, RenderedPage } from './formatter';\r\nimport { SmoBeamer } from '../../smo/xform/beamers';\r\nimport { SuiTextBlock } from './textRender';\r\nimport { layoutDebug } from './layoutDebug';\r\nimport { SourceSansProFont } from '../../styles/font_metrics/ssp-sans-metrics';\r\nimport { SmoRenderConfiguration } from './configuration';\r\nimport { createTopDomContainer } from '../../common/htmlHelpers';\r\nimport { UndoBuffer } from '../../smo/xform/undo';\r\nimport { SvgPageMap, SvgPage } from './svgPageMap';\r\nimport { VexFlow } from '../../common/vex';\r\nimport { Note } from '../../common/vex';\r\n\r\ndeclare var $: any;\r\nconst VF = VexFlow;\r\n/**\r\n * a renderer creates the SVG render context for vexflow from the given element. Then it\r\n * renders the initial score.\r\n * @category SuiRenderParams\r\n */\r\n export interface ScoreRenderParams {\r\n elementId: any,\r\n score: SmoScore,\r\n config: SmoRenderConfiguration,\r\n undoBuffer: UndoBuffer\r\n}\r\n\r\nexport interface MapParameters {\r\n vxSystem: VxSystem, measuresToBox: SmoMeasure[], modifiersToBox: StaffModifierBase[], printing: boolean\r\n}\r\n/**\r\n * This module renders the entire score. It calculates the layout first based on the\r\n * computed dimensions.\r\n * @category SuiRender\r\n**/\r\nexport class SuiScoreRender {\r\n constructor(params: ScoreRenderParams) { \r\n this.elementId = params.elementId;\r\n this.score = params.score;\r\n this.vexContainers = new SvgPageMap(this.score.layoutManager!.globalLayout, this.elementId, this.score.layoutManager!.pageLayouts);\r\n this.setViewport();\r\n }\r\n elementId: any;\r\n startRenderTime: number = 0;\r\n formatter: SuiLayoutFormatter | null = null;\r\n vexContainers: SvgPageMap;\r\n // vexRenderer: any = null;\r\n score: SmoScore | null = null;\r\n measureMapper: SuiMapper | null = null;\r\n measuresToMap: MapParameters[] = [];\r\n viewportChanged: boolean = false;\r\n renderTime: number = 0;\r\n backgroundRender: boolean = false;\r\n static debugMask: number = 0;\r\n renderedPages: Record = {};\r\n _autoAdjustRenderTime: boolean = true;\r\n lyricsToOffset: Map = new Map();\r\n renderingPage: number = -1; \r\n get autoAdjustRenderTime() {\r\n return this._autoAdjustRenderTime;\r\n }\r\n set autoAdjustRenderTime(value: boolean) {\r\n this._autoAdjustRenderTime = value;\r\n }\r\n getRenderer(box: SvgBox | SvgPoint): SvgPage | null {\r\n return this.vexContainers.getRenderer(box);\r\n }\r\n renderTextGroup(gg: SmoTextGroup) {\r\n let ix = 0;\r\n let jj = 0;\r\n if (gg.skipRender || this.score === null || this.measureMapper === null) {\r\n return;\r\n }\r\n gg.elements.forEach((element) => {\r\n element.remove();\r\n });\r\n gg.elements = [];\r\n const layoutManager = this.score!.layoutManager!;\r\n const scaledScoreLayout = layoutManager.getScaledPageLayout(0);\r\n // If this text hasn't been rendered before, estimate the logical box.\r\n const dummyContainer = this.vexContainers.getRendererFromModifier(gg);\r\n if (dummyContainer && !gg.logicalBox) {\r\n const dummyBlock = SuiTextBlock.fromTextGroup(gg, dummyContainer, this.vexContainers, this.measureMapper!.scroller);\r\n gg.logicalBox = dummyBlock.getLogicalBox();\r\n }\r\n\r\n // If this is a per-page score text, get a text group copy for each page.\r\n // else the array contains the original.\r\n const groupAr = SmoTextGroup.getPagedTextGroups(gg, this.score!.layoutManager!.pageLayouts.length, scaledScoreLayout.pageHeight);\r\n groupAr.forEach((newGroup) => {\r\n let container: SvgPage = this.vexContainers.getRendererFromModifier(newGroup);\r\n // If this text is attached to the measure, base the block location on the rendered measure location.\r\n if (newGroup.attachToSelector) {\r\n // If this text is attached to a staff that is not visible, don't draw it.\r\n const mappedStaff = this.score!.staves.find((staff) => staff.staffId === newGroup.selector!.staff);\r\n if (!mappedStaff) {\r\n return;\r\n }\r\n // Indicate the new map;\r\n // newGroup.selector.staff = mappedStaff.staffId; \r\n const mmSel: SmoSelection | null = SmoSelection.measureSelection(this.score!, mappedStaff.staffId, newGroup.selector!.measure);\r\n if (mmSel) {\r\n const mm = mmSel.measure;\r\n if (mm.svg.logicalBox.width > 0) {\r\n const xoff = mm.svg.logicalBox.x + newGroup.musicXOffset;\r\n const yoff = mm.svg.logicalBox.y + newGroup.musicYOffset;\r\n newGroup.textBlocks[0].text.x = xoff;\r\n newGroup.textBlocks[0].text.y = yoff;\r\n }\r\n }\r\n }\r\n if (container) {\r\n const block = SuiTextBlock.fromTextGroup(newGroup, container, this.vexContainers, this.measureMapper!.scroller);\r\n block.render();\r\n if (block.currentBlock?.text.element) {\r\n gg.elements.push(block.currentBlock?.text.element);\r\n }\r\n // For the first one we render, use that as the bounding box for all the text, for\r\n // purposes of mapper/tracker\r\n if (ix === 0) {\r\n gg.logicalBox = JSON.parse(JSON.stringify(block.logicalBox));\r\n // map all the child scoreText objects, too.\r\n for (jj = 0; jj < gg.textBlocks.length; ++jj) {\r\n gg.textBlocks[jj].text.logicalBox = JSON.parse(JSON.stringify(block.inlineBlocks[jj].text.logicalBox));\r\n }\r\n }\r\n ix += 1;\r\n }\r\n });\r\n }\r\n \r\n // ### unrenderAll\r\n // ### Description:\r\n // Delete all the svg elements associated with the score.\r\n unrenderAll() {\r\n if (!this.score) {\r\n return;\r\n }\r\n this.score.staves.forEach((staff) => {\r\n this.unrenderStaff(staff);\r\n });\r\n // $(this.context.svg).find('g.lineBracket').remove();\r\n }\r\n // ### unrenderStaff\r\n // ### Description:\r\n // See unrenderMeasure. Like that, but with a staff.\r\n unrenderStaff(staff: SmoSystemStaff) {\r\n staff.measures.forEach((measure) => {\r\n this.unrenderMeasure(measure);\r\n });\r\n staff.renderableModifiers.forEach((modifier) => {\r\n if (modifier.element) {\r\n modifier.element.remove();\r\n modifier.element = null;\r\n }\r\n });\r\n }\r\n clearRenderedPage(pg: number) {\r\n if (this.renderedPages[pg]) {\r\n this.renderedPages[pg] = null;\r\n }\r\n }\r\n // ### _setViewport\r\n // Create (or recrate) the svg viewport, considering the dimensions of the score.\r\n setViewport() {\r\n if (this.score === null) {\r\n return;\r\n }\r\n const layoutManager = this.score!.layoutManager!;\r\n // All pages have same width/height, so use that\r\n const layout = layoutManager.getGlobalLayout();\r\n this.vexContainers.updateLayout(layout, layoutManager.pageLayouts);\r\n this.renderedPages = {};\r\n this.viewportChanged = true;\r\n if (this.measureMapper) {\r\n this.measureMapper.scroller.scrollAbsolute(0, 0);\r\n }\r\n if (this.measureMapper) {\r\n this.measureMapper.scroller.updateViewport();\r\n }\r\n // this.context.setFont(this.font.typeface, this.font.pointSize, \"\").setBackgroundFillStyle(this.font.fillStyle);\r\n if (SuiScoreRender.debugMask) {\r\n console.log('layout setViewport: pstate initial');\r\n }\r\n }\r\n\r\n async renderScoreModifiers(): Promise {\r\n return new Promise((resolve) => {\r\n // remove existing modifiers, and also remove parent group for 'extra'\r\n // groups associated with pagination (once per page etc)\r\n for (var i = 0; i < this.score!.textGroups.length; ++i) {\r\n const tg = this.score!.textGroups[i];\r\n tg.elements.forEach((element) => {\r\n element.remove();\r\n });\r\n tg.elements = [];\r\n }\r\n // group.classList.add('all-score-text');\r\n for (var i = 0; i < this.score!.textGroups.length; ++i) {\r\n const tg = this.score!.textGroups[i];\r\n this.renderTextGroup(tg);\r\n }\r\n resolve();\r\n });\r\n }\r\n\r\n /**\r\n * for music we've just rendered, get the bounding boxes. We defer this step so we don't force\r\n * a reflow, which can slow rendering.\r\n * @param vxSystem \r\n * @param measures \r\n * @param modifiers \r\n * @param printing \r\n */\r\n measureRenderedElements(vxSystem: VxSystem, measures: SmoMeasure[], modifiers: StaffModifierBase[], printing: boolean) {\r\n const pageContext = vxSystem.context;\r\n measures.forEach((smoMeasure) => {\r\n const element = smoMeasure.svg.element;\r\n if (element) { \r\n smoMeasure.setBox(pageContext.offsetBbox(element), 'vxMeasure bounding box');\r\n }\r\n const vxMeasure = vxSystem.getVxMeasure(smoMeasure);\r\n if (vxMeasure) {\r\n vxMeasure.modifiersToBox.forEach((modifier) => {\r\n if (modifier.element) {\r\n modifier.logicalBox = pageContext.offsetBbox(modifier.element);\r\n }\r\n });\r\n }\r\n // unit test codes don't have tracker.\r\n if (this.measureMapper) {\r\n const tmpStaff: SmoSystemStaff | undefined = this.score!.staves.find((ss) => ss.staffId === smoMeasure.measureNumber.staffId);\r\n if (tmpStaff) {\r\n this.measureMapper.mapMeasure(tmpStaff, smoMeasure, printing);\r\n }\r\n } \r\n });\r\n modifiers.forEach((modifier) => {\r\n if (modifier.element) {\r\n modifier.logicalBox = pageContext.offsetBbox(modifier.element);\r\n }\r\n });\r\n }\r\n _renderSystem(lineIx: number, printing: boolean) {\r\n if (this.score === null || this.formatter === null) {\r\n return;\r\n }\r\n const measuresToBox: SmoMeasure[] = [];\r\n const modifiersToBox: StaffModifierBase[] = [];\r\n const columns: Record = this.formatter.systems[lineIx].systems;\r\n\r\n // If this page hasn't changed since rendered\r\n const pageIndex = columns[0][0].svg.pageIndex;\r\n \r\n if (this.renderingPage !== pageIndex && this.renderedPages[pageIndex] && !printing) {\r\n if (SuiScoreRender.debugMask) {\r\n console.log(`skipping render on page ${pageIndex}`);\r\n }\r\n return;\r\n }\r\n const context = this.vexContainers.getRendererForPage(pageIndex);\r\n if (this.renderingPage !== pageIndex) {\r\n context.clearMap();\r\n this.renderingPage = pageIndex;\r\n }\r\n const vxSystem: VxSystem = new VxSystem(context, 0, lineIx, this.score);\r\n const colKeys = Object.keys(columns);\r\n colKeys.forEach((colKey) => {\r\n columns[parseInt(colKey, 10)].forEach((measure: SmoMeasure) => {\r\n if (this.measureMapper !== null) {\r\n const modId = 'mod-' + measure.measureNumber.staffId + '-' + measure.measureNumber.measureIndex;\r\n SvgHelpers.removeElementsByClass(context.svg, modId);\r\n vxSystem.renderMeasure(measure, printing);\r\n const pageIndex = measure.svg.pageIndex;\r\n const renderMeasures = this.renderedPages[pageIndex];\r\n if (!renderMeasures) {\r\n this.renderedPages[pageIndex] = {\r\n startMeasure: measure.measureNumber.measureIndex,\r\n endMeasure: measure.measureNumber.measureIndex\r\n }\r\n } else {\r\n renderMeasures.endMeasure = measure.measureNumber.measureIndex;\r\n }\r\n measuresToBox.push(measure);\r\n if (!printing && !measure.format.isDefault) {\r\n const at: any[] = [];\r\n at.push({ y: measure.svg.logicalBox.y - 5 });\r\n at.push({ x: measure.svg.logicalBox.x + 25 });\r\n at.push({ 'font-family': SourceSansProFont.fontFamily });\r\n at.push({ 'font-size': '12pt' });\r\n SvgHelpers.placeSvgText(context.svg, at, 'measure-format', '*');\r\n }\r\n }\r\n });\r\n });\r\n this.score.staves.forEach((stf) => {\r\n this.renderModifiers(stf, vxSystem).forEach((modifier) => {\r\n modifiersToBox.push(modifier);\r\n });\r\n });\r\n if (this.measureMapper !== null) {\r\n vxSystem.renderEndings(this.measureMapper.scroller);\r\n }\r\n this.measuresToMap.push({vxSystem, measuresToBox, modifiersToBox, printing });\r\n // this.measureRenderedElements(vxSystem, measuresToBox, modifiersToBox, printing);\r\n\r\n const timestamp = new Date().valueOf();\r\n if (!this.lyricsToOffset.has(vxSystem.lineIndex)) {\r\n this.lyricsToOffset.set(vxSystem.lineIndex, vxSystem);\r\n }\r\n // vxSystem.updateLyricOffsets();\r\n layoutDebug.setTimestamp(layoutDebug.codeRegions.POST_RENDER, new Date().valueOf() - timestamp);\r\n }\r\n _renderNextSystemPromise(systemIx: number, keys: number[], printing: boolean) {\r\n return new Promise((resolve: any) => {\r\n // const sleepDate = new Date().valueOf();\r\n this._renderSystem(keys[systemIx], printing);\r\n requestAnimationFrame(() => resolve());\r\n });\r\n }\r\n\r\n async _renderNextSystem(lineIx: number, keys: number[], printing: boolean) {\r\n createTopDomContainer('#renderProgress', 'progress');\r\n if (lineIx < keys.length) {\r\n const progress = Math.round((100 * lineIx) / keys.length);\r\n $('#renderProgress').attr('max', 100);\r\n $('#renderProgress').val(progress);\r\n await this._renderNextSystemPromise(lineIx,keys, printing);\r\n lineIx++;\r\n await this._renderNextSystem(lineIx, keys, printing);\r\n } else {\r\n await this.renderScoreModifiers();\r\n this.numberMeasures();\r\n this.measuresToMap.forEach((mm) => {\r\n this.measureRenderedElements(mm.vxSystem, mm.measuresToBox, mm.modifiersToBox, mm.printing);\r\n });\r\n this.lyricsToOffset.forEach((vv) => {\r\n vv.updateLyricOffsets();\r\n });\r\n this.measuresToMap = [];\r\n this.lyricsToOffset = new Map();\r\n // We pro-rate the background render timer on how long it takes\r\n // to actually render the score, so we are not thrashing on a large\r\n // score.\r\n if (this._autoAdjustRenderTime) {\r\n this.renderTime = new Date().valueOf() - this.startRenderTime;\r\n }\r\n $('body').removeClass('show-render-progress');\r\n // indicate the display is 'clean' and up-to-date with the score\r\n $('body').removeClass('refresh-1');\r\n if (this.measureMapper !== null) {\r\n this.measureMapper.updateMap();\r\n if (layoutDebug.mask & layoutDebug.values['artifactMap']) {\r\n this.score?.staves.forEach((staff) => {\r\n staff.measures.forEach((mm) => {\r\n mm.voices.forEach((voice: SmoVoice) => {\r\n voice.notes.forEach((note) => {\r\n if (note.logicalBox) {\r\n const page = this.vexContainers.getRendererFromPoint(note.logicalBox);\r\n if (page) {\r\n const noteBox = SvgHelpers.smoBox(note.logicalBox);\r\n noteBox.y -= page.box.y;\r\n SvgHelpers.debugBox(page.svg, noteBox, 'measure-place-dbg', 0);\r\n }\r\n }\r\n });\r\n });\r\n });\r\n });\r\n }\r\n }\r\n this.backgroundRender = false;\r\n }\r\n }\r\n \r\n // ### unrenderMeasure\r\n // All SVG elements are associated with a logical SMO element. We need to erase any SVG element before we change a SMO\r\n // element in such a way that some of the logical elements go away (e.g. when deleting a measure).\r\n unrenderMeasure(measure: SmoMeasure) {\r\n if (!measure) {\r\n return;\r\n }\r\n const modId = 'mod-' + measure.measureNumber.staffId + '-' + measure.measureNumber.measureIndex;\r\n const context = this.vexContainers.getRenderer(measure.svg.logicalBox);\r\n if (!context) {\r\n return;\r\n }\r\n SvgHelpers.removeElementsByClass(context.svg, modId);\r\n\r\n if (measure.svg.element) {\r\n measure.svg.element.remove();\r\n measure.svg.element = null;\r\n if (measure.svg.tabElement) {\r\n measure.svg.tabElement.remove();\r\n measure.svg.tabElement = undefined;\r\n }\r\n }\r\n const renderPage = this.renderedPages[measure.svg.pageIndex];\r\n if (renderPage) {\r\n this.renderedPages[measure.svg.pageIndex] = null;\r\n }\r\n measure.setYTop(0, 'unrender');\r\n }\r\n // ### _renderModifiers\r\n // ### Description:\r\n // Render staff modifiers (modifiers straddle more than one measure, like a slur). Handle cases where the destination\r\n // is on a different system due to wrapping.\r\n renderModifiers(staff: SmoSystemStaff, system: VxSystem): StaffModifierBase[] {\r\n let nextNote: SmoSelection | null = null;\r\n let lastNote: SmoSelection | null = null;\r\n let testNote: Note | null = null;\r\n let vxStart: Note | null = null;\r\n let vxEnd: Note | null = null;\r\n const modifiersToBox: StaffModifierBase[] = [];\r\n const removedModifiers: StaffModifierBase[] = [];\r\n if (this.score === null || this.measureMapper === null) {\r\n return [];\r\n }\r\n const renderedId: Record = {};\r\n staff.renderableModifiers.forEach((modifier) => {\r\n const startNote = SmoSelection.noteSelection(this.score!,\r\n modifier.startSelector.staff, modifier.startSelector.measure, modifier.startSelector.voice, modifier.startSelector.tick);\r\n const endNote = SmoSelection.noteSelection(this.score!,\r\n modifier.endSelector.staff, modifier.endSelector.measure, modifier.endSelector.voice, modifier.endSelector.tick);\r\n if (!startNote || !endNote) {\r\n // If the modifier doesn't have score endpoints, delete it from the score\r\n removedModifiers.push(modifier);\r\n return;\r\n }\r\n if (startNote.note !== null) {\r\n vxStart = system.getVxNote(startNote.note);\r\n }\r\n if (endNote.note !== null) {\r\n vxEnd = system.getVxNote(endNote.note);\r\n }\r\n\r\n // If the modifier goes to the next staff, draw what part of it we can on this staff.\r\n if (vxStart && !vxEnd) {\r\n nextNote = SmoSelection.nextNoteSelection(this.score!,\r\n modifier.startSelector.staff, modifier.startSelector.measure, modifier.startSelector.voice, modifier.startSelector.tick);\r\n if (nextNote === null) {\r\n console.warn('bad selector ' + JSON.stringify(modifier.startSelector, null, ' '));\r\n } else {\r\n if (nextNote.note !== null) {\r\n testNote = system.getVxNote(nextNote.note);\r\n }\r\n while (testNote) {\r\n vxEnd = testNote;\r\n nextNote = SmoSelection.nextNoteSelection(this.score!,\r\n nextNote.selector.staff, nextNote.selector.measure, nextNote.selector.voice, nextNote.selector.tick);\r\n if (!nextNote) {\r\n break;\r\n }\r\n if (nextNote.note !== null) {\r\n testNote = system.getVxNote(nextNote.note);\r\n } else {\r\n testNote = null;\r\n }\r\n }\r\n }\r\n }\r\n if (vxEnd && !vxStart) {\r\n lastNote = SmoSelection.lastNoteSelection(this.score!,\r\n modifier.endSelector.staff, modifier.endSelector.measure, modifier.endSelector.voice, modifier.endSelector.tick);\r\n if (lastNote !== null && lastNote.note !== null) {\r\n testNote = system.getVxNote(lastNote.note);\r\n while (testNote !== null) {\r\n vxStart = testNote;\r\n lastNote = SmoSelection.lastNoteSelection(this.score!,\r\n lastNote.selector.staff, lastNote.selector.measure, lastNote.selector.voice, lastNote.selector.tick);\r\n if (!lastNote) {\r\n break;\r\n }\r\n if (lastNote.note !== null) {\r\n testNote = system.getVxNote(lastNote.note);\r\n } else {\r\n testNote = null;\r\n }\r\n }\r\n }\r\n }\r\n if (!vxStart && !vxEnd || renderedId[modifier.attrs.id]) {\r\n return;\r\n }\r\n renderedId[modifier.attrs.id] = true;\r\n system.renderModifier(this.measureMapper!.scroller, modifier, vxStart, vxEnd, startNote, endNote);\r\n modifiersToBox.push(modifier);\r\n });\r\n // Silently remove modifiers from the score if the endpoints no longer exist\r\n removedModifiers.forEach((mod) => {\r\n staff.removeStaffModifier(mod);\r\n });\r\n return modifiersToBox;\r\n }\r\n\r\n drawPageLines() {\r\n let i = 0;\r\n const printing = $('body').hasClass('print-render');\r\n const layoutMgr = this.score!.layoutManager;\r\n if (printing || !layoutMgr) {\r\n return;\r\n }\r\n for (i = 1; i < layoutMgr.pageLayouts.length; ++i) {\r\n const context = this.vexContainers.getRendererForPage(i - 1);\r\n if (context) {\r\n $(context.svg).find('.pageLine').remove();\r\n const scaledPage = layoutMgr.getScaledPageLayout(i);\r\n const y = scaledPage.pageHeight * i - context.box.y;\r\n SvgHelpers.line(context.svg, 0, y, scaledPage.pageWidth, y,\r\n { strokeName: 'line', stroke: '#321', strokeWidth: '2', strokeDasharray: '4,1', fill: 'none', opacity: 1.0 }, 'pageLine');\r\n \r\n }\r\n }\r\n }\r\n replaceSelection(staffMap: Record, change: SmoSelection) {\r\n let system: VxSystem | null = null;\r\n if (this.renderedPages[change.measure.svg.pageIndex]) {\r\n this.renderedPages[change.measure.svg.pageIndex] = null;\r\n }\r\n SmoBeamer.applyBeams(change.measure);\r\n // Defer modifier update until all selected measures are drawn.\r\n if (!staffMap[change.staff.staffId]) {\r\n const context = this.vexContainers.getRenderer(change.measure.svg.logicalBox);\r\n if (context) {\r\n system = new VxSystem(context, change.measure.staffY, change.measure.svg.lineIndex, this.score!);\r\n staffMap[change.staff.staffId] = { system, staff: change.staff }; \r\n }\r\n } else {\r\n system = staffMap[change.staff.staffId].system;\r\n }\r\n const selections = SmoSelection.measuresInColumn(this.score!, change.measure.measureNumber.measureIndex);\r\n const measuresToMeasure: SmoMeasure[] = [];\r\n selections.forEach((selection) => {\r\n if (system !== null && this.measureMapper !== null) {\r\n this.unrenderMeasure(selection.measure);\r\n system.renderMeasure(selection.measure, false);\r\n measuresToMeasure.push(selection.measure);\r\n }\r\n });\r\n if (system) {\r\n this.measureRenderedElements(system, measuresToMeasure, [], false);\r\n }\r\n }\r\n\r\n async renderAllMeasures(lines: number[]) {\r\n if (!this.score) {\r\n return;\r\n }\r\n const printing = $('body').hasClass('print-render');\r\n $('.measure-format').remove();\r\n \r\n if (!printing) {\r\n $('body').addClass('show-render-progress');\r\n const isShowing = SuiPiano.isShowing;\r\n if (this.score.preferences.showPiano && !isShowing) {\r\n SuiPiano.showPiano();\r\n this.measureMapper!.scroller.updateViewport();\r\n } else if (isShowing && !this.score.preferences.showPiano) {\r\n SuiPiano.hidePiano();\r\n this.measureMapper!.scroller.updateViewport();\r\n }\r\n }\r\n this.backgroundRender = true;\r\n this.startRenderTime = new Date().valueOf();\r\n this.renderingPage = -1;\r\n this.vexContainers.updateContainerOffset(this.measureMapper!.scroller.scrollState);\r\n await this._renderNextSystem(0, lines, printing);\r\n }\r\n // Number the measures at the first measure in each system.\r\n numberMeasures() {\r\n const printing: boolean = $('body').hasClass('print-render');\r\n const staff = this.score!.staves[0];\r\n const measures = staff.measures.filter((measure) => measure.measureNumber.systemIndex === 0);\r\n $('.measure-number').remove();\r\n\r\n measures.forEach((measure) => {\r\n const context = this.vexContainers.getRenderer(measure.svg.logicalBox);\r\n if (measure.measureNumber.localIndex > 0 && measure.measureNumber.systemIndex === 0 && measure.svg.logicalBox && context) {\r\n const numAr: any[] = [];\r\n const modBox = context.offsetSvgPoint(measure.svg.logicalBox);\r\n numAr.push({ y: modBox.y - 10 });\r\n numAr.push({ x: modBox.x });\r\n numAr.push({ 'font-family': SourceSansProFont.fontFamily });\r\n numAr.push({ 'font-size': '10pt' });\r\n SvgHelpers.placeSvgText(context.svg, numAr, 'measure-number', (measure.measureNumber.localIndex + 1).toString());\r\n\r\n // Show line-feed symbol\r\n if (measure.format.systemBreak && !printing) {\r\n const starAr: any[] = [];\r\n const symbol = '\\u21b0';\r\n starAr.push({ y: modBox.y - 5 });\r\n starAr.push({ x: modBox.x + 25 });\r\n starAr.push({ 'font-family': SourceSansProFont.fontFamily });\r\n starAr.push({ 'font-size': '12pt' });\r\n SvgHelpers.placeSvgText(context.svg, starAr, 'measure-format', symbol);\r\n }\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * This calculates the position of all the elements in the score, then renders the score\r\n * @returns \r\n */\r\n async layout() {\r\n if (!this.score) {\r\n return;\r\n }\r\n const score = this.score;\r\n $('head title').text(this.score.scoreInfo.name);\r\n const formatter = new SuiLayoutFormatter(score, this.vexContainers, this.renderedPages);\r\n Object.keys(this.renderedPages).forEach((key) => {\r\n this.vexContainers.clearModifiersForPage(parseInt(key));\r\n });\r\n const startPageCount = this.score.layoutManager!.pageLayouts.length;\r\n this.formatter = formatter;\r\n formatter.layout(); \r\n if (this.formatter.trimPages(startPageCount)) {\r\n this.setViewport();\r\n }\r\n this.measuresToMap = [];\r\n this.lyricsToOffset = new Map();\r\n await this.renderAllMeasures(formatter.lines);\r\n } \r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SmoScore } from '../../smo/data/score';\r\nimport { SmoMeasure } from '../../smo/data/measure';\r\nimport { SmoTextGroup } from '../../smo/data/scoreText';\r\nimport { SmoGraceNote } from '../../smo/data/noteModifiers';\r\nimport { SmoSystemStaff } from '../../smo/data/systemStaff';\r\nimport { SmoPartInfo } from '../../smo/data/partInfo';\r\nimport { StaffModifierBase } from '../../smo/data/staffModifiers';\r\nimport { SmoSelection, SmoSelector } from '../../smo/xform/selections';\r\nimport { UndoBuffer, copyUndo } from '../../smo/xform/undo';\r\nimport { PasteBuffer } from '../../smo/xform/copypaste';\r\nimport { SuiScroller } from './scroller';\r\nimport { SvgHelpers } from './svgHelpers';\r\nimport { SuiTracker } from './tracker';\r\nimport { createTopDomContainer } from '../../common/htmlHelpers';\r\nimport { SmoRenderConfiguration } from './configuration';\r\nimport { SuiRenderState, scoreChangeEvent } from './renderState';\r\nimport { ScoreRenderParams } from './scoreRender';\r\nimport { SmoOperation } from '../../smo/xform/operations';\r\nimport { SuiAudioPlayer } from '../audio/player';\r\nimport { SuiAudioAnimationParams } from '../audio/musicCursor';\r\nimport { SmoTempoText } from '../../smo/data/measureModifiers';\r\nimport { TimeSignature } from '../../smo/data/measureModifiers';\r\n\r\ndeclare var $: any;\r\n\r\n/**\r\n * Indicates a stave is/is not displayed in the score\r\n * @category SuiRender\r\n */\r\nexport interface ViewMapEntry {\r\n show: boolean;\r\n}\r\n\r\n/**\r\n * Base class for all operations on the rendered score. The base class handles the following:\r\n * 1. Undo and recording actions for the operation\r\n * 2. Maintain/change which staves in the score are displayed (staff map)\r\n * 3. Mapping between the displayed score and the data representation\r\n * @category SuiRender\r\n */\r\nexport abstract class SuiScoreView {\r\n static Instance: SuiScoreView | null = null;\r\n score: SmoScore; // The score that is displayed\r\n storeScore: SmoScore; // the full score, including invisible staves\r\n staffMap: number[]; // mapping the 2 things above\r\n storeUndo: UndoBuffer; // undo buffer for operations to above\r\n tracker: SuiTracker; // UI selections\r\n renderer: SuiRenderState;\r\n scroller: SuiScroller;\r\n pasteBuffer: PasteBuffer;\r\n storePaste: PasteBuffer;\r\n config: SmoRenderConfiguration;\r\n audioAnimation: SuiAudioAnimationParams;\r\n constructor(config: SmoRenderConfiguration, svgContainer: HTMLElement, score: SmoScore, scrollSelector: HTMLElement, undoBuffer: UndoBuffer) {\r\n this.score = score;\r\n const renderParams: ScoreRenderParams = {\r\n elementId: svgContainer,\r\n score,\r\n config,\r\n undoBuffer\r\n };\r\n this.audioAnimation = config.audioAnimation;\r\n this.renderer = new SuiRenderState(renderParams);\r\n this.config = config;\r\n const scoreJson = score.serialize({ skipStaves: false, useDictionary: false });\r\n this.scroller = new SuiScroller(scrollSelector, this.renderer.renderer.vexContainers);\r\n this.pasteBuffer = new PasteBuffer();\r\n this.storePaste = new PasteBuffer();\r\n this.tracker = new SuiTracker(this.renderer, this.scroller, this.pasteBuffer);\r\n this.renderer.setMeasureMapper(this.tracker);\r\n\r\n this.storeScore = SmoScore.deserialize(JSON.stringify(scoreJson));\r\n this.synchronizeTextGroups()\r\n this.storeUndo = new UndoBuffer();\r\n this.staffMap = this.defaultStaffMap;\r\n SuiScoreView.Instance = this; // for debugging\r\n this.setMappedStaffIds();\r\n createTopDomContainer('.saveLink'); // for file upload\r\n }\r\n synchronizeTextGroups() {\r\n // Synchronize the score text IDs so cut/paste/undo works transparently\r\n this.score.textGroups.forEach((tg, ix) => {\r\n if (this.storeScore.textGroups.length > ix) {\r\n this.storeScore.textGroups[ix].attrs.id = tg.attrs.id;\r\n }\r\n });\r\n }\r\n /**\r\n * Await on the full update of the score\r\n * @returns \r\n */\r\n renderPromise(): Promise {\r\n return this.renderer.renderPromise();\r\n }\r\n /**\r\n * Await on the partial update of the score in the view\r\n * @returns \r\n */\r\n updatePromise(): Promise {\r\n return this.renderer.updatePromise();\r\n }\r\n async awaitRender(): Promise {\r\n this.renderer.rerenderAll();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * await on the full update of the score, also resetting the viewport (to reflect layout changes)\r\n * @returns \r\n */\r\n refreshViewport(): Promise {\r\n this.renderer.preserveScroll();\r\n this.renderer.setViewport();\r\n this.renderer.setRefresh();\r\n return this.renderer.renderPromise();\r\n }\r\n handleScrollEvent(scrollLeft: number, scrollTop: number) {\r\n this.tracker.scroller.handleScroll(scrollLeft, scrollTop);\r\n }\r\n getPartMap(): { keys: number[], partMap: Record } {\r\n let keepNext = false;\r\n let partCount = 0;\r\n let partMap: Record = {};\r\n const keys: number[] = [];\r\n this.storeScore.staves.forEach((staff) => {\r\n const partInfo = staff.partInfo;\r\n partInfo.associatedStaff = staff.staffId;\r\n if (!keepNext) {\r\n partMap[partCount] = partInfo;\r\n keys.push(partCount);\r\n partCount += 1;\r\n if (partInfo.stavesAfter > 0) {\r\n keepNext = true;\r\n }\r\n } else {\r\n keepNext = false;\r\n }\r\n });\r\n return { keys, partMap };\r\n }\r\n /**\r\n * This is used in some Smoosic demos and pens.\r\n * @param action any action, but most usefully a SuiScoreView method\r\n * @param repetition number of times to repeat, waiting on render promise between\r\n * if not specified, defaults to 1\r\n * @returns promise, resolved action has been completed and score is updated.\r\n */\r\n waitableAction(action: () => void, repetition?: number): Promise {\r\n const rep = repetition ?? 1;\r\n const self = this;\r\n const promise = new Promise((resolve: any) => {\r\n const fc = async (count: number) => {\r\n if (count > 0) {\r\n action();\r\n await self.renderer.updatePromise();\r\n fc(count - 1);\r\n } else {\r\n resolve();\r\n }\r\n };\r\n fc(rep);\r\n });\r\n return promise;\r\n }\r\n\r\n /**\r\n * The plural form of _getEquivalentSelection\r\n * @param selections \r\n * @returns \r\n */\r\n _getEquivalentSelections(selections: SmoSelection[]): SmoSelection[] {\r\n const rv: SmoSelection[] = [];\r\n selections.forEach((selection) => {\r\n const sel = this._getEquivalentSelection(selection);\r\n if (sel !== null) {\r\n rv.push(sel);\r\n }\r\n });\r\n return rv;\r\n }\r\n /**\r\n * A staff modifier has changed, create undo operations for the measures affected\r\n * @param label \r\n * @param staffModifier \r\n * @param subtype \r\n */\r\n _undoStaffModifier(label: string, staffModifier: StaffModifierBase, subtype: number) {\r\n const copy = StaffModifierBase.deserialize(staffModifier.serialize());\r\n copy.startSelector = this._getEquivalentSelector(copy.startSelector);\r\n copy.endSelector = this._getEquivalentSelector(copy.endSelector);\r\n this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.STAFF_MODIFIER, SmoSelector.default,\r\n copy.serialize(), subtype);\r\n }\r\n /** \r\n * Return the index of the page that is in the center of the client screen.\r\n */\r\n getFocusedPage(): number {\r\n if (this.score.layoutManager === undefined) {\r\n return 0;\r\n }\r\n const scrollAvg = this.tracker.scroller.netScroll.y + (this.tracker.scroller.viewport.height / 2);\r\n const midY = scrollAvg;\r\n const layoutManager = this.score.layoutManager.getGlobalLayout();\r\n const lh = layoutManager.pageHeight / layoutManager.svgScale;\r\n const lw = layoutManager.pageWidth / layoutManager.svgScale;\r\n const pt = this.renderer.pageMap.svgToClient(SvgHelpers.smoBox({ x: lw, y: lh }));\r\n return Math.round(midY / pt.y);\r\n }\r\n /**\r\n * Create a rectangle undo, like a multiple columns but not necessarily the whole\r\n * score.\r\n */\r\n _undoColumn(label: string, measureIndex: number) {\r\n this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.COLUMN, SmoSelector.default,\r\n { score: this.storeScore, measureIndex }, UndoBuffer.bufferSubtypes.NONE);\r\n }\r\n /**\r\n * Score preferences don't affect the display, but they do have an undo\r\n * @param label \r\n */\r\n _undoScorePreferences(label: string) {\r\n this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.SCORE_ATTRIBUTES, SmoSelector.default, this.storeScore, UndoBuffer.bufferSubtypes.NONE);\r\n }\r\n \r\n /**\r\n * Add to the undo buffer the current set of measures selected.\r\n * @param label \r\n * @returns \r\n */\r\n _undoTrackerMeasureSelections(label: string): SmoSelection[] {\r\n const measureSelections = SmoSelection.getMeasureList(this.tracker.selections);\r\n measureSelections.forEach((measureSelection) => {\r\n const equiv = this._getEquivalentSelection(measureSelection);\r\n if (equiv !== null) {\r\n this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.MEASURE, equiv.selector, equiv.measure,\r\n UndoBuffer.bufferSubtypes.NONE);\r\n }\r\n });\r\n return measureSelections;\r\n }\r\n /**\r\n * operation that only affects the first selection. Setup undo for the measure\r\n */\r\n _undoFirstMeasureSelection(label: string): SmoSelection {\r\n const sel = this.tracker.selections[0];\r\n const equiv = this._getEquivalentSelection(sel);\r\n if (equiv !== null) {\r\n this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.MEASURE, equiv.selector, equiv.measure,\r\n UndoBuffer.bufferSubtypes.NONE);\r\n }\r\n return sel;\r\n }\r\n /**\r\n * Add the selection to the undo buffer\r\n * @param label \r\n * @param selection \r\n */\r\n _undoSelection(label: string, selection: SmoSelection) {\r\n const equiv = this._getEquivalentSelection(selection);\r\n if (equiv !== null) {\r\n this.storeUndo.addBuffer(label,\r\n UndoBuffer.bufferTypes.MEASURE, equiv.selector, equiv.measure,\r\n UndoBuffer.bufferSubtypes.NONE);\r\n }\r\n }\r\n /**\r\n * Add multiple selections to the undo buffer as a group\r\n * @param label \r\n * @param selections \r\n */\r\n _undoSelections(label: string, selections: SmoSelection[]) {\r\n this.storeUndo.grouping = true;\r\n selections.forEach((selection) => {\r\n this._undoSelection(label, selection);\r\n });\r\n this.storeUndo.grouping = false;\r\n }\r\n\r\n /** \r\n * Update renderer for measures that have changed\r\n */\r\n _renderChangedMeasures(measureSelections: SmoSelection[]) {\r\n if (!Array.isArray(measureSelections)) {\r\n measureSelections = [measureSelections];\r\n }\r\n measureSelections.forEach((measureSelection) => {\r\n this.renderer.addToReplaceQueue(measureSelection);\r\n });\r\n }\r\n /**\r\n * Update renderer for some columns\r\n * @param fromSelector \r\n * @param toSelector \r\n */\r\n _renderRectangle(fromSelector: SmoSelector, toSelector: SmoSelector) {\r\n this._getRectangleSelections(fromSelector, toSelector).forEach((s) => {\r\n this.renderer.addToReplaceQueue(s);\r\n });\r\n }\r\n\r\n /**\r\n * Setup undo for operation that affects the whole score\r\n * @param label \r\n */\r\n _undoScore(label: string) {\r\n this.storeUndo.addBuffer(label, UndoBuffer.bufferTypes.SCORE, SmoSelector.default, this.storeScore,\r\n UndoBuffer.bufferSubtypes.NONE);\r\n }\r\n /**\r\n * Get the selector from this.storeScore that maps to the displayed selector from this.score\r\n * @param selector \r\n * @returns \r\n */\r\n _getEquivalentSelector(selector: SmoSelector) {\r\n const rv = JSON.parse(JSON.stringify(selector));\r\n rv.staff = this.staffMap[selector.staff];\r\n return rv;\r\n }\r\n /**\r\n * Get the equivalent staff id from this.storeScore that maps to the displayed selector from this.score\r\n * @param staffId \r\n * @returns \r\n */\r\n _getEquivalentStaff(staffId: number) {\r\n return this.staffMap[staffId];\r\n }\r\n /**\r\n * Get the equivalent selection from this.storeScore that maps to the displayed selection from this.score\r\n * @param selection \r\n * @returns \r\n */\r\n _getEquivalentSelection(selection: SmoSelection): SmoSelection | null {\r\n try {\r\n if (typeof (selection.selector.tick) === 'undefined') {\r\n return SmoSelection.measureSelection(this.storeScore, this.staffMap[selection.selector.staff], selection.selector.measure);\r\n }\r\n if (typeof (selection.selector.pitches) === 'undefined') {\r\n return SmoSelection.noteSelection(this.storeScore, this.staffMap[selection.selector.staff], selection.selector.measure, selection.selector.voice,\r\n selection.selector.tick);\r\n }\r\n return SmoSelection.pitchSelection(this.storeScore, this.staffMap[selection.selector.staff], selection.selector.measure, selection.selector.voice,\r\n selection.selector.tick, selection.selector.pitches);\r\n } catch (ex) {\r\n console.warn(ex);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Get the equivalent selection from this.storeScore that maps to the displayed selection from this.score\r\n * @param selection \r\n * @returns \r\n */\r\n _getEquivalentGraceNote(selection: SmoSelection, gn: SmoGraceNote): SmoGraceNote {\r\n if (selection.note !== null) {\r\n const rv = selection.note.getGraceNotes().find((gg) => gg.attrs.id === gn.attrs.id);\r\n if (rv) {\r\n return rv;\r\n }\r\n }\r\n return gn;\r\n }\r\n /**\r\n * Get the rectangle of selections indicated by the parameters from the score\r\n * @param startSelector \r\n * @param endSelector \r\n * @param score \r\n * @returns \r\n */\r\n _getRectangleSelections(startSelector: SmoSelector, endSelector: SmoSelector): SmoSelection[] {\r\n const rv: SmoSelection[] = [];\r\n let i = 0;\r\n let j = 0;\r\n for (i = startSelector.staff; i <= endSelector.staff; i++) {\r\n for (j = startSelector.measure; j <= endSelector.measure; j++) {\r\n const target = SmoSelection.measureSelection(this.score, i, j);\r\n if (target !== null) {\r\n rv.push(target);\r\n }\r\n }\r\n }\r\n return rv;\r\n }\r\n /**\r\n * set the grouping flag for undo operations\r\n * @param val \r\n */\r\n groupUndo(val: boolean) {\r\n this.storeUndo.grouping = val;\r\n }\r\n\r\n /**\r\n * Show all staves, 1:1 mapping of view score staff to stored score staff\r\n */\r\n get defaultStaffMap(): number[] {\r\n let i = 0;\r\n const rv: number[] = [];\r\n for (i = 0; i < this.storeScore.staves.length; ++i) {\r\n rv.push(i);\r\n }\r\n return rv;\r\n }\r\n /**\r\n * Bootstrapping function, creates the renderer and associated timers\r\n */\r\n startRenderingEngine() {\r\n if (!this.renderer.score) {\r\n // If there is only one part, display the part.\r\n if (this.storeScore.isPartExposed()) {\r\n this.exposePart(this.score.staves[0]);\r\n }\r\n // If the score is transposing, hide the instrument xpose settings\r\n this._setTransposing();\r\n this.renderer.score = this.score;\r\n this.renderer.setViewport();\r\n }\r\n this.renderer.startDemon();\r\n }\r\n /**\r\n * Gets the current mapping of displayed staves to score staves (this.storeScore)\r\n * @returns \r\n */\r\n getView(): ViewMapEntry[] {\r\n const rv = [];\r\n let i = 0;\r\n for (i = 0; i < this.storeScore.staves.length; ++i) {\r\n const show = this.staffMap.indexOf(i) >= 0;\r\n rv.push({ show });\r\n }\r\n return rv;\r\n }\r\n /**\r\n * Update the staff ID when the view changes\r\n */\r\n setMappedStaffIds() {\r\n this.score.staves.forEach((staff) => {\r\n if (!this.isPartExposed()) {\r\n staff.partInfo.displayCues = staff.partInfo.cueInScore;\r\n } else {\r\n staff.partInfo.displayCues = false;\r\n }\r\n staff.setMappedStaffId(this.staffMap[staff.staffId]);\r\n });\r\n }\r\n resetPartView() {\r\n if (this.staffMap.length === 1) {\r\n const staff = this.storeScore.staves[this.staffMap[0]];\r\n this.exposePart(staff);\r\n }\r\n }\r\n /**\r\n * Exposes a part: hides non-part staves, shows part staves.\r\n * Note this will reset the view. After this operation, staff 0 will\r\n * be the selected part.\r\n * @param staff \r\n */\r\n exposePart(staff: SmoSystemStaff) {\r\n let i = 0;\r\n const exposeMap: ViewMapEntry[] = [];\r\n let pushNext = false;\r\n for (i = 0; i < this.storeScore.staves.length; ++i) {\r\n const tS = this.storeScore.staves[i];\r\n const show = tS.staffId === staff.staffId;\r\n if (pushNext) {\r\n exposeMap.push({ show: true });\r\n pushNext = false;\r\n } else {\r\n exposeMap.push({ show });\r\n if (tS.partInfo.stavesAfter > 0 && show) {\r\n pushNext = true;\r\n }\r\n }\r\n }\r\n this.setView(exposeMap);\r\n }\r\n /**\r\n * Indicates if the score is displaying in part-mode vs. score mode.\r\n * @returns \r\n */\r\n isPartExposed(): boolean {\r\n return this.score.isPartExposed();\r\n }\r\n /**\r\n * Parts have different formatting options from the parent score, indluding layout. Reset\r\n * them when exposing a part.\r\n */\r\n _mapPartFormatting() {\r\n this.score.layoutManager = this.score.staves[0].partInfo.layoutManager;\r\n let replacedText = false;\r\n this.score.staves.forEach((staff) => {\r\n staff.updateMeasureFormatsForPart();\r\n if (staff.partInfo.preserveTextGroups && !replacedText) {\r\n const tga: SmoTextGroup[] = [];\r\n replacedText = true;\r\n staff.partInfo.textGroups.forEach((tg) => {\r\n tga.push(tg)\r\n });\r\n this.score.textGroups = tga;\r\n }\r\n });\r\n }\r\n /**\r\n * Update the list of staves in the score that are displayed.\r\n */\r\n setView(rows: ViewMapEntry[]) {\r\n let i = 0;\r\n const any = rows.find((row) => row.show === true);\r\n if (!any) {\r\n return;\r\n }\r\n const nscore = SmoScore.deserialize(JSON.stringify(this.storeScore.serialize({ skipStaves: true, useDictionary: false })));\r\n const staffMap = [];\r\n for (i = 0; i < rows.length; ++i) {\r\n const row = rows[i];\r\n if (row.show) {\r\n const srcStave = this.storeScore.staves[i];\r\n const jsonObj = srcStave.serialize({ skipMaps: false });\r\n jsonObj.staffId = staffMap.length;\r\n const nStave = SmoSystemStaff.deserialize(jsonObj);\r\n nStave.mapStaffFromTo(i, nscore.staves.length);\r\n nscore.staves.push(nStave);\r\n if (srcStave.keySignatureMap) {\r\n nStave.keySignatureMap = JSON.parse(JSON.stringify(srcStave.keySignatureMap));\r\n }\r\n nStave.measures.forEach((measure: SmoMeasure, ix) => {\r\n const srcMeasure = srcStave.measures[ix];\r\n measure.tempo = new SmoTempoText(srcMeasure.tempo.serialize());\r\n measure.timeSignature = new TimeSignature(srcMeasure.timeSignature);\r\n measure.keySignature = srcMeasure.keySignature;\r\n });\r\n staffMap.push(i);\r\n }\r\n }\r\n nscore.numberStaves();\r\n this.staffMap = staffMap;\r\n this.score = nscore;\r\n // Indicate which score staff view staves are mapped to, to decide to display\r\n // modifiers.\r\n this.setMappedStaffIds();\r\n // TODO: add part-specific measure formatting, etc.\r\n this._setTransposing();\r\n this.renderer.score = nscore;\r\n // If this current view is a part, show the part layout\r\n if (this.isPartExposed()) {\r\n this._mapPartFormatting(); \r\n this.score.staves.forEach((staff) => {\r\n staff.partInfo.displayCues = false;\r\n });\r\n SmoOperation.computeMultipartRest(nscore);\r\n } else {\r\n this.score.staves.forEach((staff) => {\r\n staff.partInfo.displayCues = staff.partInfo.cueInScore;\r\n });\r\n }\r\n window.dispatchEvent(new CustomEvent(scoreChangeEvent, { detail: { view: this } }));\r\n this.renderer.setViewport();\r\n }\r\n /**\r\n * view all the staffs in score mode.\r\n */\r\n viewAll() {\r\n this.score = SmoScore.deserialize(JSON.stringify(\r\n this.storeScore.serialize({ skipStaves: false, useDictionary: false })));\r\n this.staffMap = this.defaultStaffMap;\r\n this.setMappedStaffIds();\r\n this._setTransposing();\r\n this.synchronizeTextGroups();\r\n this.renderer.score = this.score;\r\n this.pasteBuffer.setScore(this.score);\r\n window.dispatchEvent(new CustomEvent(scoreChangeEvent, { detail: { view: this } }));\r\n this.renderer.setViewport();\r\n }\r\n /**\r\n * Update score based on transposing flag.\r\n */\r\n _setTransposing() {\r\n if (!this.isPartExposed()) {\r\n const xpose = this.score.preferences?.transposingScore;\r\n if (xpose) {\r\n this.score.setTransposing();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Update the view after loading or restoring a completely new score\r\n * @param score \r\n * @returns \r\n */\r\n async changeScore(score: SmoScore) {\r\n this.storeUndo.reset();\r\n SuiAudioPlayer.stopPlayer();\r\n this.renderer.score = score;\r\n this.renderer.setViewport();\r\n this.storeScore = SmoScore.deserialize(JSON.stringify(\r\n score.serialize({ skipStaves: false, useDictionary: false })));\r\n this.score = score;\r\n // If the score is non-transposing, hide the instrument xpose settings\r\n this._setTransposing();\r\n this.staffMap = this.defaultStaffMap;\r\n this.setMappedStaffIds();\r\n this.synchronizeTextGroups();\r\n if (this.storeScore.isPartExposed()) {\r\n this.exposePart(this.score.staves[0]);\r\n }\r\n const rv = await this.awaitRender();\r\n window.dispatchEvent(new CustomEvent(scoreChangeEvent, { detail: { view: this } }));\r\n return rv;\r\n }\r\n\r\n /**\r\n * for the view score, the renderer decides what to render\r\n * depending on what is undone.\r\n * @returns \r\n */\r\n undo() {\r\n if (!this.renderer.score) {\r\n return;\r\n }\r\n \r\n // A score-level undo might have changed the score.\r\n if (this.storeUndo.buffer.length < 1) {\r\n return;\r\n }\r\n const staffMap: Record = {};\r\n const identityMap: Record = {};\r\n this.defaultStaffMap.forEach((nn) => identityMap[nn] = nn);\r\n this.staffMap.forEach((mm, ix) => staffMap[mm] = ix);\r\n this.score = this.renderer.undo(this.storeUndo, staffMap);\r\n this.storeScore = this.storeUndo.undo(this.storeScore, identityMap, true);\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SuiScoreView } from './scoreView';\r\nimport { SmoScore, engravingFontType } from '../../smo/data/score';\r\nimport { SmoSystemStaffParams, SmoSystemStaff } from '../../smo/data/systemStaff';\r\nimport { SmoPartInfo } from '../../smo/data/partInfo';\r\nimport { SmoMeasure } from '../../smo/data/measure';\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { KeyEvent, SvgBox, Pitch, PitchLetter } from '../../smo/data/common';\r\nimport { SmoRenderConfiguration } from './configuration';\r\nimport { SmoSystemGroup, SmoPageLayout, SmoGlobalLayout, SmoLayoutManager, SmoAudioPlayerSettings,\r\n SmoScorePreferences, SmoScoreInfo } from '../../smo/data/scoreModifiers';\r\nimport { SmoTextGroup } from '../../smo/data/scoreText';\r\nimport { SmoDynamicText, SmoNoteModifierBase, SmoGraceNote, SmoArticulation, \r\n SmoOrnament, SmoLyric, SmoMicrotone, SmoArpeggio, SmoArpeggioType, SmoClefChange, \r\n SmoTabNote} from '../../smo/data/noteModifiers';\r\nimport { SmoTempoText, SmoVolta, SmoBarline, SmoRepeatSymbol, SmoRehearsalMark, SmoMeasureFormat, TimeSignature } from '../../smo/data/measureModifiers';\r\nimport { UndoBuffer, SmoUndoable } from '../../smo/xform/undo';\r\nimport { SmoOperation } from '../../smo/xform/operations';\r\nimport { BatchSelectionOperation } from '../../smo/xform/operations';\r\nimport { smoSerialize } from '../../common/serializationHelpers';\r\nimport { FontInfo } from '../../common/vex';\r\nimport { SmoMusic } from '../../smo/data/music';\r\nimport { SuiOscillator } from '../audio/oscillator';\r\nimport { XmlToSmo } from '../../smo/mxml/xmlToSmo';\r\nimport { SuiAudioPlayer } from '../audio/player';\r\nimport { SuiXhrLoader } from '../../ui/fileio/xhrLoader';\r\nimport { SmoSelection, SmoSelector } from '../../smo/xform/selections';\r\nimport { StaffModifierBase, SmoSlur,\r\n SmoInstrument, SmoInstrumentParams, SmoStaffTextBracket, SmoTabStave } from '../../smo/data/staffModifiers';\r\nimport { SuiPiano } from './piano';\r\nimport { SvgHelpers } from './svgHelpers';\r\nimport { PromiseHelpers } from '../../common/promiseHelpers';\r\ndeclare var $: any;\r\ndeclare var SmoConfig: SmoRenderConfiguration;\r\n\r\n/**\r\n * MVVM-like operations on the displayed score.\r\n * \r\n * All operations that can be performed on a 'live' score go through this\r\n * module. It maps the score view to the actual score and makes sure the\r\n * model and view stay in sync. \r\n * \r\n * Because this object operates on the current selections, \r\n * all operations return promise so applications can wait for the \r\n * operation to complete and update the selection list.\r\n * @category SuiRender\r\n */\r\nexport class SuiScoreViewOperations extends SuiScoreView {\r\n /**\r\n * Add a new text group to the score \r\n * @param textGroup a new text group\r\n * @returns \r\n */\r\n async addTextGroup(textGroup: SmoTextGroup): Promise {\r\n const altNew = SmoTextGroup.deserializePreserveId(textGroup.serialize());\r\n SmoUndoable.changeTextGroup(this.storeScore, this.storeUndo, altNew,\r\n UndoBuffer.bufferSubtypes.ADD);\r\n if (this.isPartExposed()) {\r\n this.score.updateTextGroup(textGroup, true);\r\n const partInfo = this.storeScore.staves[this._getEquivalentStaff(0)].partInfo;\r\n partInfo.updateTextGroup(altNew, true);\r\n } else {\r\n this.score.addTextGroup(textGroup);\r\n this.storeScore.addTextGroup(altNew);\r\n }\r\n await this.renderer.renderScoreModifiers();\r\n return this.renderer.updatePromise()\r\n }\r\n\r\n /**\r\n * Remove the text group from the score\r\n * @param textGroup \r\n * @returns \r\n */\r\n async removeTextGroup(textGroup: SmoTextGroup): Promise {\r\n this.score.updateTextGroup(textGroup, false);\r\n const altGroup = SmoTextGroup.deserializePreserveId(textGroup.serialize());\r\n textGroup.elements.forEach((el) => el.remove());\r\n textGroup.elements = [];\r\n const isPartExposed = this.isPartExposed();\r\n if (!isPartExposed) {\r\n SmoUndoable.changeTextGroup(this.storeScore, this.storeUndo, altGroup,\r\n UndoBuffer.bufferSubtypes.REMOVE);\r\n this.storeScore.updateTextGroup(altGroup, false);\r\n } else {\r\n const stave = this.storeScore.staves[this._getEquivalentStaff(0)];\r\n stave.partInfo.textGroups = this.score.textGroups;\r\n SmoUndoable.changeTextGroup(this.storeScore, this.storeUndo, altGroup,\r\n UndoBuffer.bufferSubtypes.REMOVE); \r\n }\r\n await this.renderer.renderScoreModifiers();\r\n return this.renderer.updatePromise()\r\n }\r\n\r\n /**\r\n * UPdate an existing text group. The original is passed in, because since TG not tied to a musical\r\n * element, we need to find the one we're updating.\r\n * @param oldVersion \r\n * @param newVersion \r\n * @returns \r\n */\r\n async updateTextGroup(newVersion: SmoTextGroup): Promise {\r\n const isPartExposed = this.isPartExposed();\r\n const altNew = SmoTextGroup.deserializePreserveId(newVersion.serialize());\r\n this.score.updateTextGroup(newVersion, true);\r\n // If this is part text, don't store it in the score text, except for the displayed score\r\n if (!isPartExposed) {\r\n SmoUndoable.changeTextGroup(this.storeScore, this.storeUndo, altNew, UndoBuffer.bufferSubtypes.UPDATE);\r\n this.storeScore.updateTextGroup(altNew, true);\r\n } else {\r\n this.storeScore.staves[this._getEquivalentStaff(0)].partInfo.updateTextGroup(altNew, true);\r\n }\r\n // TODO: only render the one TG.\r\n await this.renderer.renderScoreModifiers();\r\n // return this.renderer.updatePromise();\r\n }\r\n /**\r\n * load an mxml score remotely, return a promise that \r\n * completes when the file is loaded\r\n * @param url where to find the xml file\r\n * @returns \r\n */\r\n async loadRemoteXml(url: string): Promise {\r\n const req = new SuiXhrLoader(url);\r\n const self = this;\r\n // Shouldn't we return promise of actually displaying the score?\r\n await req.loadAsync();\r\n const parser = new DOMParser();\r\n const xml = parser.parseFromString(req.value, 'text/xml');\r\n const score = XmlToSmo.convert(xml);\r\n score.layoutManager!.zoomToWidth($('body').width());\r\n await self.changeScore(score);\r\n }\r\n /**\r\n * load a remote score in SMO format\r\n * @param url url to find the score\r\n * @returns \r\n */\r\n async loadRemoteJson(url: string) : Promise {\r\n const req = new SuiXhrLoader(url);\r\n await req.loadAsync();\r\n const score = SmoScore.deserialize(req.value);\r\n await this.changeScore(score);\r\n }\r\n /**\r\n * Load a remote score, return promise when it's been loaded\r\n * from afar.\r\n * @param pref \r\n * @returns \r\n */\r\n async loadRemoteScore(url: string): Promise {\r\n if (url.endsWith('xml') || url.endsWith('mxl')) {\r\n return this.loadRemoteXml(url);\r\n } else {\r\n return this.loadRemoteJson(url);\r\n }\r\n }\r\n async updateAudioSettings(pref: SmoAudioPlayerSettings) {\r\n this._undoScorePreferences('Update preferences');\r\n this.score.audioSettings = pref;\r\n this.storeScore.audioSettings = new SmoAudioPlayerSettings(pref);\r\n // No rendering to be done\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Global settings that control how the score editor behaves\r\n * @param pref \r\n * @returns \r\n */\r\n async updateScorePreferences(pref: SmoScorePreferences): Promise {\r\n this._undoScorePreferences('Update preferences');\r\n const oldXpose = this.score.preferences.transposingScore;\r\n const curXpose = pref.transposingScore;\r\n this.score.updateScorePreferences(new SmoScorePreferences(pref));\r\n this.storeScore.updateScorePreferences(new SmoScorePreferences(pref));\r\n if (curXpose === false && oldXpose === true) {\r\n this.score.setNonTransposing();\r\n } else if (curXpose === true && oldXpose === false) {\r\n this.score.setTransposing();\r\n }\r\n this.renderer.setDirty();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Update information about the score, composer etc.\r\n * @param scoreInfo \r\n * @returns \r\n */\r\n async updateScoreInfo(scoreInfo: SmoScoreInfo): Promise {\r\n this._undoScorePreferences('Update preferences');\r\n this.score.scoreInfo = scoreInfo;\r\n this.storeScore.scoreInfo = JSON.parse(JSON.stringify(scoreInfo));\r\n return this.renderer.updatePromise()\r\n }\r\n\r\n /**\r\n * Add a specific microtone modifier to the selected notes\r\n * @param tone \r\n * @returns \r\n */\r\n async addRemoveMicrotone(tone: SmoMicrotone): Promise {\r\n const selections = this.tracker.selections;\r\n const altSelections = this._getEquivalentSelections(selections);\r\n const measureSelections = this._undoTrackerMeasureSelections('add/remove microtone');\r\n\r\n SmoOperation.addRemoveMicrotone(null, selections, tone);\r\n SmoOperation.addRemoveMicrotone(null, altSelections, tone);\r\n this._renderChangedMeasures(measureSelections);\r\n return this.renderer.updatePromise()\r\n }\r\n async addRemoveArpeggio(code: SmoArpeggioType) {\r\n const selections = this.tracker.selections;\r\n const altSelections = this._getEquivalentSelections(selections);\r\n const measureSelections = this._undoTrackerMeasureSelections('add/remove arpeggio');\r\n [selections, altSelections].forEach((selType) => {\r\n selType.forEach((sel) => {\r\n if (sel.note) {\r\n if (code === 'none') {\r\n sel.note.arpeggio = undefined;\r\n } else {\r\n sel.note.arpeggio = new SmoArpeggio({ type: code });\r\n }\r\n }\r\n });\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise()\r\n }\r\n /**\r\n * A clef change mid-measure (clefNote)\r\n * @param clef \r\n */\r\n async addRemoveClefChange(clef: SmoClefChange) {\r\n const selections = [this.tracker.selections[0]];\r\n const altSelections = this._getEquivalentSelections(selections);\r\n const measureSelections = this._undoTrackerMeasureSelections('add/remove clef change');\r\n [selections, altSelections].forEach((selType) => {\r\n selType.forEach((sel) => {\r\n if (sel.note) {\r\n const measureClef = sel.measure.clef;\r\n // If the clef is the same as measure clef, remove any clef change from Note\r\n if (measureClef === clef.clef) {\r\n sel.note.clefNote = null;\r\n } else {\r\n sel.note.clefNote = clef;\r\n }\r\n sel.measure.updateClefChangeNotes();\r\n }\r\n });\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise()\r\n }\r\n /**\r\n * Modify the dynamics assoicated with the specific selection\r\n * @param selection \r\n * @param dynamic \r\n * @returns \r\n */\r\n async addDynamic(selection: SmoSelection, dynamic: SmoDynamicText): Promise {\r\n this._undoFirstMeasureSelection('add dynamic');\r\n this._removeDynamic(selection, dynamic);\r\n const equiv = this._getEquivalentSelection(selection);\r\n SmoOperation.addDynamic(selection, dynamic);\r\n SmoOperation.addDynamic(equiv!, SmoNoteModifierBase.deserialize(dynamic.serialize() as any));\r\n this.renderer.addToReplaceQueue(selection);\r\n await this.renderer.updatePromise()\r\n }\r\n /**\r\n * Remove dynamics from the selection \r\n * @param selection \r\n * @param dynamic \r\n * @returns \r\n */\r\n async _removeDynamic(selection: SmoSelection, dynamic: SmoDynamicText): Promise {\r\n const equiv = this._getEquivalentSelection(selection);\r\n if (equiv !== null && equiv.note !== null) {\r\n const altModifiers = equiv.note.getModifiers('SmoDynamicText');\r\n SmoOperation.removeDynamic(selection, dynamic);\r\n if (altModifiers.length) {\r\n SmoOperation.removeDynamic(equiv, altModifiers[0] as SmoDynamicText);\r\n }\r\n }\r\n await this.renderer.updatePromise()\r\n }\r\n /**\r\n * Remove dynamics from the current selection\r\n * @param dynamic\r\n * @returns \r\n */\r\n async removeDynamic(dynamic: SmoDynamicText): Promise {\r\n const sel = this.tracker.modifierSelections[0];\r\n if (!sel.selection) {\r\n return PromiseHelpers.emptyPromise();\r\n }\r\n this.tracker.selections = [sel.selection];\r\n this._undoFirstMeasureSelection('remove dynamic');\r\n this._removeDynamic(sel.selection, dynamic);\r\n this.renderer.addToReplaceQueue(sel.selection);\r\n await this.renderer.updatePromise()\r\n }\r\n /**\r\n * we never really delete a note, but we will convert it into a rest and if it's\r\n * already a rest we will try to hide it.\r\n * Operates on current selections\r\n * */\r\n async deleteNote(): Promise {\r\n const measureSelections = this._undoTrackerMeasureSelections('delete note');\r\n this.tracker.selections.forEach((sel) => {\r\n if (sel.note) {\r\n\r\n const altSel = this._getEquivalentSelection(sel);\r\n\r\n // set the pitch to be a good position for the rest\r\n const pitch = JSON.parse(JSON.stringify(\r\n SmoMeasure.defaultPitchForClef[sel.measure.clef]));\r\n const altPitch = JSON.parse(JSON.stringify(\r\n SmoMeasure.defaultPitchForClef[altSel!.measure.clef]));\r\n sel.note.pitches = [pitch];\r\n altSel!.note!.pitches = [altPitch];\r\n\r\n // If the note is a note, make it into a rest. If the note is a rest already,\r\n // make it invisible. If it is invisible already, make it back into a rest.\r\n if (sel.note.isRest() && !sel.note.isHidden()) {\r\n sel.note.makeHidden(true);\r\n altSel!.note!.makeHidden(true);\r\n } else {\r\n sel.note.makeRest();\r\n altSel!.note!.makeRest();\r\n sel.note.makeHidden(false);\r\n altSel!.note!.makeHidden(false);\r\n }\r\n }\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise()\r\n }\r\n /**\r\n * The lyric editor moves around, so we can't depend on the tracker for the\r\n * correct selection. We get it directly from the editor.\r\n * \r\n * @param selector - the selector of the note with the lyric to remove\r\n * @param lyric - a copy of the lyric to remove. We use the verse, parser to identify it\r\n * @returns render promise\r\n */\r\n async removeLyric(selector: SmoSelector, lyric: SmoLyric): Promise {\r\n const selection = SmoSelection.noteFromSelector(this.score, selector);\r\n if (selection === null) {\r\n return PromiseHelpers.emptyPromise();\r\n }\r\n this._undoSelection('remove lyric', selection);\r\n selection.note!.removeLyric(lyric);\r\n const equiv = this._getEquivalentSelection(selection);\r\n const storeLyric = equiv!.note!.getLyricForVerse(lyric.verse, lyric.parser);\r\n if (typeof (storeLyric) !== 'undefined') {\r\n equiv!.note!.removeLyric(lyric);\r\n }\r\n this.renderer.addToReplaceQueue(selection);\r\n lyric.deleted = true;\r\n await this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * @param selector where to add or update the lyric\r\n * @param lyric a copy of the lyric to remove\r\n * @returns \r\n */\r\n async addOrUpdateLyric(selector: SmoSelector, lyric: SmoLyric): Promise {\r\n const selection = SmoSelection.noteFromSelector(this.score, selector);\r\n if (selection === null) {\r\n return;\r\n }\r\n this._undoSelection('update lyric', selection);\r\n selection.note!.addLyric(lyric);\r\n const equiv = this._getEquivalentSelection(selection);\r\n const altLyric = SmoNoteModifierBase.deserialize(lyric.serialize() as any) as SmoLyric;\r\n equiv!.note!.addLyric(altLyric);\r\n this.renderer.addToReplaceQueue(selection);\r\n await this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * Delete all the notes for the currently selected voice\r\n * @returns \r\n */\r\n async depopulateVoice(): Promise {\r\n const measureSelections = this._undoTrackerMeasureSelections('depopulate voice');\r\n measureSelections.forEach((selection) => {\r\n const ix = selection.measure.getActiveVoice();\r\n if (ix !== 0) {\r\n SmoOperation.depopulateVoice(selection, ix);\r\n const equiv = this._getEquivalentSelection(selection);\r\n SmoOperation.depopulateVoice(equiv!, ix);\r\n }\r\n });\r\n SmoOperation.setActiveVoice(this.score, 0);\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Change the active voice in a multi-voice measure.\r\n * @param index \r\n * @returns \r\n */\r\n _changeActiveVoice(index: number): SmoSelection[] {\r\n const measuresToAdd: SmoSelection[] = [];\r\n const measureSelections = SmoSelection.getMeasureList(this.tracker.selections);\r\n measureSelections.forEach((measureSelection) => {\r\n if (index === measureSelection.measure.voices.length) {\r\n measuresToAdd.push(measureSelection);\r\n }\r\n });\r\n return measuresToAdd;\r\n }\r\n /**\r\n * Populate a new voice with default notes\r\n * @param index the voice to populate\r\n * @returns \r\n */\r\n async populateVoice(index: number): Promise {\r\n const measuresToAdd = this._changeActiveVoice(index);\r\n if (measuresToAdd.length === 0) {\r\n SmoOperation.setActiveVoice(this.score, index);\r\n this.tracker.selectActiveVoice();\r\n return this.renderer.updatePromise();\r\n }\r\n measuresToAdd.forEach((selection) => {\r\n this._undoSelection('popualteVoice', selection);\r\n SmoOperation.populateVoice(selection, index);\r\n const equiv = this._getEquivalentSelection(selection);\r\n SmoOperation.populateVoice(equiv!, index);\r\n });\r\n SmoOperation.setActiveVoice(this.score, index);\r\n this._renderChangedMeasures(measuresToAdd);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Assign an instrument to a set of measures\r\n * @param instrument the instrument to assign to the selections\r\n * @param selections \r\n * @returns \r\n */\r\n async changeInstrument(instrument: SmoInstrument, selections: SmoSelection[]): Promise {\r\n if (typeof (selections) === 'undefined') {\r\n selections = this.tracker.selections;\r\n }\r\n this._undoSelections('change instrument', selections);\r\n const altSelections = this._getEquivalentSelections(selections);\r\n SmoOperation.changeInstrument(instrument, selections);\r\n SmoOperation.changeInstrument(instrument, altSelections);\r\n this._renderChangedMeasures(selections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Set the time signature for a selection\r\n * @param timeSignature actual time signature\r\n */\r\n async setTimeSignature(timeSignature: TimeSignature): Promise {\r\n this._undoScore('Set time signature');\r\n const selections = this.tracker.selections;\r\n const altSelections = this._getEquivalentSelections(selections);\r\n SmoOperation.setTimeSignature(this.score, selections, timeSignature);\r\n SmoOperation.setTimeSignature(this.storeScore, altSelections, timeSignature);\r\n this._renderChangedMeasures(SmoSelection.getMeasureList(this.tracker.selections));\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Move selected staff up or down in the score.\r\n * @param index direction to move\r\n * @returns \r\n */\r\n async moveStaffUpDown(index: number): Promise {\r\n this._undoScore('re-order staves');\r\n // Get staff to move\r\n const selection = this._getEquivalentSelection(this.tracker.selections[0]);\r\n // Make the move in the model, and reset the view so we can see the new\r\n // arrangement\r\n SmoOperation.moveStaffUpDown(this.storeScore, selection!, index);\r\n this.viewAll();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Update the staff group for a score, which determines how the staves\r\n * are justified and bracketed\r\n * @param staffGroup \r\n */\r\n async addOrUpdateStaffGroup(staffGroup: SmoSystemGroup): Promise {\r\n this._undoScore('group staves');\r\n // Assume that the view is now set to full score\r\n this.score.addOrReplaceSystemGroup(staffGroup);\r\n this.storeScore.addOrReplaceSystemGroup(staffGroup);\r\n this.renderer.setDirty();\r\n await this.renderer.updatePromise();\r\n }\r\n async updateTabStave(tabStave: SmoTabStave) {\r\n const selections = SmoSelection.getMeasuresBetween(this.score, tabStave.startSelector, tabStave.endSelector);\r\n const altSelections = this._getEquivalentSelections(selections);\r\n if (selections.length === 0) {\r\n return;\r\n }\r\n this._undoSelections('updateTabStave', selections);\r\n const staff: number = selections[0].selector.staff;\r\n const altStaff = altSelections[0].selector.staff;\r\n const altTabStave = new SmoTabStave(tabStave.serialize());\r\n altTabStave.startSelector.staff = altStaff;\r\n altTabStave.endSelector.staff = altStaff;\r\n altTabStave.attrs.id = tabStave.attrs.id;\r\n this.score.staves[staff].updateTabStave(tabStave);\r\n this.storeScore.staves[altStaff].updateTabStave(altTabStave);\r\n this._renderChangedMeasures(SmoSelection.getMeasureList(this.tracker.selections));\r\n await this.renderer.updatePromise();\r\n }\r\n async removeTabStave() {\r\n const selections = this.tracker.selections;\r\n const altSelections = this._getEquivalentSelections(selections);\r\n if (selections.length === 0) {\r\n return;\r\n }\r\n this._undoSelections('updateTabStave', selections);\r\n const stavesToRemove: SmoTabStave[] = [];\r\n const altStavesToRemove: SmoTabStave[] = [];\r\n const added: Record = {};\r\n selections.forEach((sel, ix) => {\r\n const altSel = altSelections[ix];\r\n const tabStave = sel.staff.getTabStaveForMeasure(sel.selector);\r\n const altTabStave = altSel.staff.getTabStaveForMeasure(altSel.selector);\r\n if (tabStave && altTabStave) {\r\n if (!added[tabStave.attrs.id]) {\r\n added[tabStave.attrs.id] = tabStave;\r\n stavesToRemove.push(tabStave);\r\n altStavesToRemove.push(altTabStave);\r\n }\r\n }\r\n });\r\n selections[0].staff.removeTabStaves(stavesToRemove);\r\n altSelections[0].staff.removeTabStaves(altStavesToRemove);\r\n this._renderChangedMeasures(SmoSelection.getMeasureList(this.tracker.selections));\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Update tempo for all or part of the score\r\n * @param measure the measure with the tempo. Tempo is measure-wide parameter\r\n * @param scoreMode if true, update whole score. Else selections\r\n * @returns \r\n */\r\n async updateTempoScore(measure: SmoMeasure, tempo: SmoTempoText, scoreMode: boolean, selectionMode: boolean): Promise {\r\n let measureIndex = 0; \r\n const originalTempo = new SmoTempoText(measure.tempo);\r\n this._undoColumn('update tempo', measure.measureNumber.measureIndex);\r\n let startMeasure = measure.measureNumber.measureIndex;\r\n let endMeasure = this.score.staves[0].measures.length;\r\n let displayed = false;\r\n if (selectionMode) {\r\n const endSel = this.tracker.getExtremeSelection(1);\r\n if (endSel.selector.measure > startMeasure) {\r\n endMeasure = endSel.selector.measure;\r\n }\r\n }\r\n // If we are only changing the position of the text, it only affects the tempo measure.\r\n if (SmoTempoText.eq(originalTempo, tempo) && tempo.yOffset !== originalTempo.yOffset && endMeasure > startMeasure) {\r\n endMeasure = startMeasure + 1; \r\n }\r\n for (measureIndex = startMeasure; measureIndex < endMeasure; ++measureIndex) {\r\n if (!scoreMode && !selectionMode) {\r\n // If not whole score or selections, change until the tempo doesn't match previous measure's tempo (next tempo change)\r\n const compMeasure = this.score.staves[0].measures[measureIndex];\r\n if (SmoTempoText.eq(originalTempo, compMeasure.tempo) || displayed === false) {\r\n const sel = SmoSelection.measureSelection(this.score, 0, measureIndex);\r\n const altSel = SmoSelection.measureSelection(this.storeScore, 0, measureIndex);\r\n if (sel && sel.measure.tempo.display && !displayed) {\r\n this.renderer.addToReplaceQueue(sel);\r\n displayed = true;\r\n }\r\n if (sel) {\r\n SmoOperation.addTempo(this.score, sel, tempo);\r\n }\r\n if (altSel) {\r\n SmoOperation.addTempo(this.storeScore, altSel, tempo);\r\n }\r\n } else {\r\n break;\r\n }\r\n } else {\r\n const sel = SmoSelection.measureSelection(this.score, 0, measureIndex);\r\n const altSel = SmoSelection.measureSelection(this.storeScore, 0, measureIndex);\r\n if (sel) {\r\n SmoOperation.addTempo(this.score, sel, tempo);\r\n if (!displayed) {\r\n this.renderer.addToReplaceQueue(sel);\r\n displayed = true;\r\n }\r\n }\r\n if (altSel) {\r\n SmoOperation.addTempo(this.storeScore, altSel, tempo);\r\n }\r\n }\r\n }\r\n await this.renderer.updatePromise();\r\n }\r\n async updateTabNote(tabNote: SmoTabNote) {\r\n const selections = SmoSelection.getMeasuresBetween(this.score, \r\n this.tracker.getExtremeSelection(-1).selector, this.tracker.getExtremeSelection(1).selector);\r\n const altSelections = this._getEquivalentSelections(selections);\r\n this._undoSelections('updateTabNote', selections);\r\n SmoOperation.updateTabNote(selections, tabNote);\r\n SmoOperation.updateTabNote(altSelections, tabNote);\r\n this.renderer.addToReplaceQueue(selections);\r\n await this.renderer.updatePromise();\r\n }\r\n async removeTabNote() {\r\n const selections = SmoSelection.getMeasuresBetween(this.score, \r\n this.tracker.getExtremeSelection(-1).selector, this.tracker.getExtremeSelection(1).selector);\r\n const altSelections = this._getEquivalentSelections(selections);\r\n this._undoSelections('updateTabNote', selections);\r\n SmoOperation.removeTabNote(selections);\r\n SmoOperation.removeTabNote(altSelections);\r\n this.renderer.addToReplaceQueue(selections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * 'remove' tempo, which means either setting the bars to the \r\n * default tempo, or the previously-set tempo.\r\n * @param scoreMode whether to reset entire score\r\n */\r\n async removeTempo(measure: SmoMeasure, tempo: SmoTempoText, scoreMode: boolean, selectionMode: boolean): Promise {\r\n const startSelection = this.tracker.selections[0];\r\n if (startSelection.selector.measure > 0) {\r\n const measureIx = startSelection.selector.measure - 1;\r\n const target = startSelection.staff.measures[measureIx];\r\n const tempo = target.getTempo();\r\n const newTempo = new SmoTempoText(tempo);\r\n newTempo.display = false;\r\n this.updateTempoScore(measure, newTempo, scoreMode, selectionMode);\r\n } else {\r\n this.updateTempoScore(measure, new SmoTempoText(SmoTempoText.defaults), scoreMode, selectionMode);\r\n }\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Add a grace note to the selected real notes.\r\n */\r\n async addGraceNote(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('add grace note');\r\n selections.forEach((selection) => {\r\n const index = selection.note!.getGraceNotes().length;\r\n const pitches = JSON.parse(JSON.stringify(selection.note!.pitches));\r\n const grace = new SmoGraceNote({\r\n pitches, ticks:\r\n { numerator: 2048, denominator: 1, remainder: 0 }\r\n });\r\n SmoOperation.addGraceNote(selection, grace, index);\r\n\r\n const altPitches = JSON.parse(JSON.stringify(selection.note!.pitches));\r\n const altGrace = new SmoGraceNote({\r\n pitches: altPitches, ticks:\r\n { numerator: 2048, denominator: 1, remainder: 0 }\r\n });\r\n altGrace.attrs.id = grace.attrs.id;\r\n const altSelection = this._getEquivalentSelection(selection);\r\n SmoOperation.addGraceNote(altSelection!, altGrace, index);\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * remove selected grace note\r\n * @returns\r\n */\r\n async removeGraceNote(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('remove grace note');\r\n selections.forEach((selection) => {\r\n // TODO: get the correct offset\r\n SmoOperation.removeGraceNote(selection, 0);\r\n const altSel = (this._getEquivalentSelection(selection));\r\n SmoOperation.removeGraceNote(altSel!, 0);\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Toggle slash in stem of grace note\r\n */\r\n async slashGraceNotes(): Promise {\r\n const grace = this.tracker.getSelectedGraceNotes();\r\n const measureSelections = this._undoTrackerMeasureSelections('slash grace note toggle');\r\n grace.forEach((gn) => {\r\n SmoOperation.slashGraceNotes(gn);\r\n if (gn.selection !== null) {\r\n const altSelection = this._getEquivalentSelection(gn.selection);\r\n const altGn = this._getEquivalentGraceNote(altSelection!, gn.modifier as SmoGraceNote);\r\n SmoOperation.slashGraceNotes({\r\n selection: altSelection, modifier: altGn as any,\r\n box: SvgBox.default, index: 0\r\n });\r\n }\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n async transposeScore(offset: number): Promise {\r\n this._undoScore('transpose score');\r\n SmoOperation.transposeScore(this.score, offset);\r\n SmoOperation.transposeScore(this.storeScore, offset);\r\n this.renderer.rerenderAll();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * transpose selected notes\r\n * @param offset 1/2 steps\r\n * @returns \r\n */\r\n async transposeSelections(offset: number): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('transpose');\r\n const grace = this.tracker.getSelectedGraceNotes();\r\n if (grace.length) {\r\n grace.forEach((artifact) => {\r\n if (artifact.selection !== null && artifact.selection.note !== null) {\r\n const gn1 = artifact.modifier as SmoGraceNote;\r\n const index = artifact.selection.note.graceNotes.findIndex((x) => x.attrs.id === gn1.attrs.id);\r\n const altSelection = this._getEquivalentSelection(artifact.selection);\r\n if (altSelection && altSelection.note !== null) {\r\n const gn2 = altSelection.note.graceNotes[index];\r\n SmoOperation.transposeGraceNotes(altSelection!, [gn2], offset);\r\n }\r\n SmoOperation.transposeGraceNotes(artifact.selection, [gn1], offset);\r\n }\r\n });\r\n\r\n } else {\r\n selections.forEach((selected) => {\r\n SmoOperation.transpose(selected, offset);\r\n const altSel = this._getEquivalentSelection(selected);\r\n SmoOperation.transpose(altSel!, offset);\r\n });\r\n if (selections.length === 1 && this.score.preferences.autoPlay) {\r\n SuiOscillator.playSelectionNow(selections[0], this.score, 1);\r\n }\r\n }\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * toggle the accidental spelling of the selected notes\r\n * @returns\r\n */\r\n async toggleEnharmonic(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('toggle enharmonic');\r\n const grace = this.tracker.getSelectedGraceNotes();\r\n if (grace.length) {\r\n grace.forEach((artifact) => {\r\n SmoOperation.toggleGraceNoteEnharmonic(artifact.selection!, [artifact.modifier as SmoGraceNote]);\r\n const altSelection = this._getEquivalentSelection(artifact.selection!);\r\n const altGr = this._getEquivalentGraceNote(altSelection!, artifact.modifier as SmoGraceNote);\r\n SmoOperation.toggleGraceNoteEnharmonic(altSelection!,\r\n [altGr]);\r\n });\r\n } else {\r\n selections.forEach((selected) => {\r\n if (typeof (selected.selector.pitches) === 'undefined') {\r\n selected.selector.pitches = [];\r\n }\r\n SmoOperation.toggleEnharmonic(selected);\r\n const altSel = this._getEquivalentSelection(selected);\r\n SmoOperation.toggleEnharmonic(altSel!);\r\n });\r\n }\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * Toggle cautionary/courtesy accidentals\r\n */\r\n async toggleCourtesyAccidentals(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('toggle courtesy accidental');\r\n const grace = this.tracker.getSelectedGraceNotes();\r\n if (grace.length) {\r\n grace.forEach((artifact) => {\r\n const gn1 = [artifact.modifier] as SmoGraceNote[];\r\n SmoOperation.toggleGraceNoteCourtesy(artifact.selection, gn1);\r\n const altSel = this._getEquivalentSelection(artifact.selection!);\r\n const gn2 = this._getEquivalentGraceNote(altSel!, gn1[0]);\r\n SmoOperation.toggleGraceNoteCourtesy(altSel!, [gn2]);\r\n });\r\n } else {\r\n selections.forEach((selection) => {\r\n SmoOperation.toggleCourtesyAccidental(selection);\r\n const altSel = this._getEquivalentSelection(selection);\r\n SmoOperation.toggleCourtesyAccidental(altSel!);\r\n });\r\n }\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * change the duration of notes for selected, creating more \r\n * or fewer notes. \r\n * After the change, reset the selection so it's as close as possible \r\n * to the original length\r\n * @param operation \r\n * @returns \r\n */\r\n async batchDurationOperation(operation: BatchSelectionOperation): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('change duration');\r\n const grace = this.tracker.getSelectedGraceNotes();\r\n const graceMap: Record = {\r\n doubleDuration: 'doubleGraceNoteDuration',\r\n halveDuration: 'halveGraceNoteDuration'\r\n };\r\n if (grace.length && typeof (graceMap[operation]) !== 'undefined') {\r\n operation = graceMap[operation];\r\n grace.forEach((artifact) => {\r\n (SmoOperation as any)[operation](artifact.selection, artifact.modifier);\r\n const altSelection = this._getEquivalentSelection(artifact.selection!);\r\n const gn2 = this._getEquivalentGraceNote(altSelection!, artifact.modifier as SmoGraceNote);\r\n (SmoOperation as any)[operation](altSelection!, gn2);\r\n });\r\n } else {\r\n const altAr = this._getEquivalentSelections(selections);\r\n SmoOperation.batchSelectionOperation(this.score, selections, operation);\r\n SmoOperation.batchSelectionOperation(this.storeScore, altAr, operation);\r\n }\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Toggle selected modifier on selected notes\r\n * @param modifier \r\n * @param ctor parent class constructor (e.g. SmoOrnament)\r\n * @returns \r\n */\r\n async toggleArticulation(modifier: string, ctor: string): Promise {\r\n const measureSelections = this._undoTrackerMeasureSelections('toggle articulation');\r\n this.tracker.selections.forEach((sel) => {\r\n if (ctor === 'SmoArticulation') {\r\n const aa = new SmoArticulation({ articulation: modifier });\r\n const altAa = new SmoArticulation({ articulation: modifier });\r\n altAa.attrs.id = aa.attrs.id;\r\n SmoOperation.toggleArticulation(sel, aa);\r\n const altSelection = this._getEquivalentSelection(sel);\r\n SmoOperation.toggleArticulation(altSelection!, altAa);\r\n } else {\r\n const aa = new SmoOrnament({ ornament: modifier });\r\n const altAa = new SmoOrnament({ ornament: modifier });\r\n altAa.attrs.id = aa.attrs.id;\r\n const altSelection = this._getEquivalentSelection(sel!);\r\n SmoOperation.toggleOrnament(sel, aa);\r\n SmoOperation.toggleOrnament(altSelection!, altAa);\r\n }\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * convert non-tuplet not to a tuplet\r\n * @param numNotes 3 means triplet, etc.\r\n */\r\n async makeTuplet(numNotes: number): Promise {\r\n const selection = this.tracker.selections[0];\r\n const measureSelections = this._undoTrackerMeasureSelections('make tuplet');\r\n SmoOperation.makeTuplet(selection, numNotes);\r\n const altSelection = this._getEquivalentSelection(selection!);\r\n SmoOperation.makeTuplet(altSelection!, numNotes);\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Convert selected tuplet to a single (if possible) non-tuplet\r\n */\r\n async unmakeTuplet(): Promise {\r\n const selection = this.tracker.selections[0];\r\n const measureSelections = this._undoTrackerMeasureSelections('unmake tuplet');\r\n SmoOperation.unmakeTuplet(selection);\r\n const altSelection = this._getEquivalentSelection(selection);\r\n SmoOperation.unmakeTuplet(altSelection!);\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * Create a chord by adding an interval to selected note\r\n * @param interval 1/2 steps\r\n * @returns \r\n */\r\n async setInterval(interval: number): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('set interval');\r\n selections.forEach((selected) => {\r\n SmoOperation.interval(selected, interval);\r\n const altSelection = this._getEquivalentSelection(selected);\r\n SmoOperation.interval(altSelection!, interval);\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * change the selected chord into a single note\r\n * @returns\r\n */\r\n async collapseChord(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('collapse chord');\r\n selections.forEach((selected) => {\r\n const note: SmoNote | null = selected.note;\r\n if (note) {\r\n const pp = JSON.parse(JSON.stringify(note.pitches[0]));\r\n const altpp = JSON.parse(JSON.stringify(note.pitches[0]));\r\n // No operation for this?\r\n note.pitches = [pp];\r\n const altSelection = this._getEquivalentSelection(selected);\r\n altSelection!.note!.pitches = [altpp];\r\n }\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Toggle chicken-scratches, for jazz improv, comping etc.\r\n */\r\n async toggleSlash(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('make slash');\r\n selections.forEach((selection) => {\r\n SmoOperation.toggleSlash(selection);\r\n const altSel = this._getEquivalentSelection(selection);\r\n SmoOperation.toggleSlash(altSel!);\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * make selected notes into a rest, or visa-versa\r\n * @returns\r\n */\r\n async makeRest(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('make rest');\r\n selections.forEach((selection) => {\r\n SmoOperation.toggleRest(selection);\r\n const altSel = this._getEquivalentSelection(selection);\r\n SmoOperation.toggleRest(altSel!);\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * toggle the 'end beam' flag for selected notes\r\n * @returns \r\n */\r\n async toggleBeamGroup(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('toggle beam group');\r\n selections.forEach((selection) => {\r\n SmoOperation.toggleBeamGroup(selection);\r\n const altSel = this._getEquivalentSelection(selection);\r\n SmoOperation.toggleBeamGroup(altSel!);\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n async toggleCue() {\r\n const measureSelections = this._undoTrackerMeasureSelections('toggle note cue');\r\n this.tracker.selections.forEach((selection) => {\r\n const altSelection = this._getEquivalentSelection(selection);\r\n if (selection.note && selection.note.isRest() === false) {\r\n selection.note.isCue = !selection.note.isCue;\r\n if (altSelection && altSelection.note) {\r\n altSelection.note.isCue = selection.note.isCue;\r\n }\r\n }\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * up or down\r\n * @returns \r\n */\r\n async toggleBeamDirection(): Promise {\r\n const selections = this.tracker.selections;\r\n if (selections.length < 1) {\r\n return PromiseHelpers.emptyPromise();\r\n }\r\n const measureSelections = this._undoTrackerMeasureSelections('toggle beam direction');\r\n SmoOperation.toggleBeamDirection(selections);\r\n SmoOperation.toggleBeamDirection(this._getEquivalentSelections(selections));\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Add the selected notes to a beam group\r\n */\r\n async beamSelections(): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('beam selections');\r\n SmoOperation.beamSelections(this.score, selections);\r\n SmoOperation.beamSelections(this.storeScore, this._getEquivalentSelections(selections));\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * change key signature for selected measures\r\n * @param keySignature vex key signature\r\n */\r\n async addKeySignature(keySignature: string): Promise {\r\n const measureSelections = this._undoTrackerMeasureSelections('set key signature ' + keySignature);\r\n measureSelections.forEach((sel) => {\r\n SmoOperation.addKeySignature(this.score, sel, keySignature);\r\n const altSel = this._getEquivalentSelection(sel);\r\n SmoOperation.addKeySignature(this.storeScore, altSel!, keySignature);\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Sets a pitch from the piano widget.\r\n * @param pitch {Pitch}\r\n * @param chordPedal {boolean} - indicates we are adding to a chord\r\n */\r\n async setPitchPiano(pitch: Pitch, chordPedal: boolean): Promise {\r\n const measureSelections = this._undoTrackerMeasureSelections(\r\n 'setAbsolutePitch ' + pitch.letter + '/' + pitch.accidental);\r\n this.tracker.selections.forEach((selected) => {\r\n const npitch: Pitch = {\r\n letter: pitch.letter,\r\n accidental: pitch.accidental, octave: pitch.octave\r\n };\r\n const octave = SmoMeasure.defaultPitchForClef[selected.measure.clef].octave;\r\n npitch.octave += octave;\r\n const altSel = this._getEquivalentSelection(selected);\r\n if (chordPedal && selected.note) {\r\n selected.note.toggleAddPitch(npitch);\r\n altSel!.note!.toggleAddPitch(npitch);\r\n } else {\r\n SmoOperation.setPitch(selected, [npitch]);\r\n SmoOperation.setPitch(altSel!, [npitch]);\r\n }\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * show or hide the piano widget\r\n * @param value to show it\r\n */\r\n async showPiano(value: boolean): Promise {\r\n this.score.preferences.showPiano = value;\r\n this.storeScore.preferences.showPiano = value;\r\n if (value) {\r\n SuiPiano.showPiano();\r\n } else {\r\n SuiPiano.hidePiano();\r\n }\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Render a pitch for each letter name-pitch in the string,\r\n * @param pitches letter names for pitches\r\n * @returns promise, resolved when all pitches rendered\r\n * @see setPitch\r\n */\r\n async setPitchesPromise(pitches: PitchLetter[]): Promise {\r\n const self = this;\r\n const promise = new Promise((resolve: any) => {\r\n const fc = async (index: number) => {\r\n if (index >= pitches.length) {\r\n resolve();\r\n } else {\r\n await self.setPitch(pitches[index]);\r\n fc(index + 1);\r\n }\r\n };\r\n fc(0);\r\n });\r\n await promise;\r\n }\r\n\r\n /**\r\n * Add a pitch to the score at the cursor. This tries to find the best pitch\r\n * to match the letter key (F vs F# for instance) based on key and surrounding notes\r\n * @param letter string\r\n */\r\n async setPitch(letter: PitchLetter): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('set pitch ' + letter);\r\n selections.forEach((selected) => {\r\n const selector = selected.selector;\r\n let hintSel = SmoSelection.lastNoteSelectionNonRest(this.score,\r\n selector.staff, selector.measure, selector.voice, selector.tick);\r\n if (!hintSel) {\r\n hintSel = SmoSelection.nextNoteSelectionNonRest(this.score,\r\n selector.staff, selector.measure, selector.voice, selector.tick);\r\n }\r\n // The selection no longer exists, possibly deleted\r\n if (hintSel === null || hintSel.note === null) {\r\n return PromiseHelpers.emptyPromise();\r\n }\r\n const pitch = SmoMusic.getLetterNotePitch(hintSel.note.pitches[0],\r\n letter, hintSel.measure.keySignature);\r\n SmoOperation.setPitch(selected, [pitch]);\r\n const altSel = this._getEquivalentSelection(selected);\r\n SmoOperation.setPitch(altSel!, [pitch]);\r\n if (this.score.preferences.autoAdvance) {\r\n this.tracker.moveSelectionRight(this.score, null, true);\r\n }\r\n });\r\n if (selections.length === 1 && this.score.preferences.autoPlay) {\r\n SuiOscillator.playSelectionNow(selections[0], this.score, 1);\r\n }\r\n this._renderChangedMeasures(measureSelections);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Generic clipboard copy action\r\n */\r\n async copy(): Promise {\r\n this.pasteBuffer.setSelections(this.score, this.tracker.selections);\r\n const altAr: SmoSelection[] = [];\r\n this.tracker.selections.forEach((sel) => {\r\n const noteSelection = this._getEquivalentSelection(sel);\r\n if (noteSelection !== null) {\r\n altAr.push(noteSelection);\r\n }\r\n });\r\n this.storePaste.setSelections(this.storeScore, altAr);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * clipboard paste action\r\n * @returns \r\n */\r\n async paste(): Promise {\r\n // We undo the whole score on a paste, since we don't yet know the\r\n // extent of the overlap\r\n this._undoScore('paste');\r\n this.renderer.preserveScroll();\r\n const firstSelection = this.tracker.selections[0];\r\n const pasteTarget = firstSelection.selector;\r\n const altSelection = this._getEquivalentSelection(firstSelection);\r\n const altTarget = altSelection!.selector;\r\n this.pasteBuffer.pasteSelections(pasteTarget);\r\n this.storePaste.pasteSelections(altTarget);\r\n this._renderChangedMeasures(this.pasteBuffer.replacementMeasures);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * specify a note head other than the default for the duration\r\n * @param head \r\n */\r\n async setNoteHead(head: string): Promise {\r\n const selections = this.tracker.selections;\r\n const measureSelections = this._undoTrackerMeasureSelections('set note head');\r\n SmoOperation.setNoteHead(selections, head);\r\n SmoOperation.setNoteHead(this._getEquivalentSelections(selections), head);\r\n this._renderChangedMeasures(measureSelections);\r\n return this.renderer.updatePromise();\r\n }\r\n\r\n /**\r\n * Add a volta for selected measures\r\n */\r\n async addEnding(): Promise {\r\n // TODO: we should have undo for columns\r\n this._undoScore('Add Volta');\r\n const ft = this.tracker.getExtremeSelection(-1);\r\n const tt = this.tracker.getExtremeSelection(1);\r\n const params = SmoVolta.defaults;\r\n params.startBar = ft.selector.measure;\r\n params.endBar = tt.selector.measure;\r\n params.number = 1;\r\n const volta = new SmoVolta(params);\r\n const altVolta = new SmoVolta(params);\r\n this._renderChangedMeasures([ft, tt]);\r\n SmoOperation.addEnding(this.storeScore, altVolta);\r\n SmoOperation.addEnding(this.score, volta);\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * @param ending volta settings\r\n * @returns \r\n */\r\n async updateEnding(ending: SmoVolta): Promise {\r\n this._undoScore('Change Volta');\r\n ending.elements.forEach((el) => {\r\n $(el).find('g.' + ending.attrs.id).remove();\r\n });\r\n ending.elements = [];\r\n SmoOperation.removeEnding(this.storeScore, ending);\r\n SmoOperation.removeEnding(this.score, ending);\r\n const altVolta = new SmoVolta(ending);\r\n SmoOperation.addEnding(this.storeScore, altVolta);\r\n SmoOperation.addEnding(this.score, ending);\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * \r\n * @param ending volta to remove\r\n * @returns \r\n */\r\n async removeEnding(ending: SmoVolta): Promise {\r\n this._undoScore('Remove Volta');\r\n ending.elements.forEach((el) => {\r\n $(el).find('g.' + ending.attrs.id).remove();\r\n });\r\n ending.elements = [];\r\n SmoOperation.removeEnding(this.storeScore, ending);\r\n SmoOperation.removeEnding(this.score, ending);\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * \r\n * @param position begin or end\r\n * @param barline barline type\r\n * @returns \r\n */\r\n async setBarline(position: number, barline: number): Promise {\r\n const obj = new SmoBarline({ position, barline });\r\n const altObj = new SmoBarline({ position, barline });\r\n const selection = this.tracker.selections[0];\r\n this._undoColumn('set barline', selection.selector.measure);\r\n SmoOperation.setMeasureBarline(this.score, selection, obj);\r\n const altSel = this._getEquivalentSelection(selection);\r\n SmoOperation.setMeasureBarline(this.storeScore, altSel!, altObj);\r\n this._renderChangedMeasures([selection]);\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * \r\n * @param position start or end\r\n * @param symbol coda, etc.\r\n */\r\n async setRepeatSymbol(position: number, symbol: number): Promise {\r\n const params = SmoRepeatSymbol.defaults;\r\n params.position = position;\r\n params.symbol = symbol;\r\n const obj = new SmoRepeatSymbol(params);\r\n const altObj = new SmoRepeatSymbol(params);\r\n const selection = this.tracker.selections[0];\r\n this._undoColumn('set repeat symbol', selection.selector.measure);\r\n SmoOperation.setRepeatSymbol(this.score, selection, obj);\r\n const altSel = this._getEquivalentSelection(selection);\r\n SmoOperation.setRepeatSymbol(this.storeScore, altSel!, altObj);\r\n this._renderChangedMeasures([selection]);\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * toggle rehearsal mark on first selected measure\r\n * @returns\r\n */\r\n async toggleRehearsalMark(): Promise {\r\n const selection = this.tracker.getExtremeSelection(-1);\r\n const altSelection = this._getEquivalentSelection(selection);\r\n const cmd = selection.measure.getRehearsalMark() ? 'removeRehearsalMark' : 'addRehearsalMark';\r\n SmoOperation[cmd](this.score, selection, new SmoRehearsalMark(SmoRehearsalMark.defaults));\r\n SmoOperation[cmd](this.storeScore, altSelection!, new SmoRehearsalMark(SmoRehearsalMark.defaults));\r\n this._renderChangedMeasures([selection]);\r\n return this.renderer.updatePromise();\r\n }\r\n _removeStaffModifier(modifier: StaffModifierBase) {\r\n this.score.staves[modifier.associatedStaff].removeStaffModifier(modifier);\r\n const altModifier = StaffModifierBase.deserialize(modifier.serialize());\r\n altModifier.startSelector = this._getEquivalentSelector(altModifier.startSelector);\r\n altModifier.endSelector = this._getEquivalentSelector(altModifier.endSelector);\r\n this.storeScore.staves[this._getEquivalentStaff(modifier.associatedStaff)].removeStaffModifier(altModifier);\r\n }\r\n /**\r\n * Remove selected modifier\r\n * @param modifier slur, hairpin, etc.\r\n * @returns \r\n */\r\n async removeStaffModifier(modifier: StaffModifierBase): Promise {\r\n this._undoStaffModifier('Set measure proportion', modifier,\r\n UndoBuffer.bufferSubtypes.REMOVE);\r\n this._removeStaffModifier(modifier);\r\n this._renderRectangle(modifier.startSelector, modifier.endSelector);\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Change a staff modifier\r\n * @param original original version\r\n * @param modifier modified version\r\n * @returns \r\n */\r\n async addOrUpdateStaffModifier(original: StaffModifierBase, modifier: StaffModifierBase): Promise {\r\n if (!modifier) {\r\n if (original) {\r\n // Handle legacy API changed\r\n modifier = StaffModifierBase.deserialize(original);\r\n } else {\r\n console.warn('update modifier: bad modifier');\r\n return PromiseHelpers.emptyPromise();\r\n }\r\n }\r\n const existing = this.score.staves[modifier.startSelector.staff]\r\n .getModifier(modifier);\r\n const subtype = existing === null ? UndoBuffer.bufferSubtypes.ADD :\r\n UndoBuffer.bufferSubtypes.UPDATE;\r\n this._undoStaffModifier('Set measure proportion', original,\r\n subtype);\r\n this._removeStaffModifier(modifier);\r\n const copy = StaffModifierBase.deserialize(modifier.serialize());\r\n copy.startSelector = this._getEquivalentSelector(copy.startSelector);\r\n copy.endSelector = this._getEquivalentSelector(copy.endSelector);\r\n const sel = SmoSelection.noteFromSelector(this.score, modifier.startSelector);\r\n if (sel !== null) {\r\n const altSel = this._getEquivalentSelection(sel);\r\n SmoOperation.addStaffModifier(sel, modifier);\r\n SmoOperation.addStaffModifier(altSel!, copy);\r\n const modId = 'mod-' + sel.selector.staff + '-' + sel.selector.measure;\r\n const context = this.renderer.renderer.getRenderer(sel.measure.svg.logicalBox);\r\n if (context) {\r\n SvgHelpers.removeElementsByClass(context.svg, modId);\r\n }\r\n }\r\n this._renderRectangle(modifier.startSelector, modifier.endSelector);\r\n return this.renderer.updatePromise();\r\n }\r\n _lineOperation(op: string) {\r\n // if (this.tracker.selections.length < 2) {\r\n // return;\r\n // }\r\n const measureSelections = this._undoTrackerMeasureSelections(op);\r\n const ft = this.tracker.getExtremeSelection(-1);\r\n const tt = this.tracker.getExtremeSelection(1);\r\n const ftAlt = this._getEquivalentSelection(ft);\r\n const ttAlt = this._getEquivalentSelection(tt);\r\n const modifier = (SmoOperation as any)[op](ft, tt);\r\n (SmoOperation as any)[op](ftAlt, ttAlt);\r\n this._undoStaffModifier('add ' + op, modifier, UndoBuffer.bufferSubtypes.ADD);\r\n this._renderChangedMeasures(measureSelections);\r\n }\r\n /**\r\n * Add crescendo to selection\r\n */\r\n async crescendo(): Promise {\r\n this._lineOperation('crescendo');\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Add crescendo to selection\r\n */\r\n async crescendoBracket(): Promise {\r\n this._lineOperation('crescendoBracket');\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Add crescendo to selection\r\n */\r\n async dimenuendo(): Promise {\r\n this._lineOperation('dimenuendo');\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Add crescendo to selection\r\n */\r\n async accelerando(): Promise {\r\n this._lineOperation('accelerando');\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Add crescendo to selection\r\n */\r\n async ritard(): Promise {\r\n this._lineOperation('ritard');\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * diminuendo selections\r\n * @returns \r\n */\r\n async decrescendo(): Promise {\r\n this._lineOperation('decrescendo');\r\n return this.renderer.updatePromise();\r\n }\r\n async removeTextBracket(bracket: SmoStaffTextBracket): Promise {\r\n return this.removeStaffModifier(bracket);\r\n }\r\n async addOrReplaceTextBracket(modifier: SmoStaffTextBracket) {\r\n const from1 = SmoSelection.noteFromSelector(this.score, modifier.startSelector);\r\n const to1 = SmoSelection.noteFromSelector(this.score, modifier.endSelector);\r\n if (from1 === null || to1 === null) {\r\n return;\r\n }\r\n const altFrom = this._getEquivalentSelection(from1);\r\n const altTo = this._getEquivalentSelection(to1);\r\n if (altFrom === null || altTo === null) {\r\n return;\r\n }\r\n SmoOperation.addOrReplaceBracket(modifier, from1, to1);\r\n SmoOperation.addOrReplaceBracket(modifier, altFrom, altTo);\r\n const redraw = SmoSelection.getMeasuresBetween(this.score, from1.selector, to1.selector);\r\n this._undoStaffModifier('add repl text bracket', modifier, UndoBuffer.bufferSubtypes.ADD);\r\n this._renderChangedMeasures(redraw);\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Slur selected notes\r\n * @returns\r\n */\r\n async slur(): Promise {\r\n const measureSelections = this._undoTrackerMeasureSelections('slur');\r\n const ft = this.tracker.getExtremeSelection(-1);\r\n const tt = this.tracker.getExtremeSelection(1);\r\n const ftAlt = this._getEquivalentSelection(ft);\r\n const ttAlt = this._getEquivalentSelection(tt);\r\n const modifier = SmoOperation.slur(this.score, ft, tt);\r\n const altModifier = SmoOperation.slur(this.storeScore, ftAlt!, ttAlt!);\r\n this._undoStaffModifier('add ' + 'op', new SmoSlur(modifier), UndoBuffer.bufferSubtypes.ADD);\r\n this._renderChangedMeasures(measureSelections);\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * tie selected notes\r\n * @returns \r\n */\r\n async tie(): Promise {\r\n this._lineOperation('tie');\r\n return this.renderer.updatePromise();\r\n }\r\n async updateZoom(zoomFactor: number): Promise {\r\n const original = this.score.layoutManager!.getGlobalLayout();\r\n original.zoomScale = zoomFactor;\r\n this.score.layoutManager!.globalLayout.zoomScale = zoomFactor;\r\n this.renderer.pageMap.updateZoom(zoomFactor);\r\n this.renderer.pageMap.updateContainerOffset(this.scroller.scrollState);\r\n }\r\n /**\r\n * set global page for score, zoom etc.\r\n * @param layout global SVG settings\r\n * @returns \r\n */\r\n async setGlobalLayout(layout: SmoGlobalLayout): Promise {\r\n this._undoScore('Set Global Layout');\r\n const original = this.score.layoutManager!.getGlobalLayout().svgScale;\r\n this.score.layoutManager!.updateGlobalLayout(layout);\r\n this.score.scaleTextGroups(original / layout.svgScale);\r\n this.storeScore.layoutManager!.updateGlobalLayout(layout);\r\n this.renderer.rerenderAll();\r\n return this.renderer.preserveScroll();\r\n }\r\n /**\r\n * Set the layout of a single page\r\n * @param layout page layout\r\n * @param pageIndex which page to change\r\n * @returns \r\n */\r\n async setPageLayout(layout: SmoPageLayout, pageIndex: number) {\r\n this.score.layoutManager!.updatePage(layout, pageIndex);\r\n this.storeScore.layoutManager!.updatePage(layout, pageIndex);\r\n // If we are in part mode, save the page layout in the part so it is there next time\r\n // the part is exposed.\r\n if (this.isPartExposed()) {\r\n this.score.staves.forEach((staff, staffIx) => {\r\n staff.partInfo.layoutManager.updatePage(layout, pageIndex);\r\n const altStaff = this.storeScore.staves[this.staffMap[staffIx]];\r\n altStaff.partInfo.layoutManager.updatePage(layout, pageIndex);\r\n });\r\n }\r\n await this.refreshViewport();\r\n }\r\n async setPageLayouts(layout: SmoPageLayout, startIndex: number, endIndex: number) {\r\n this._undoScore('Set Page Layout');\r\n let i = 0;\r\n for (i = startIndex; i <= endIndex; ++i) {\r\n this.setPageLayout(layout, i);\r\n }\r\n this.renderer.rerenderAll();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Update the music font\r\n * @param family \r\n * @returns \r\n */\r\n async setEngravingFontFamily(family: engravingFontType): Promise {\r\n this.score.engravingFont = family;\r\n this.storeScore.engravingFont = family;\r\n this.renderer.notifyFontChange(); \r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Upate global font used for chord changes\r\n * @param fontInfo\r\n * @returns \r\n */\r\n async setChordFont(fontInfo: FontInfo): Promise {\r\n this._undoScore('Set Chord Font');\r\n this.score.setChordFont(fontInfo);\r\n this.storeScore.setChordFont(fontInfo);\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Update font used for lyrics\r\n * @param fontInfo \r\n * @returns \r\n */\r\n async setLyricFont(fontInfo: FontInfo): Promise {\r\n this._undoScore('Set Lyric Font');\r\n this.score.setLyricFont(fontInfo);\r\n this.storeScore.setLyricFont(fontInfo);\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * @param value if false, lyric widths don't affect measure width\r\n * @returns \r\n */\r\n async setLyricAdjustWidth(value: boolean): Promise {\r\n this._undoScore('Set Lyric Adj Width');\r\n this.score.setLyricAdjustWidth(value);\r\n this.storeScore.setLyricAdjustWidth(value);\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * delete selected measures\r\n * @returns \r\n */\r\n async deleteMeasure(): Promise {\r\n this._undoScore('Delete Measure');\r\n if (this.storeScore.staves[0].measures.length < 2) {\r\n return PromiseHelpers.emptyPromise();\r\n }\r\n const selections = SmoSelection.getMeasureList(this.tracker.selections);\r\n // THe measures get renumbered, so keep the index at 0\r\n const index = selections[0].selector.measure;\r\n for (var i = 0; i < selections.length; ++i) {\r\n // Unrender the deleted measure\r\n this.score.staves.forEach((staff) => {\r\n this.tracker.clearMeasureMap(staff.measures[index]);\r\n this.renderer.unrenderMeasure(staff.measures[index]);\r\n this.renderer.unrenderMeasure(staff.measures[staff.measures.length - 1]);\r\n // A little hacky - delete the modifiers if they start or end on\r\n // the measure\r\n staff.renderableModifiers.forEach((modifier) => {\r\n if (modifier.startSelector.measure === index || modifier.endSelector.measure === index) {\r\n if (modifier.logicalBox) {\r\n const context = this.renderer.renderer.getRenderer(modifier.logicalBox);\r\n if (context) {\r\n $(context.svg).find('g.' + modifier.attrs.id).remove();\r\n }\r\n }\r\n }\r\n });\r\n });\r\n // Remove the SVG artifacts mapped to this measure.\r\n this.score.deleteMeasure(index);\r\n this.storeScore.deleteMeasure(index);\r\n // Note: index doesn't increment since there are now 1 fewer measures\r\n };\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * add number of measures, with default notes selections\r\n * @param append \r\n * @param numberToAdd \r\n * @returns \r\n */\r\n async addMeasures(append: boolean, numberToAdd: number): Promise {\r\n let pos = 0;\r\n let ix = 0;\r\n this._undoScore('Add Measure');\r\n for (ix = 0; ix < numberToAdd; ++ix) {\r\n const measure = this.tracker.getFirstMeasureOfSelection();\r\n if (measure) {\r\n const nmeasure = SmoMeasure.getDefaultMeasureWithNotes(measure);\r\n pos = measure.measureNumber.measureIndex;\r\n if (append) {\r\n pos += 1;\r\n }\r\n nmeasure.measureNumber.measureIndex = pos;\r\n nmeasure.setActiveVoice(0);\r\n this.score.addMeasure(pos);\r\n this.storeScore.addMeasure(pos);\r\n }\r\n }\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * add a single measure before or after selection\r\n * @param append \r\n * @returns \r\n */\r\n async addMeasure(append: boolean): Promise {\r\n this._undoScore('Add Measure');\r\n let pos = 0;\r\n const measure = this.tracker.getFirstMeasureOfSelection();\r\n if (!measure) {\r\n return;\r\n }\r\n const nmeasure = SmoMeasure.getDefaultMeasureWithNotes(measure);\r\n pos = measure.measureNumber.measureIndex;\r\n if (append) {\r\n pos += 1;\r\n }\r\n nmeasure.measureNumber.measureIndex = pos;\r\n nmeasure.setActiveVoice(0);\r\n this.score.addMeasure(pos);\r\n this.storeScore.addMeasure(pos);\r\n this.renderer.clearLine(measure);\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * remove an entire line of music\r\n * @returns\r\n */\r\n async removeStaff(): Promise {\r\n this._undoScore('Remove Instrument');\r\n if (this.storeScore.staves.length < 2 || this.score.staves.length < 2) {\r\n return PromiseHelpers.emptyPromise();\r\n }\r\n // if we are looking at a subset of the score,\r\n // revert to the full score view before removing the staff.\r\n const sel = this.tracker.selections[0];\r\n const scoreSel = this._getEquivalentSelection(sel);\r\n const staffIndex = scoreSel!.selector.staff;\r\n SmoOperation.removeStaff(this.storeScore, staffIndex);\r\n this.viewAll();\r\n this.renderer.setRefresh();\r\n return this.renderer.updatePromise();\r\n }\r\n async addStaff(instrument: SmoSystemStaffParams): Promise {\r\n this._undoScore('Add Instrument');\r\n // if we are looking at a subset of the score, we won't see the new staff. So\r\n // revert to the full view\r\n const staff = SmoOperation.addStaff(this.storeScore, instrument);\r\n const instKeys = Object.keys(staff.measureInstrumentMap);\r\n // update the key signatures for the new part\r\n instKeys.forEach((key) => {\r\n const numKey = parseInt(key, 10);\r\n const inst = staff.measureInstrumentMap[numKey];\r\n const selections = SmoSelection.innerSelections(this.storeScore, inst.startSelector, inst.endSelector);\r\n SmoOperation.changeInstrument(inst, selections);\r\n })\r\n if (instrument.staffId > 0) {\r\n const selection = SmoSelection.measureSelection(this.storeScore, instrument.staffId - 1, 0);\r\n const sel = SmoSelector.default;\r\n sel.staff = instrument.staffId - 1;\r\n if (selection) {\r\n let grp = this.storeScore.getSystemGroupForStaff(selection);\r\n if (grp) {\r\n grp.endSelector.staff = instrument.staffId;\r\n } else {\r\n let grp = new SmoSystemGroup(SmoSystemGroup.defaults);\r\n grp.startSelector.staff = instrument.staffId - 1;\r\n grp.endSelector.staff = instrument.staffId;\r\n this.storeScore.systemGroups.push(grp);\r\n }\r\n }\r\n }\r\n this.viewAll();\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Update part info assumes that the part is currently exposed - that\r\n * staff 0 is the first staff in the part prior to editing.\r\n * @param info\r\n */\r\n async updatePartInfo(info: SmoPartInfo): Promise {\r\n let i: number = 0;\r\n this._undoScore('Update part info');\r\n const storeStaff = this.staffMap[0] - info.stavesBefore;\r\n const partLength = info.stavesBefore + info.stavesAfter + 1;\r\n const resetView = !SmoLayoutManager.areLayoutsEqual(info.layoutManager.getGlobalLayout(), this.score.layoutManager!.getGlobalLayout());\r\n const restChange = this.score.staves[0].partInfo.expandMultimeasureRests != info.expandMultimeasureRests;\r\n const stavesChange = this.score.staves[0].partInfo.stavesAfter !== info.stavesAfter;\r\n for (i = 0; i < partLength; ++i) {\r\n const nStaffIndex = storeStaff + i;\r\n const nInfo = new SmoPartInfo(info);\r\n nInfo.stavesBefore = i;\r\n nInfo.stavesAfter = partLength - i - 1;\r\n this.storeScore.staves[nStaffIndex].partInfo = nInfo;\r\n // If the staff index is currently displayed, \r\n const displayedIndex = this.staffMap.findIndex((x) => x === nStaffIndex);\r\n if (displayedIndex >= 0) {\r\n this.score.staves[displayedIndex].partInfo = new SmoPartInfo(nInfo);\r\n this.score.layoutManager = nInfo.layoutManager;\r\n }\r\n }\r\n if (resetView || restChange || stavesChange) {\r\n SmoOperation.computeMultipartRest(this.score);\r\n // this.resetPartView();\r\n this.renderer.rerenderAll()\r\n }\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * A simpler API for applications to add a new staff to the score.\r\n * @param params - the instrument, which determines clef, etc.\r\n * @returns \r\n */\r\n async addStaffSimple(params: Partial): Promise {\r\n const instrumentParams = SmoInstrument.defaults;\r\n instrumentParams.startSelector.staff = instrumentParams.endSelector.staff = this.score.staves.length;\r\n instrumentParams.clef = params.clef ?? instrumentParams.clef;\r\n\r\n const staffParams = SmoSystemStaff.defaults;\r\n staffParams.staffId = this.storeScore.staves.length; // add a staff\r\n staffParams.measureInstrumentMap[0] = new SmoInstrument(instrumentParams);\r\n this.addStaff(staffParams);\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Save the score to local storage.\r\n */\r\n quickSave() {\r\n const scoreStr = JSON.stringify(this.storeScore.serialize());\r\n localStorage.setItem(smoSerialize.localScore, scoreStr);\r\n }\r\n updateRepeatCount(count: number) {\r\n const measureSelections = this._undoTrackerMeasureSelections('repeat bar');\r\n const symbol = count > 0 ? true : false; \r\n measureSelections.forEach((ms) => {\r\n const store = this._getEquivalentSelection(ms);\r\n ms.measure.repeatCount = count;\r\n ms.measure.repeatSymbol = symbol;\r\n if (store) {\r\n store.measure.repeatCount = count;\r\n store.measure.repeatSymbol = symbol;\r\n }\r\n });\r\n this._renderChangedMeasures(measureSelections);\r\n return this.updatePromise();\r\n }\r\n /**\r\n * Update the measure formatting parameters for the current selection\r\n * @param format generic measure formatting parameters\r\n * @returns \r\n */\r\n setMeasureFormat(format: SmoMeasureFormat): Promise {\r\n const label = 'set measure format';\r\n const fromSelector = this.tracker.getExtremeSelection(-1).selector;\r\n const toSelector = this.tracker.getExtremeSelection(1).selector;\r\n const measureSelections = this.tracker.getSelectedMeasures();\r\n // If the formatting is on a part, preserve it in the part's info\r\n const isPart = this.isPartExposed();\r\n measureSelections.forEach((m) => {\r\n this._undoColumn(label, m.selector.measure);\r\n SmoOperation.setMeasureFormat(this.score, m, format);\r\n if (isPart) {\r\n m.staff.partInfo.measureFormatting[m.measure.measureNumber.measureIndex] = new SmoMeasureFormat(format);\r\n }\r\n const alt = this._getEquivalentSelection(m);\r\n SmoOperation.setMeasureFormat(this.storeScore, alt!, format);\r\n if (isPart) {\r\n alt!.staff.partInfo.measureFormatting[m.measure.measureNumber.measureIndex] = new SmoMeasureFormat(format);\r\n }\r\n });\r\n this._renderRectangle(fromSelector, toSelector);\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Remove system breaks from the measure formatting for selected measures\r\n * @returns \r\n */\r\n removeSystemBreaks(): Promise {\r\n const label = 'set measure format';\r\n const fromSelector = this.tracker.getExtremeSelection(-1).selector;\r\n const toSelector = this.tracker.getExtremeSelection(1).selector;\r\n const measureSelections = this.tracker.getSelectedMeasures();\r\n // If the formatting is on a part, preserve it in the part's info\r\n const isPart = this.isPartExposed();\r\n measureSelections.forEach((m) => {\r\n this._undoColumn(label, m.selector.measure);\r\n const format = new SmoMeasureFormat(m.measure.format);\r\n format.systemBreak = false;\r\n SmoOperation.setMeasureFormat(this.score, m, format);\r\n if (isPart) {\r\n m.staff.partInfo.measureFormatting[m.measure.measureNumber.measureIndex] = new SmoMeasureFormat(format);\r\n }\r\n const alt = this._getEquivalentSelection(m);\r\n SmoOperation.setMeasureFormat(this.storeScore, alt!, format);\r\n if (isPart) {\r\n alt!.staff.partInfo.measureFormatting[m.measure.measureNumber.measureIndex] = new SmoMeasureFormat(format);\r\n }\r\n });\r\n this._renderRectangle(fromSelector, toSelector);\r\n return this.renderer.updatePromise();\r\n }\r\n renumberMeasures(measureIndex: number, localIndex: number) {\r\n this.score.updateRenumberingMap(measureIndex, localIndex);\r\n this.storeScore.updateRenumberingMap(measureIndex, localIndex);\r\n const mmsel = SmoSelection.measureSelection(this.score, 0, measureIndex);\r\n if (mmsel) {\r\n this._renderChangedMeasures([mmsel]);\r\n }\r\n return this.renderer.updatePromise();\r\n }\r\n /**\r\n * Play the music from the starting selection\r\n * @returns \r\n */\r\n playFromSelection(): void {\r\n var mm = this.tracker.getExtremeSelection(-1);\r\n if (SuiAudioPlayer.playingInstance && SuiAudioPlayer.playingInstance.paused) {\r\n SuiAudioPlayer.playingInstance.play();\r\n return;\r\n }\r\n if (SuiAudioPlayer.playing) {\r\n return;\r\n }\r\n new SuiAudioPlayer({ audioAnimation: this.audioAnimation, score: this.score, startIndex: mm.selector.measure, view: this }).play();\r\n }\r\n stopPlayer() {\r\n SuiAudioPlayer.stopPlayer();\r\n }\r\n pausePlayer() {\r\n SuiAudioPlayer.pausePlayer();\r\n }\r\n\r\n /**\r\n * Proxy calls to move the tracker parameters according to the\r\n * rules of the 'Home' key (depending on shift/ctrl/alt)\r\n * @param ev \r\n * @returns \r\n */\r\n async moveHome(ev: KeyEvent): Promise {\r\n this.tracker.moveHome(this.score, ev);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Proxy calls to move the tracker parameters according to the\r\n * rules of the 'End' key (depending on shift/ctrl/alt)\r\n * @param ev \r\n * @returns \r\n */\r\n async moveEnd(ev: KeyEvent): Promise {\r\n this.tracker.moveEnd(this.score, ev);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Grow the current selection by one to the left, if possible\r\n * @param ev \r\n * @returns \r\n */\r\n async growSelectionLeft(): Promise {\r\n this.tracker.growSelectionLeft();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Grow the current selection by one to the right, if possible\r\n * @param ev \r\n * @returns \r\n */\r\n async growSelectionRight(): Promise {\r\n this.tracker.growSelectionRight();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Select the next tabbable modifier near one of the selected notes\r\n * @param keyEv \r\n * @returns \r\n */\r\n async advanceModifierSelection(keyEv: KeyEvent): Promise {\r\n this.tracker.advanceModifierSelection(this.score, keyEv);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Select the next entire measure, if possible\r\n * @returns \r\n */\r\n async growSelectionRightMeasure(): Promise {\r\n this.tracker.growSelectionRightMeasure();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Advance cursor forwards, if possible\r\n * @param ev \r\n * @returns \r\n */\r\n async moveSelectionRight(ev: KeyEvent): Promise {\r\n this.tracker.moveSelectionRight(this.score, ev, true);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Advance cursor backwards, if possible\r\n * @param ev \r\n * @returns \r\n */\r\n async moveSelectionLeft(): Promise {\r\n this.tracker.moveSelectionLeft();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Advance cursor back entire measure, if possible\r\n * @returns \r\n */\r\n async moveSelectionLeftMeasure(): Promise {\r\n this.tracker.moveSelectionLeftMeasure();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Advance cursor forward one measure, if possible\r\n * @returns \r\n */\r\n async moveSelectionRightMeasure(): Promise {\r\n this.tracker.moveSelectionRightMeasure();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Move cursor to a higher pitch in the current chord, with wrap\r\n * @returns \r\n */\r\n async moveSelectionPitchUp(): Promise {\r\n this.tracker.moveSelectionPitchUp();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Move cursor to a lower pitch in the current chord, with wrap\r\n */\r\n async moveSelectionPitchDown(): Promise {\r\n this.tracker.moveSelectionPitchDown();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Move cursor up a staff in the system, if possible\r\n * @returns \r\n */\r\n async moveSelectionUp(): Promise {\r\n this.tracker.moveSelectionUp();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Move cursor down a staff in the system, if possible\r\n * @returns \r\n */\r\n async moveSelectionDown(): Promise {\r\n this.tracker.moveSelectionDown();\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Set the current suggestions (hover element) as the selection\r\n * @returns \r\n */\r\n async selectSuggestion(evData: KeyEvent): Promise {\r\n this.tracker.selectSuggestion(this.score, evData);\r\n await this.renderer.updatePromise();\r\n }\r\n /**\r\n * Find an element at the given box, and make it the current selection\r\n * */\r\n async intersectingArtifact(evData: SvgBox): Promise {\r\n this.tracker.intersectingArtifact(evData);\r\n await this.renderer.updatePromise();\r\n } \r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SvgHelpers } from './svgHelpers';\r\nimport { SvgBox, SvgPoint } from '../../smo/data/common';\r\nimport { SvgPageMap } from './svgPageMap';\r\nimport { layoutDebug } from './layoutDebug';\r\ndeclare var $: any;\r\n\r\n/**\r\n * Respond to scroll events in music DOM, and handle the scroll of the viewport\r\n * @category SuiRender\r\n */\r\nexport class SuiScroller {\r\n selector: HTMLElement;\r\n svgPages: SvgPageMap;\r\n _scroll: SvgPoint;\r\n _offsetInitial: SvgPoint;\r\n viewport: SvgBox = SvgBox.default;\r\n logicalViewport: SvgBox = SvgBox.default;\r\n scrolling: boolean = false;\r\n // ### constructor\r\n // selector is the scrollable DOM container of the music container\r\n // (grandparent of svg element)\r\n constructor(selector: HTMLElement, svgPages: SvgPageMap) {\r\n const self = this;\r\n this.selector = selector;\r\n this._scroll = { x: 0, y: 0 };\r\n this.svgPages = svgPages;\r\n const scroller = $(selector);\r\n this._offsetInitial = { x: $(scroller).offset().left, y: $(scroller).offset().top };\r\n }\r\n\r\n get scrollState(): SvgPoint {\r\n return { x: this._scroll.x, y: this._scroll.y };\r\n }\r\n restoreScrollState(state: SvgPoint) {\r\n this.scrollOffset(state.x - this._scroll.x, state.y - this._scroll.y);\r\n this.deferUpdateDebug();\r\n }\r\n\r\n // ### handleScroll\r\n // update viewport in response to scroll events\r\n handleScroll(x: number, y: number) {\r\n this._scroll = { x, y };\r\n this.deferUpdateDebug();\r\n }\r\n updateDebug() {\r\n layoutDebug.updateScrollDebug(this._scroll);\r\n }\r\n deferUpdateDebug() {\r\n if (layoutDebug.mask & layoutDebug.values.scroll) {\r\n setTimeout(() => {\r\n this.updateDebug();\r\n }, 1);\r\n }\r\n }\r\n\r\n scrollAbsolute(x: number, y: number) {\r\n $(this.selector)[0].scrollLeft = x;\r\n $(this.selector)[0].scrollTop = y;\r\n this.netScroll.x = this._scroll.x = x;\r\n this.netScroll.y = this._scroll.y = y;\r\n this.deferUpdateDebug();\r\n }\r\n\r\n /**\r\n * Scroll such that the box is fully visible, if possible (if it is\r\n * not larger than the screen) \r\n **/\r\n scrollVisibleBox(box: SvgBox) {\r\n let yoff = 0;\r\n let xoff = 0;\r\n\r\n const screenBox = this.svgPages.svgToClientNoOffset(box);\r\n const scrollState = this.scrollState;\r\n const scrollDown = () => screenBox.y + screenBox.height > scrollState.y + this.viewport.height;\r\n const scrollUp = () => screenBox.y < scrollState.y;\r\n const scrollLeft = () => screenBox.x < scrollState.x;\r\n const scrollRight = () => screenBox.x + screenBox.width > scrollState.x + this.viewport.width;\r\n // Math: make sure we don't scroll down if scrollUp is indicated, etc.\r\n if (scrollUp()) {\r\n yoff = Math.min(screenBox.y - scrollState.y, 0);\r\n } \r\n if (scrollDown()) {\r\n yoff = Math.max(screenBox.y - (scrollState.y - screenBox.height), 0);\r\n }\r\n if (scrollLeft()) {\r\n xoff = Math.min(screenBox.x - scrollState.x, 0);\r\n }\r\n if (scrollRight()) {\r\n xoff = Math.max(screenBox.x - (scrollState.x - screenBox.height), 0);\r\n }\r\n this.scrollOffset(xoff, yoff);\r\n}\r\n // Update viewport size, and also fix height of scroll region.\r\n updateViewport() {\r\n $(this.selector).css('height', (window.innerHeight - $(this.selector).offset().top).toString() + 'px');\r\n this.viewport = SvgHelpers.boxPoints(\r\n $(this.selector).offset().left,\r\n $(this.selector).offset().top,\r\n $(this.selector).width(),\r\n $(this.selector).height());\r\n this.deferUpdateDebug();\r\n }\r\n\r\n // ### scrollBox\r\n // get the current viewport, in scrolled coordinates. When tracker maps the\r\n // music element to client coordinates, these are the coordinates used in the\r\n // map\r\n get scrollBox(): SvgBox {\r\n return SvgHelpers.boxPoints(this.viewport.x + this.netScroll.x,\r\n this.viewport.y + this.netScroll.y,\r\n this.viewport.width,\r\n this.viewport.height\r\n );\r\n }\r\n\r\n // ### scrollOffset\r\n // scroll the offset from the starting scroll point\r\n scrollOffset(x: number, y: number) {\r\n const xScreen = Math.max(this._scroll.x + x, 0);\r\n const yScreen = Math.max(this._scroll.y + y, 0);\r\n this.scrollAbsolute(xScreen, yScreen);\r\n }\r\n\r\n // ### netScroll\r\n // return the net amount we've scrolled, based on when the maps were make (initial)\r\n // , the offset of the container, and the absolute coordinates of the scrollbar.\r\n get netScroll() {\r\n var xoffset = $(this.selector).offset().left - this._offsetInitial.x;\r\n var yoffset = $(this.selector).offset().top - this._offsetInitial.y;\r\n return { x: this._scroll.x - xoffset, y: this._scroll.y - yoffset };\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\n\r\nimport { Transposable, SvgBox, SvgPoint } from '../../smo/data/common';\r\nimport { SvgPage } from './svgPageMap';\r\n\r\ndeclare var $: any;\r\n\r\nexport interface StrokeInfo {\r\n strokeName: string,\r\n stroke: string,\r\n strokeWidth: string | number,\r\n strokeDasharray: string | number,\r\n fill: string,\r\n opacity: number\r\n}\r\n\r\nexport interface OutlineInfo {\r\n stroke: StrokeInfo,\r\n classes: string,\r\n box: SvgBox | SvgBox[],\r\n scroll: SvgPoint,\r\n context: SvgPage,\r\n timeOff: number,\r\n timer?: number,\r\n element?: SVGSVGElement\r\n}\r\n\r\nexport interface GradientInfo {\r\n color: string, offset: string, opacity: number\r\n}\r\n\r\nexport interface Boxable {\r\n box: SvgBox\r\n}\r\n\r\nexport class SvgBuilder {\r\n e: Element;\r\n constructor(el: string) {\r\n const ns = SvgHelpers.namespace;\r\n this.e = document.createElementNS(ns, el);\r\n }\r\n classes(cl: string): SvgBuilder {\r\n this.e.setAttributeNS('', 'class', cl);\r\n return this;\r\n }\r\n attr(name: string, value: string): SvgBuilder {\r\n this.e.setAttributeNS('', name, value);\r\n return this;\r\n }\r\n\r\n text(x: number | string, y: number | string, classes: string, text: string): SvgBuilder {\r\n x = typeof (x) == 'string' ? x : x.toString();\r\n y = typeof (y) == 'string' ? y : y.toString();\r\n this.e.setAttributeNS('', 'class', classes);\r\n this.e.setAttributeNS('', 'x', x);\r\n this.e.setAttributeNS('', 'y', y);\r\n this.e.textContent = text;\r\n return this;\r\n }\r\n rect(x: number | string, y: number | string, width: number | string, height: number | string, classes: string): SvgBuilder {\r\n x = typeof (x) == 'string' ? x : x.toString();\r\n y = typeof (y) == 'string' ? y : y.toString();\r\n width = typeof (width) == 'string' ? width : width.toString();\r\n height = typeof (height) == 'string' ? height : height.toString();\r\n this.e.setAttributeNS('', 'x', x);\r\n this.e.setAttributeNS('', 'y', y);\r\n this.e.setAttributeNS('', 'width', width);\r\n this.e.setAttributeNS('', 'height', height);\r\n if (classes) {\r\n this.e.setAttributeNS('', 'class', classes);\r\n }\r\n return this;\r\n }\r\n line(x1: number | string, y1: number | string, x2: number | string, y2: number | string, classes: string): SvgBuilder {\r\n x1 = typeof (x1) == 'string' ? x1 : x1.toString();\r\n y1 = typeof (y1) == 'string' ? y1 : y1.toString();\r\n x2 = typeof (x2) == 'string' ? x2 : x2.toString();\r\n y2 = typeof (y2) == 'string' ? y2 : y2.toString();\r\n\r\n this.e.setAttributeNS('', 'x1', x1);\r\n this.e.setAttributeNS('', 'y1', y1);\r\n this.e.setAttributeNS('', 'x2', x2);\r\n this.e.setAttributeNS('', 'y2', y2);\r\n if (classes) {\r\n this.e.setAttributeNS('', 'class', classes);\r\n }\r\n return this;\r\n }\r\n append(el: any): SvgBuilder {\r\n this.e.appendChild(el.e);\r\n return this;\r\n }\r\n dom(): Element {\r\n return this.e;\r\n }\r\n static b(element: string): SvgBuilder {\r\n return new SvgBuilder(element);\r\n }\r\n}\r\n// ## SvgHelpers\r\n// Mostly utilities for converting coordinate spaces based on transforms, etc.\r\n// ### static class methods:\r\n// ---\r\nexport class SvgHelpers {\r\n static get namespace(): string {\r\n return \"http://www.w3.org/2000/svg\";\r\n }\r\n\r\n // ### gradient\r\n // Create an svg linear gradient.\r\n // Stops look like this:\r\n // `[{color:\"#eee\", offset:\"0%\",opacity:0.5}]`\r\n // orientation is horizontal or vertical\r\n static gradient(svg: SVGSVGElement, id: string, orientation: string, stops: GradientInfo[]) {\r\n var ns = SvgHelpers.namespace;\r\n var x2 = orientation === 'vertical' ? 0 : 1;\r\n var y2 = orientation === 'vertical' ? 1 : 0;\r\n\r\n var e = document.createElementNS(ns, 'linearGradient');\r\n e.setAttributeNS('', 'id', id);\r\n e.setAttributeNS('', 'x1', '0');\r\n e.setAttributeNS('', 'x2', x2.toString());\r\n e.setAttributeNS('', 'y1', '0');\r\n e.setAttributeNS('', 'y2', y2.toString());\r\n stops.forEach((stop) => {\r\n var s = document.createElementNS(ns, 'stop');\r\n s.setAttributeNS('', 'stop-opacity', stop.opacity.toString());\r\n s.setAttributeNS('', 'stop-color', stop.color);\r\n s.setAttributeNS('', 'offset', stop.offset);\r\n e.appendChild(s);\r\n\r\n });\r\n svg.appendChild(e);\r\n }\r\n\r\n static renderCursor(svg: SVGSVGElement, x: number, y: number, height: number) {\r\n var ns = SvgHelpers.namespace;\r\n const width = height * 0.4;\r\n x = x - (width / 2);\r\n var mcmd = (d: string, x: number, y: number) => {\r\n return d + 'M ' + x.toString() + ' ' + y.toString() + ' ';\r\n };\r\n var qcmd = (d: string, x1: number, y1: number, x2: number, y2: number) => {\r\n return d + 'q ' + x1.toString() + ' ' + y1.toString() + ' ' + x2.toString() + ' ' + y2.toString() + ' ';\r\n };\r\n var lcmd = (d: string, x: number, y: number) => {\r\n return d + 'L ' + x.toString() + ' ' + y.toString() + ' ';\r\n };\r\n var x1 = (width / 2) * .333;\r\n var y1 = -1 * (x1 / 4);\r\n var x2 = (width / 2);\r\n var y2 = x2 / 4;\r\n var ns = SvgHelpers.namespace;\r\n var e = document.createElementNS(ns, 'path');\r\n var d = '';\r\n d = mcmd(d, x, y);\r\n d = qcmd(d, x1, y1, x2, y2);\r\n d = lcmd(d, x + (width / 2), y + height - (width / 8));\r\n d = mcmd(d, x + width, y);\r\n d = qcmd(d, -1 * x1, y1, -1 * x2, y2);\r\n d = mcmd(d, x, y + height);\r\n d = qcmd(d, x1, -1 * y1, x2, -1 * y2);\r\n d = mcmd(d, x + width, y + height);\r\n d = qcmd(d, -1 * x1, -1 * y1, -1 * x2, -1 * y2);\r\n e.setAttributeNS('', 'd', d);\r\n e.setAttributeNS('', 'stroke-width', '1');\r\n e.setAttributeNS('', 'stroke', '#555');\r\n e.setAttributeNS('', 'fill', 'none');\r\n svg.appendChild(e);\r\n }\r\n\r\n // ### boxNote\r\n // update the note geometry based on current viewbox conditions.\r\n // This may not be the appropriate place for this...maybe in layout\r\n static updateArtifactBox(context: SvgPage, element: SVGSVGElement | undefined, artifact: Transposable) {\r\n if (!element) {\r\n console.log('updateArtifactBox: undefined element!');\r\n return;\r\n }\r\n artifact.logicalBox = context.offsetBbox(element);\r\n }\r\n\r\n // ### eraseOutline\r\n // Erases old outlineRects.\r\n static eraseOutline(params: OutlineInfo) {\r\n // Hack: Assume a stroke style, should just take a stroke param.\r\n if (params.element) {\r\n params.element.remove();\r\n params.element = undefined;\r\n }\r\n }\r\n\r\n static outlineRect(params: OutlineInfo) {\r\n const context = params.context;\r\n if (params.element && params.timer) {\r\n clearTimeout(params.timer);\r\n params.timer = undefined;\r\n params.element.remove();\r\n params.element = undefined;\r\n }\r\n if (params.timeOff) {\r\n params.timer = window.setTimeout(() => {\r\n if (params.element) {\r\n params.element.remove();\r\n params.element = undefined;\r\n params.timer = undefined;\r\n }\r\n }, params.timeOff);\r\n }\r\n // Don't highlight in print mode.\r\n if ($('body').hasClass('printing')) {\r\n return;\r\n }\r\n const classes = params.classes.length > 0 ? params.classes + ' ' + params.stroke.strokeName : params.stroke.strokeName;\r\n var grp = context.getContext().openGroup(classes, classes + '-outline');\r\n params.element = grp;\r\n const boxes = Array.isArray(params.box) ? params.box : [params.box];\r\n\r\n boxes.forEach((box: SvgBox) => {\r\n if (box) {\r\n var strokeObj:any = params.stroke;\r\n strokeObj['stroke-width'] = params.stroke.strokeWidth;\r\n var margin = 5;\r\n /* if (params.clientCoordinates === true) {\r\n box = SvgHelpers.smoBox(SvgHelpers.clientToLogical(context.svg, SvgHelpers.smoBox(SvgHelpers.adjustScroll(box, scroll))));\r\n } */\r\n context.getContext().rect(box.x - margin, box.y - margin, box.width + margin * 2, box.height + margin * 2, strokeObj);\r\n }\r\n });\r\n context.getContext().closeGroup(grp);\r\n }\r\n\r\n static setSvgStyle(element: Element, attrs: StrokeInfo) {\r\n element.setAttributeNS('', 'stroke', attrs.stroke);\r\n if (attrs.strokeDasharray) {\r\n element.setAttributeNS('', 'stroke-dasharray', attrs.strokeDasharray.toString());\r\n }\r\n if (attrs.strokeWidth) {\r\n element.setAttributeNS('', 'stroke-width', attrs.strokeWidth.toString());\r\n }\r\n if (attrs.fill) {\r\n element.setAttributeNS('', 'fill', attrs.fill);\r\n }\r\n }\r\n static rect(svg: Document, box: SvgBox, attrs: StrokeInfo, classes: string) {\r\n var rect = document.createElementNS(SvgHelpers.namespace, 'rect');\r\n SvgHelpers.setSvgStyle(rect, attrs);\r\n if (classes) {\r\n rect.setAttributeNS('', 'class', classes);\r\n }\r\n svg.appendChild(rect);\r\n return rect;\r\n }\r\n\r\n static line(svg: SVGSVGElement, x1: number | string, y1: number | string, x2: number | string, y2: number | string, attrs: StrokeInfo, classes: string) {\r\n var line = document.createElementNS(SvgHelpers.namespace, 'line');\r\n x1 = typeof (x1) == 'string' ? x1 : x1.toString();\r\n y1 = typeof (y1) == 'string' ? y1 : y1.toString();\r\n x2 = typeof (x2) == 'string' ? x2 : x2.toString();\r\n y2 = typeof (y2) == 'string' ? y2 : y2.toString();\r\n\r\n line.setAttributeNS('', 'x1', x1);\r\n line.setAttributeNS('', 'y1', y1);\r\n line.setAttributeNS('', 'x2', x2);\r\n line.setAttributeNS('', 'y2', y2);\r\n SvgHelpers.setSvgStyle(line, attrs);\r\n if (classes) {\r\n line.setAttributeNS('', 'class', classes);\r\n }\r\n svg.appendChild(line);\r\n }\r\n\r\n static arrowDown(svg: SVGSVGElement, box: SvgBox) {\r\n const arrowStroke: StrokeInfo = { strokeName: 'arrow-stroke', stroke: '#321', strokeWidth: '2', strokeDasharray: '4,1', fill: 'none', opacity: 1.0 };\r\n SvgHelpers.line(svg, box.x + box.width / 2, box.y, box.x + box.width / 2, box.y + box.height, arrowStroke, '');\r\n var arrowY = box.y + box.height / 4;\r\n SvgHelpers.line(svg, box.x, arrowY, box.x + box.width / 2, box.y + box.height, arrowStroke, '');\r\n SvgHelpers.line(svg, box.x + box.width, arrowY, box.x + box.width / 2, box.y + box.height, arrowStroke, '');\r\n }\r\n static debugBox(svg: SVGSVGElement, box: SvgBox | null, classes: string, voffset: number) {\r\n voffset = voffset ?? 0;\r\n classes = classes ?? '';\r\n if (!box)\r\n return;\r\n classes += ' svg-debug-box';\r\n var b = SvgBuilder.b;\r\n var mid = box.x + box.width / 2;\r\n var xtext = 'x1: ' + Math.round(box.x);\r\n var wtext = 'x2: ' + Math.round(box.width + box.x);\r\n var ytext = 'y1: ' + Math.round(box.y);\r\n var htext = 'y2: ' + Math.round(box.height + box.y);\r\n var ytextp = Math.round(box.y + box.height);\r\n var ytextp2 = Math.round(box.y + box.height - 30);\r\n\r\n var r = b('g').classes(classes)\r\n .append(\r\n b('text').text(box.x + 20, box.y - 14 + voffset, 'svg-debug-text', xtext))\r\n .append(\r\n b('text').text(mid - 20, box.y - 14 + voffset, 'svg-debug-text', wtext))\r\n .append(\r\n b('line').line(box.x, box.y - 2, box.x + box.width, box.y - 2, ''))\r\n .append(\r\n b('line').line(box.x, box.y - 8, box.x, box.y + 5, ''))\r\n .append(\r\n b('line').line(box.x + box.width, box.y - 8, box.x + box.width, box.y + 5, ''))\r\n .append(\r\n b('text').text(Math.round(box.x - 14 + voffset), ytextp, 'svg-vdebug-text', ytext)\r\n .attr('transform', 'rotate(-90,' + Math.round(box.x - 14 + voffset) + ',' + ytextp + ')'));\r\n if (box.height > 2) {\r\n r.append(\r\n b('text').text(Math.round(box.x - 14 + voffset), ytextp2, 'svg-vdebug-text', htext)\r\n .attr('transform', 'rotate(-90,' + Math.round(box.x - 14 + voffset) + ',' + (ytextp2) + ')'))\r\n .append(\r\n b('line').line(Math.round(box.x - 2), Math.round(box.y + box.height), box.x - 2, box.y, ''))\r\n .append(\r\n b('line').line(Math.round(box.x - 8), Math.round(box.y + box.height), box.x + 6, Math.round(box.y + box.height), ''))\r\n .append(\r\n b('line').line(Math.round(box.x - 8), Math.round(box.y), Math.round(box.x + 6), Math.round(box.y),''));\r\n }\r\n svg.appendChild(r.dom());\r\n }\r\n static debugBoxNoText(svg: SVGSVGElement, box: SvgBox | null, classes: string, voffset: number) {\r\n voffset = voffset ?? 0;\r\n classes = classes ?? '';\r\n if (!box)\r\n return;\r\n classes += ' svg-debug-box';\r\n var b = SvgBuilder.b;\r\n var r = b('g').classes(classes)\r\n .append(\r\n b('line').line(box.x, box.y - 2, box.x + box.width, box.y - 2, ''))\r\n .append(\r\n b('line').line(box.x, box.y - 8, box.x, box.y + 5, ''))\r\n .append(\r\n b('line').line(box.x + box.width, box.y - 8, box.x + box.width, box.y + 5, ''));\r\n if (box.height > 2) {\r\n r.append(\r\n b('line').line(Math.round(box.x - 2), Math.round(box.y + box.height), box.x - 2, box.y, ''))\r\n .append(\r\n b('line').line(Math.round(box.x - 8), Math.round(box.y + box.height), box.x + 6, Math.round(box.y + box.height), ''))\r\n .append(\r\n b('line').line(Math.round(box.x - 8), Math.round(box.y), Math.round(box.x + 6), Math.round(box.y),''));\r\n }\r\n svg.appendChild(r.dom());\r\n }\r\n\r\n static placeSvgText(svg: SVGSVGElement, attributes: Record[], classes: string, text: string): SVGSVGElement {\r\n var ns = SvgHelpers.namespace;\r\n var e = document.createElementNS(ns, 'text');\r\n attributes.forEach((attr) => {\r\n var key: string = Object.keys(attr)[0];\r\n e.setAttributeNS('', key, attr[key].toString());\r\n })\r\n if (classes) {\r\n e.setAttributeNS('', 'class', classes);\r\n }\r\n var tn = document.createTextNode(text);\r\n e.appendChild(tn);\r\n svg.appendChild(e);\r\n return (e as any);\r\n }\r\n static doesBox1ContainBox2(box1?: SvgBox, box2?: SvgBox): boolean {\r\n if (!box1 || !box2) {\r\n return false;\r\n }\r\n const i1 = box2.x - box1.x;\r\n const i2 = box2.y - box1.y;\r\n return (i1 > 0 && i1 < box1.width && i2 > 0 && i2 < box1.height);\r\n }\r\n\r\n // ### findIntersectionArtifact\r\n // find all object that intersect with the rectangle\r\n static findIntersectingArtifact(clientBox: SvgBox, objects: Boxable[]): Boxable[] {\r\n var box = SvgHelpers.smoBox(clientBox); //svgHelpers.untransformSvgPoint(this.context.svg,clientBox);\r\n\r\n // box.y = box.y - this.renderElement.offsetTop;\r\n // box.x = box.x - this.renderElement.offsetLeft;\r\n var rv: Boxable[] = [];\r\n objects.forEach((object) => {\r\n // Measure has been updated, but not drawn.\r\n if (!object.box) {\r\n // console.log('there is no box');\r\n } else {\r\n var obox = SvgHelpers.smoBox(object.box);\r\n if (SvgHelpers.doesBox1ContainBox2(obox, box)) {\r\n rv.push(object);\r\n }\r\n }\r\n });\r\n\r\n return rv;\r\n }\r\n\r\n static findSmallestIntersection(clientBox: SvgBox, objects: Boxable[]) {\r\n var ar = SvgHelpers.findIntersectingArtifact(clientBox, objects);\r\n if (!ar.length) {\r\n return null;\r\n }\r\n var rv = ar[0];\r\n var min = ar[0].box.width * ar[0].box.height;\r\n ar.forEach((obj) => {\r\n var tst = obj.box.width * obj.box.height;\r\n if (tst < min) {\r\n rv = obj;\r\n min = tst;\r\n }\r\n });\r\n return rv;\r\n }\r\n\r\n static translateElement(g: SVGSVGElement, x: number | string, y: number | string) {\r\n g.setAttributeNS('', 'transform', 'translate(' + x + ' ' + y + ')');\r\n }\r\n\r\n static stringify(box: SvgBox): string {\r\n if (box['width']) {\r\n\r\n return JSON.stringify({\r\n x: box.x,\r\n y: box.y,\r\n width: box.width,\r\n height: box.height\r\n }, null, ' ');\r\n } else {\r\n return JSON.stringify({\r\n x: box.x,\r\n y: box.y\r\n }, null, ' ');\r\n }\r\n }\r\n\r\n static log(box: SvgBox) {\r\n if (box['width']) {\r\n console.log(JSON.stringify({\r\n x: box.x,\r\n y: box.y,\r\n width: box.width,\r\n height: box.height\r\n }, null, ' '));\r\n } else {\r\n console.log('{}');\r\n }\r\n }\r\n\r\n // ### smoBox:\r\n // return a simple box object that can be serialized, copied\r\n // (from svg DOM box)\r\n static smoBox(box: any) {\r\n if (typeof (box) === \"undefined\" || box === null) {\r\n return SvgBox.default;\r\n }\r\n let testBox = box;\r\n if (Array.isArray(box)) {\r\n testBox = box[0];\r\n }\r\n const hround = (f: number): number => {\r\n return Math.round((f + Number.EPSILON) * 100) / 100;\r\n }\r\n const x = typeof (testBox.x) == 'undefined' ? hround(testBox.left) : hround(testBox.x);\r\n const y = typeof (testBox.y) == 'undefined' ? hround(testBox.top) : hround(testBox.y);\r\n return ({\r\n x: hround(x),\r\n y: hround(y),\r\n width: hround(testBox.width),\r\n height: hround(testBox.height)\r\n });\r\n }\r\n // ### unionRect\r\n // grow the bounding box two objects to include both.\r\n static unionRect(b1: SvgBox, b2: SvgBox): SvgBox {\r\n const x = Math.min(b1.x, b2.x);\r\n const y = Math.min(b1.y, b2.y);\r\n const width = Math.max(b1.x + b1.width, b2.x + b2.width) - x;\r\n const height = Math.max(b1.y + b1.height, b2.y + b2.height) - y;\r\n return {\r\n x: x,\r\n y: y,\r\n width: width,\r\n height: height\r\n };\r\n }\r\n\r\n static boxPoints(x: number, y: number, w: number, h: number): SvgBox {\r\n return ({\r\n x: x,\r\n y: y,\r\n width: w,\r\n height: h\r\n });\r\n }\r\n\r\n // ### svgViewport\r\n // set `svg` element to `width`,`height` and viewport `scale`\r\n static svgViewport(svg: SVGSVGElement, xOffset: number, yOffset: Number, width: number, height: number, scale: number) {\r\n svg.setAttributeNS('', 'width', '' + width);\r\n svg.setAttributeNS('', 'height', '' + height);\r\n svg.setAttributeNS('', 'viewBox', '' + xOffset + ' ' + yOffset + ' ' + Math.round(width / scale) + ' ' +\r\n Math.round(height / scale));\r\n }\r\n static removeElementsByClass(svg: SVGSVGElement, className: string) {\r\n const els = svg.getElementsByClassName(className);\r\n const ellength = els.length\r\n for (var xxx = 0; xxx < ellength; ++xxx) {\r\n els[0].remove();\r\n }\r\n }\r\n}\r\n","import { SvgHelpers, StrokeInfo } from \"./svgHelpers\";\r\nimport { SvgPoint, SvgBox, Renderable } from '../../smo/data/common';\r\nimport { layoutDebug } from './layoutDebug';\r\nimport { SmoGlobalLayout, SmoPageLayout } from '../../smo/data/scoreModifiers';\r\nimport { SmoTextGroup } from '../../smo/data/scoreText';\r\nimport { SmoSelection, SmoSelector } from '../../smo/xform/selections';\r\nimport { ModifierTab } from '../../smo/xform/selections';\r\nimport { VexFlow } from '../../common/vex';\r\n\r\nconst VF = VexFlow;\r\n/**\r\n * classes for managing the SVG containers where the music is rendered. Each\r\n * page is a different SVG element. Screen coordinates need to be mapped to the\r\n * correct page and then to the correct element on that page.\r\n * @module /render/sui/svgPageMap\r\n */\r\ndeclare var $: any;\r\n/**\r\n * A selection map maps a sub-section of music (a measure, for instance) to a region\r\n * on the screen. SelectionMap can contain other SelectionMaps with\r\n * different 'T', for instance, notes in a measure, in a 'Russian Dolls' kind of model.\r\n * This allows us to search for elements in < O(n) time and avoid\r\n * expensive geometry operations.\r\n */\r\nexport abstract class SelectionMap {\r\n /**\r\n * Create a key from the selection (selector). e.g. (1,1)\r\n * @param selection \r\n */\r\n abstract createKey(selection: SmoSelection): K;\r\n /**\r\n * get a set of coordinates from this selection, if it has been rendered.\r\n * @param selection \r\n */\r\n abstract boxFromSelection(selection: SmoSelection): SvgBox;\r\n /**\r\n * Add the selection to our map, and possibly to our child map.\r\n * @param key \r\n * @param selection \r\n */\r\n abstract addKeyToMap(key: K, selection: SmoSelection): void;\r\n /**\r\n * find a collection of selection that match a bounding box, possibly by\r\n * recursing through our child SelectionMaps.\r\n * @param value \r\n * @param box \r\n * @param rv \r\n */\r\n abstract findValueInMap(value: T, box: SvgBox): SmoSelection[];\r\n /**\r\n * the outer bounding box of these selections\r\n */\r\n box: SvgBox = SvgBox.default;\r\n /**\r\n * map of key to child SelectionMaps or SmoSelections\r\n */\r\n systemMap: Map = new Map();\r\n /**\r\n * Given a bounding box (or point), find all the musical elements contained\r\n * in that point\r\n * @param box \r\n * @returns SmoSelection[]\r\n */\r\n findArtifact(box: SvgBox): SmoSelection[] {\r\n let rv: SmoSelection[] = [];\r\n for (const [key, value] of this.systemMap) {\r\n rv = rv.concat(this.findValueInMap(value, box));\r\n }\r\n return rv;\r\n }\r\n /**\r\n * Add a rendered element to the map, and update the bounding box\r\n * @param selection \r\n * @returns \r\n */\r\n addArtifact(selection: SmoSelection) {\r\n if (!selection.note || !selection.note.logicalBox) {\r\n return; \r\n }\r\n const bounds = this.boxFromSelection(selection);\r\n if (this.systemMap.size === 0) {\r\n this.box = JSON.parse(JSON.stringify(bounds));\r\n }\r\n const ix = this.createKey(selection);\r\n this.addKeyToMap(ix, selection);\r\n this.box = SvgHelpers.unionRect(bounds, this.box);\r\n }\r\n}\r\n\r\n/**\r\n * logic to map a set of notes to a region on the screen, for searching\r\n */\r\nexport class MappedNotes extends SelectionMap{\r\n createKey(selection: SmoSelection): string {\r\n return `${selection.selector.voice}-${selection.selector.tick}`;\r\n }\r\n boxFromSelection(selection: SmoSelection): SvgBox {\r\n return selection.note?.logicalBox ?? SvgBox.default;\r\n }\r\n addKeyToMap(key: string, selection: SmoSelection) {\r\n this.systemMap.set(key, selection);\r\n }\r\n findValueInMap(value: SmoSelection, box: SvgBox): SmoSelection[] {\r\n const rv: SmoSelection[] = [];\r\n const note = value.note;\r\n if (note && note.logicalBox && SvgHelpers.doesBox1ContainBox2(note.logicalBox, box)) {\r\n rv.push(value);\r\n }\r\n return rv;\r\n }\r\n}\r\n/**\r\n * Map of measures to a region on the page.\r\n */\r\nexport class MappedMeasures extends SelectionMap {\r\n box: SvgBox = SvgBox.default;\r\n systemMap: Map = new Map();\r\n createKey(selection: SmoSelection): string {\r\n return `${selection.selector.staff}-${selection.selector.measure}`;\r\n }\r\n boxFromSelection(selection: SmoSelection): SvgBox {\r\n const noteBox = selection.note?.logicalBox ?? SvgBox.default;\r\n return SvgHelpers.unionRect(noteBox, selection.measure.svg.logicalBox);\r\n }\r\n addKeyToMap(key: string, selection: SmoSelection) {\r\n if (!this.systemMap.has(key)) {\r\n const nnote = new MappedNotes();\r\n this.systemMap.set(key, nnote); \r\n }\r\n this.systemMap.get(key)?.addArtifact(selection);\r\n }\r\n findValueInMap(value: MappedNotes, box: SvgBox): SmoSelection[] {\r\n let rv: SmoSelection[] = [];\r\n if (SvgHelpers.doesBox1ContainBox2(value.box, box)) {\r\n rv = rv.concat(value.findArtifact(box));\r\n }\r\n return rv;\r\n }\r\n}\r\n\r\n/**\r\n * Map of the systems on a page. Each system has a unique line index\r\n * which is the hash\r\n */\r\nexport class MappedSystems extends SelectionMap {\r\n box: SvgBox = SvgBox.default;\r\n systemMap: Map = new Map();\r\n createKey(selection: SmoSelection):number {\r\n return selection.measure.svg.lineIndex;\r\n }\r\n boxFromSelection(selection: SmoSelection): SvgBox {\r\n const noteBox = selection.note?.logicalBox ?? SvgBox.default;\r\n return SvgHelpers.unionRect(noteBox, selection.measure.svg.logicalBox);\r\n }\r\n addKeyToMap(selectionKey: number, selection: SmoSelection) {\r\n if (!this.systemMap.has(selectionKey)) {\r\n const nmeasure = new MappedMeasures();\r\n this.systemMap.set(selectionKey, nmeasure);\r\n }\r\n this.systemMap.get(selectionKey)?.addArtifact(selection);\r\n }\r\n findValueInMap(value: MappedMeasures, box: SvgBox) {\r\n let rv: SmoSelection[] = [];\r\n if (SvgHelpers.doesBox1ContainBox2(value.box, box)) {\r\n rv = rv.concat(value.findArtifact(box));\r\n }\r\n return rv;\r\n } \r\n clearMeasure(selection: SmoSelection) {\r\n if (this.systemMap.has(selection.measure.svg.lineIndex)) {\r\n const mmap = this.systemMap.get(selection.measure.svg.lineIndex);\r\n if (mmap) {\r\n this.systemMap.delete(selection.measure.svg.lineIndex);\r\n }\r\n }\r\n }\r\n}\r\n/**\r\n * Each page is a different SVG element, with its own offset within the DOM. This\r\n * makes partial updates faster. SvgPage keeps track of all musical elements in SelectionMaps.\r\n * staff and score modifiers are kept in seperate lists since they may span multiple\r\n * musical elements (e.g. slurs, text elements).\r\n */\r\nexport class SvgPage {\r\n _renderer: any;\r\n pageNumber: number;\r\n box: SvgBox;\r\n systemMap: MappedSystems = new MappedSystems();\r\n modifierYKeys: number[] = [];\r\n modifierTabDivs: Record = {};\r\n static get defaultMap() {\r\n return {\r\n box: SvgBox.default,\r\n systemMap: new Map()\r\n };\r\n }\r\n /**\r\n * Modifiers are divided into `modifierDivs` vertical \r\n * rectangles for event lookup.\r\n */\r\n static get modifierDivs() {\r\n return 8;\r\n }\r\n /**\r\n * This is the VextFlow renderer context (SVGContext)\r\n * @returns \r\n */\r\n getContext(): any {\r\n return this._renderer.getContext();\r\n }\r\n get divSize(): number {\r\n return this.box.height / SvgPage.modifierDivs;\r\n }\r\n constructor(renderer: any, pageNumber: number, box: SvgBox) {\r\n this._renderer = renderer;\r\n this.pageNumber = pageNumber;\r\n this.box = box;\r\n let divEnd = this.divSize;\r\n for (let i = 0; i < SvgPage.modifierDivs; ++i) {\r\n this.modifierYKeys.push(divEnd);\r\n divEnd += this.divSize;\r\n }\r\n }\r\n /**\r\n * Given SVG y, return the div for modifiers\r\n * @param y \r\n * @returns \r\n */\r\n divIndex(y: number): number {\r\n return Math.round((y - this.box.y) / this.divSize);\r\n }\r\n /**\r\n * Remove all elements and modifiers in this page, for a redraw.\r\n */\r\n clearMap() {\r\n this.systemMap = new MappedSystems();\r\n this.modifierTabDivs = {};\r\n }\r\n /**\r\n * Clear mapped objects associated with a measure, including any\r\n * modifiers that span that measure.\r\n * @param selection \r\n */\r\n clearMeasure(selection: SmoSelection) { \r\n this.systemMap.clearMeasure(selection);\r\n const div = this.divIndex(selection.measure.svg.logicalBox.y);\r\n if (div < this.modifierYKeys.length) {\r\n const mods: ModifierTab[] = [];\r\n this.modifierTabDivs[div].forEach((mt: ModifierTab) => {\r\n if (mt.selection) {\r\n if (!SmoSelector.sameMeasure(mt.selection.selector, selection.selector)) {\r\n mods.push(mt);\r\n }\r\n } else {\r\n mods.push(mt);\r\n }\r\n });\r\n this.modifierTabDivs[div] = mods;\r\n }\r\n }\r\n /**\r\n * add a modifier to the page, indexed by its rectangle\r\n * @param modifier \r\n */\r\n addModifierTab(modifier: ModifierTab) {\r\n const div = this.divIndex(modifier.box.y);\r\n if (div < this.modifierYKeys.length) {\r\n if (!this.modifierTabDivs[div]) {\r\n this.modifierTabDivs[div] = [];\r\n }\r\n this.modifierTabDivs[div].push(modifier);\r\n }\r\n }\r\n /**\r\n * Add a new selection to the page\r\n * @param selection \r\n */\r\n addArtifact(selection: SmoSelection) { \r\n this.systemMap.addArtifact(selection);\r\n }\r\n /**\r\n * Try to find a selection on this page, based on the mouse event\r\n * @param box \r\n * @returns \r\n */\r\n findArtifact(box: SvgBox): SmoSelection[] {\r\n return this.systemMap.findArtifact(box);\r\n }\r\n /**\r\n * Try to find a modifier on this page, based on the mouse event\r\n * @param box \r\n * @returns \r\n */\r\n findModifierTabs(box: SvgBox): ModifierTab[] {\r\n const rv:ModifierTab[] = [];\r\n const div = this.divIndex(box.y);\r\n if (div < this.modifierYKeys.length) {\r\n if (this.modifierTabDivs[div]) {\r\n this.modifierTabDivs[div].forEach((modTab) => {\r\n if (SvgHelpers.doesBox1ContainBox2(modTab.box, box)) {\r\n rv.push(modTab);\r\n }\r\n });\r\n }\r\n }\r\n return rv;\r\n }\r\n clearModifiers() { \r\n Object.keys(this.modifierTabDivs).forEach((key) => {\r\n const modifiers = this.modifierTabDivs[parseInt(key)];\r\n modifiers.forEach((mod) => {\r\n if (mod instanceof SmoTextGroup) {\r\n (mod as SmoTextGroup).elements.forEach((element) => {\r\n element.remove();\r\n });\r\n (mod as SmoTextGroup).elements = [];\r\n }\r\n });\r\n });\r\n this.modifierTabDivs = {};\r\n }\r\n /**\r\n * Measure the bounding box of an element. Return the box as if the top of the first page were 0,0.\r\n * Bounding boxes are stored in absolute coordinates from the top of the first page. When rendering\r\n * elements, we adjust the coordinates for hte local page.\r\n * @param element \r\n * @returns \r\n */\r\n offsetBbox(element: SVGSVGElement): SvgBox {\r\n const yoff = this.box.y;\r\n const xoff = this.box.x;\r\n const lbox = element.getBBox();\r\n return ({ x: lbox.x + xoff, y: lbox.y + yoff, width: lbox.width, height: lbox.height });\r\n }\r\n /**\r\n * Adjust the bounding box to local coordinates for this page.\r\n * @param box \r\n * @returns \r\n */\r\n offsetSvgBox(box: SvgBox) {\r\n return { x: box.x - this.box.x, y: box.y - this.box.y, width: box.width, height: box.height };\r\n }\r\n /**\r\n * Adjust the point to local coordinates for this page.\r\n * @param box \r\n * @returns \r\n */\r\n offsetSvgPoint(box: SvgPoint) {\r\n return { x: box.x - this.box.x, y: box.y - this.box.y };\r\n }\r\n get svg(): SVGSVGElement {\r\n return this.getContext().svg as SVGSVGElement;\r\n }\r\n}\r\n/**\r\n * A container for all the SVG elements, and methods to manage adding and finding elements. Each\r\n * page of the score has its own SVG element.\r\n */\r\nexport class SvgPageMap {\r\n _layout: SmoGlobalLayout;\r\n _container: HTMLElement;\r\n _pageLayouts: SmoPageLayout[];\r\n vfRenderers: SvgPage[] = [];\r\n static get strokes(): Record {\r\n return {\r\n 'debug-mouse-box': {\r\n strokeName: 'debug-mouse',\r\n stroke: '#7ce',\r\n strokeWidth: 3,\r\n strokeDasharray: '1,1',\r\n fill: 'none',\r\n opacity: 0.6\r\n }\r\n };\r\n }\r\n containerOffset: SvgPoint = SvgPoint.default;\r\n /**\r\n * \r\n * @param layout - defines the page width/height and relative zoom common to all the pages\r\n * @param container - the parent DOM element that contains all the pages\r\n * @param pages - the layouts (margins, etc) for each pages.\r\n */\r\n constructor(layout: SmoGlobalLayout, container: HTMLElement, pages: SmoPageLayout[]) {\r\n this._layout = layout;\r\n this._container = container;\r\n this._pageLayouts = pages;\r\n }\r\n get container() {\r\n return this._container;\r\n }\r\n /**\r\n * Update the offset of the music container DOM element, in client coordinates. This is used\r\n * when converting absolute screen coordinates (like from a mouse event) to SVG coordinates\r\n * @param scrollPoint \r\n */\r\n updateContainerOffset(scrollPoint: SvgPoint) {\r\n const rect = SvgHelpers.smoBox(this.container.getBoundingClientRect());\r\n this.containerOffset = { x: rect.x + scrollPoint.x, y: rect.y + scrollPoint.y };\r\n }\r\n get layout() {\r\n return this._layout;\r\n }\r\n get pageLayouts() {\r\n return this._pageLayouts;\r\n }\r\n get zoomScale() {\r\n return this.layout.zoomScale;\r\n }\r\n get renderScale() {\r\n return this.layout.svgScale;\r\n }\r\n get pageDivHeight() {\r\n return this.layout.pageHeight * this.zoomScale;\r\n }\r\n get pageDivWidth() {\r\n return this.layout.pageWidth * this.zoomScale;\r\n }\r\n get pageHeight() {\r\n return this.layout.pageHeight / this.layout.svgScale;\r\n }\r\n get pageWidth() {\r\n return this.layout.pageWidth / this.layout.svgScale;\r\n }\r\n get totalHeight() {\r\n return this.pageDivHeight * this.pageLayouts.length;\r\n }\r\n /**\r\n * create/re-create all the page SVG elements\r\n */\r\n createRenderers() {\r\n // $(this.container).html('');\r\n $(this.container).css('width', '' + Math.round(this.pageDivWidth) + 'px');\r\n $(this.container).css('height', '' + Math.round(this.totalHeight) + 'px');\r\n const toRemove: HTMLElement[] = [];\r\n this.vfRenderers.forEach((renderer) => {\r\n const container = (renderer.svg as SVGSVGElement).parentElement;\r\n if (container) {\r\n toRemove.push(container);\r\n }\r\n });\r\n toRemove.forEach((tt) => {\r\n tt.remove();\r\n });\r\n this.vfRenderers = [];\r\n this.pageLayouts.forEach(() => {\r\n this.addPage();\r\n });\r\n }\r\n addPage() {\r\n const ix = this.vfRenderers.length;\r\n const container = document.createElement('div');\r\n container.setAttribute('id', 'smoosic-svg-div-' + ix.toString());\r\n this._container.append(container);\r\n const vexRenderer = new VF.Renderer(container, VF.Renderer.Backends.SVG);\r\n const svg = (vexRenderer.getContext() as any).svg as SVGSVGElement;\r\n SvgHelpers.svgViewport(svg, 0, 0, this.pageDivWidth, this.pageDivHeight, this.renderScale * this.zoomScale);\r\n const topY = this.pageHeight * ix;\r\n const box = SvgHelpers.boxPoints(0, topY, this.pageWidth, this.pageHeight);\r\n this.vfRenderers.push(new SvgPage(vexRenderer, ix, box));\r\n }\r\n updateZoom(zoomScale: number) {\r\n this.layout.zoomScale = zoomScale;\r\n this.vfRenderers.forEach((pp) => {\r\n SvgHelpers.svgViewport(pp.svg, 0, 0, this.pageDivWidth, this.pageDivHeight, this.renderScale * this.zoomScale);\r\n });\r\n $(this.container).css('width', '' + Math.round(this.pageDivWidth) + 'px');\r\n $(this.container).css('height', '' + Math.round(this.totalHeight) + 'px');\r\n }\r\n\r\n /**\r\n * Convert from screen/client event to SVG space. We assume the scroll offset is already added to `box`\r\n * @param box \r\n * @returns \r\n */\r\n clientToSvg(box: SvgBox) {\r\n const cof = (this.zoomScale * this.renderScale);\r\n const x = (box.x - this.containerOffset.x) / cof;\r\n const y = (box.y - this.containerOffset.y) / cof;\r\n const logicalBox = SvgHelpers.boxPoints(x, y, Math.max(box.width / cof, 1), Math.max(box.height / cof, 1));\r\n logicalBox.y -= Math.round(logicalBox.y / this.layout.pageHeight) / this.layout.svgScale;\r\n if (layoutDebug.mask | layoutDebug.values['mouseDebug']) {\r\n layoutDebug.updateMouseDebug(box, logicalBox, this.containerOffset);\r\n }\r\n return logicalBox;\r\n }\r\n /**\r\n * Convert from SVG bounding box to screen coordinates\r\n * @param box \r\n * @returns \r\n */\r\n svgToClient(box: SvgBox) {\r\n const cof = (this.zoomScale * this.renderScale);\r\n const x = (box.x * cof) + this.containerOffset.x;\r\n const y = (box.y * cof) + this.containerOffset.y;\r\n const clientBox = SvgHelpers.boxPoints(x, y, box.width * cof, box.height * cof);\r\n return clientBox;\r\n }\r\n /**\r\n * Convert from SVG bounding box to screen coordinates\r\n * @param box \r\n * @returns \r\n */\r\n svgToClientNoOffset(box: SvgBox) {\r\n const cof = (this.zoomScale * this.renderScale);\r\n const x = (box.x * cof);\r\n const y = (box.y * cof);\r\n const clientBox = SvgHelpers.boxPoints(x, y, box.width * cof, box.height * cof);\r\n return clientBox;\r\n }\r\n\r\n /**\r\n * Find a selection from a mouse event\r\n * @param box - location of a mouse event or specific screen coordinates\r\n * @returns \r\n */\r\n findArtifact(logicalBox: SvgBox): { selections: SmoSelection[], page: SvgPage} {\r\n const selections: SmoSelection[] = [];\r\n const page = this.getRenderer(logicalBox);\r\n if (page) {\r\n return { selections: page.findArtifact(logicalBox), page };\r\n }\r\n return { selections, page: this.vfRenderers[0] };\r\n }\r\n /**\r\n * Find any modifiers intersecting with `box`\r\n * @param box \r\n * @returns \r\n */\r\n findModifierTabs(logicalBox: SvgBox): ModifierTab[] {\r\n const page = this.getRenderer(logicalBox);\r\n if (page) {\r\n return page.findModifierTabs(logicalBox);\r\n }\r\n return [];\r\n }\r\n /**\r\n * add a rendered page to the page map\r\n * @param selection \r\n * @returns \r\n */\r\n addArtifact(selection: SmoSelection) {\r\n if (!selection.note || !selection.note.logicalBox) {\r\n return;\r\n }\r\n const page = this.getRenderer(selection.note.logicalBox);\r\n if (page) {\r\n page.addArtifact(selection);\r\n }\r\n }\r\n /**\r\n * add a rendered modifier to the page map\r\n * @param modifier \r\n */\r\n addModifierTab(modifier: ModifierTab) {\r\n const page = this.getRenderer(modifier.box);\r\n if (page) {\r\n page.addModifierTab(modifier);\r\n }\r\n }\r\n clearModifiersForPage(page: number) {\r\n if (this.vfRenderers.length > page) {\r\n this.vfRenderers[page].clearModifiers();\r\n }\r\n }\r\n /**\r\n * The number of pages is changing, remove the last page\r\n * @returns \r\n */\r\n removePage() {\r\n let i = 0;\r\n // Don't remove the only page\r\n if (this.vfRenderers.length < 2) {\r\n return;\r\n }\r\n\r\n // Remove last page div\r\n const elementId = 'smoosic-svg-div-' + (this.vfRenderers.length - 1).toString();\r\n const container = document.getElementById(elementId);\r\n if (container) {\r\n container.remove();\r\n }\r\n // pop last renderer off the stack.\r\n const renderers = [];\r\n const layouts = [];\r\n for (i = 0; i < this.vfRenderers.length - 1; ++i) {\r\n renderers.push(this.vfRenderers[i]);\r\n layouts.push(this.pageLayouts[i]);\r\n }\r\n this.vfRenderers = renderers;\r\n this._pageLayouts = layouts;\r\n\r\n // update page height\r\n const totalHeight = this.pageDivHeight * this.pageLayouts.length ;\r\n $(this.container).css('width', '' + Math.round(this.pageDivWidth) + 'px');\r\n $(this.container).css('height', '' + Math.round(totalHeight) + 'px');\r\n }\r\n /**\r\n * The score dimensions have changed, clear maps and recreate the pages.\r\n * @param layout \r\n * @param pageLayouts \r\n */\r\n updateLayout(layout: SmoGlobalLayout, pageLayouts: SmoPageLayout[]) {\r\n this._layout = layout;\r\n this._pageLayouts = pageLayouts;\r\n this.createRenderers();\r\n }\r\n /**\r\n * Return the page by index\r\n * @param page \r\n * @returns \r\n */\r\n getRendererForPage(page: number) {\r\n if (this.vfRenderers.length > page) {\r\n return this.vfRenderers[page];\r\n }\r\n return this.vfRenderers[this.vfRenderers.length - 1];\r\n }\r\n /**\r\n * Return the SvgPage based on SVG point (conversion from client coordinates already done)\r\n * @param point \r\n * @returns \r\n */\r\n getRendererFromPoint(point: SvgPoint): SvgPage | null {\r\n const ix = Math.floor(point.y / (this.layout.pageHeight / this.layout.svgScale));\r\n if (ix < this.vfRenderers.length) {\r\n return this.vfRenderers[ix];\r\n }\r\n return null;\r\n }\r\n /**\r\n * Return the SvgPage based on SVG point (conversion from client coordinates already done)\r\n * @param box \r\n * @returns \r\n */\r\n getRenderer(box: SvgBox | SvgPoint): SvgPage {\r\n const rv = this.getRendererFromPoint({ x: box.x, y: box.y });\r\n if (rv) {\r\n return rv;\r\n }\r\n return this.vfRenderers[0];\r\n }\r\n /**\r\n * Return the page based on the coordinates of a modifier\r\n * @param modifier \r\n * @returns \r\n */\r\n getRendererFromModifier(modifier?: Renderable) {\r\n let rv = this.vfRenderers[0];\r\n if (modifier && modifier.logicalBox) {\r\n const context = this.getRenderer(modifier.logicalBox);\r\n if (context) {\r\n rv = context;\r\n }\r\n }\r\n return rv;\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SuiInlineText, SuiTextBlock } from './textRender';\r\nimport { SuiRenderState } from './renderState';\r\nimport { SuiScroller } from './scroller';\r\nimport { layoutDebug } from './layoutDebug';\r\nimport { PromiseHelpers } from '../../common/promiseHelpers';\r\nimport { OutlineInfo, StrokeInfo, SvgHelpers } from './svgHelpers';\r\nimport { SmoScoreText, SmoTextGroup } from '../../smo/data/scoreText';\r\nimport { SmoLyric } from '../../smo/data/noteModifiers';\r\nimport { SmoSelector } from '../../smo/xform/selections';\r\nimport { SvgBox, KeyEvent } from '../../smo/data/common';\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { SmoScore } from '../../smo/data/score';\r\nimport { SmoSelection } from '../../smo/xform/selections';\r\nimport { SvgPage } from './svgPageMap';\r\nimport { SuiScoreViewOperations } from './scoreViewOperations';\r\nimport { SvgPageMap } from './svgPageMap';\r\nimport { VexFlow, getChordSymbolGlyphFromCode } from '../../common/vex';\r\n\r\nconst VF = VexFlow;\r\ndeclare var $: any;\r\n\r\n/**\r\n * Basic parameters to create a text editor\r\n * @param context Vex renderer context\r\n * @param scroller\r\n * @param x initial x position\r\n * @param y initial y position\r\n * @param text initial text\r\n */\r\nexport interface SuiTextEditorParams {\r\n pageMap: SvgPageMap,\r\n context: SvgPage,\r\n scroller: SuiScroller,\r\n x: number,\r\n y: number,\r\n text: string\r\n}\r\nexport interface SuiLyricEditorParams extends SuiTextEditorParams {\r\n lyric: SmoLyric\r\n}\r\n\r\nexport interface SuiTextSessionParams {\r\n scroller: SuiScroller;\r\n renderer: SuiRenderState;\r\n scoreText: SmoScoreText;\r\n text: string;\r\n x: number;\r\n y: number;\r\n textGroup: SmoTextGroup;\r\n}\r\n\r\nexport interface SuiLyricSessionParams {\r\n score: SmoScore;\r\n renderer: SuiRenderState;\r\n scroller: SuiScroller;\r\n view: SuiScoreViewOperations;\r\n verse: number;\r\n selector: SmoSelector;\r\n}\r\nexport type SuiTextStrokeName = 'text-suggestion' | 'text-selection' | 'text-highlight' | 'text-drag' | 'inactive-text';\r\n/**\r\n * The heirarchy of text editing objects goes:\r\n * \r\n * `dialog -> component -> session -> editor`\r\n * \r\n * Editors and Sessions are defined in this module.\r\n * ### editor\r\n * handles low-level events and renders the preview using one\r\n * of the text layout objects.\r\n * ### session\r\n * creates and destroys editors, e.g. for lyrics that have a Different\r\n * editor instance for each note.\r\n * \r\n * ## SuiTextEditor\r\n * The base text editor handles the positioning and inserting\r\n * of text blocks into the text area. The derived class shoud interpret key events.\r\n * A container class will manage the session for starting/stopping the editor\r\n * and retrieving the results into the target object.\r\n * */\r\nexport class SuiTextEditor {\r\n static get States(): Record {\r\n return { RUNNING: 1, STOPPING: 2, STOPPED: 4, PENDING_EDITOR: 8 };\r\n }\r\n // parsers use this convention to represent text types (superscript)\r\n static textTypeToChar(textType: number): string {\r\n if (textType === SuiInlineText.textTypes.superScript) {\r\n return '^';\r\n }\r\n if (textType === SuiInlineText.textTypes.subScript) {\r\n return '%';\r\n }\r\n return '';\r\n }\r\n\r\n static textTypeFromChar(char: string): number {\r\n if (char === '^') {\r\n return SuiInlineText.textTypes.superScript;\r\n }\r\n if (char === '%') {\r\n return SuiInlineText.textTypes.subScript;\r\n }\r\n return SuiInlineText.textTypes.normal;\r\n }\r\n svgText: SuiInlineText | null = null;\r\n context: SvgPage;\r\n outlineInfo: OutlineInfo | null = null;\r\n pageMap: SvgPageMap;\r\n x: number = 0;\r\n y: number = 0;\r\n text: string;\r\n textPos: number = 0;\r\n selectionStart: number = -1;\r\n selectionLength: number = -1;\r\n empty: boolean = true;\r\n scroller: SuiScroller;\r\n suggestionIndex: number = -1;\r\n cursorState: boolean = false;\r\n cursorRunning: boolean = false;\r\n textType: number = SuiInlineText.textTypes.normal;\r\n fontWeight: string = 'normal';\r\n fontFamily: string = 'Merriweather';\r\n fontSize: number = 14;\r\n state: number = SuiTextEditor.States.RUNNING;\r\n suggestionRect: OutlineInfo | null = null;\r\n constructor(params: SuiTextEditorParams) {\r\n this.scroller = params.scroller;\r\n this.context = params.context;\r\n this.x = params.x;\r\n this.y = params.y;\r\n this.text = params.text;\r\n this.pageMap = params.pageMap;\r\n }\r\n\r\n static get strokes(): Record {\r\n return {\r\n 'text-suggestion': {\r\n strokeName: 'text-suggestion',\r\n stroke: '#cce',\r\n strokeWidth: 1,\r\n strokeDasharray: '4,1',\r\n fill: 'none',\r\n opacity: 1.0\r\n },\r\n 'text-selection': {\r\n strokeName: 'text-selection',\r\n stroke: '#99d',\r\n strokeWidth: 1,\r\n fill: 'none',\r\n strokeDasharray: '',\r\n opacity: 1.0\r\n }, \r\n 'text-highlight': {\r\n strokeName: 'text-highlight',\r\n stroke: '#dd9',\r\n strokeWidth: 1,\r\n strokeDasharray: '4,1',\r\n fill: 'none',\r\n opacity: 1.0\r\n }, \r\n 'text-drag': {\r\n strokeName: 'text-drag',\r\n stroke: '#d99',\r\n strokeWidth: 1,\r\n strokeDasharray: '2,1',\r\n fill: '#eee',\r\n opacity: 0.3\r\n },\r\n 'inactive-text': {\r\n strokeName: 'inactive-text',\r\n stroke: '#fff',\r\n strokeWidth: 1,\r\n strokeDasharray: '',\r\n fill: '#ddd',\r\n opacity: 0.3\r\n }\r\n };\r\n }\r\n\r\n // ### _suggestionParameters\r\n // Create the svg text outline parameters\r\n _suggestionParameters(box: SvgBox, strokeName: SuiTextStrokeName): OutlineInfo {\r\n const outlineStroke = SuiTextEditor.strokes[strokeName];\r\n if (!this.suggestionRect) {\r\n this.suggestionRect = {\r\n context: this.context, box, classes: '',\r\n stroke: outlineStroke, scroll: this.scroller.scrollState, timeOff: 1000\r\n };\r\n };\r\n this.suggestionRect.box = SvgHelpers.smoBox(box);\r\n return this.suggestionRect;\r\n }\r\n\r\n // ### _expandSelectionToSuggestion\r\n // Expand the selection to include the character the user clicked on.\r\n _expandSelectionToSuggestion() {\r\n if (this.suggestionIndex < 0) {\r\n return;\r\n }\r\n if (this.selectionStart < 0) {\r\n this._setSelectionToSugggestion();\r\n return;\r\n } else if (this.selectionStart > this.suggestionIndex) {\r\n const oldStart = this.selectionStart;\r\n this.selectionStart = this.suggestionIndex;\r\n this.selectionLength = (oldStart - this.selectionStart) + this.selectionLength;\r\n } else if (this.selectionStart < this.suggestionIndex\r\n && this.selectionStart > this.selectionStart + this.selectionLength) {\r\n this.selectionLength = (this.suggestionIndex - this.selectionStart) + 1;\r\n }\r\n this._updateSelections();\r\n }\r\n\r\n // ### _setSelectionToSugggestion\r\n // Set the selection to the character the user clicked on.\r\n _setSelectionToSugggestion() {\r\n this.selectionStart = this.suggestionIndex;\r\n this.selectionLength = 1;\r\n this.suggestionIndex = -1;\r\n this._updateSelections();\r\n }\r\n\r\n rerender() {\r\n this.svgText?.unrender();\r\n this.svgText?.render();\r\n }\r\n // ### handleMouseEvent\r\n // Handle hover/click behavior for the text under edit.\r\n // Returns: true if the event was handled here\r\n handleMouseEvent(ev: any): boolean {\r\n let handled = false;\r\n if (this.svgText === null) {\r\n return false;\r\n }\r\n const clientBox = SvgHelpers.boxPoints(\r\n ev.clientX + this.scroller.scrollState.x,\r\n ev.clientY + this.scroller.scrollState.y, \r\n 1, 1);\r\n const logicalBox = this.pageMap.clientToSvg(clientBox);\r\n var blocks = this.svgText.getIntersectingBlocks(logicalBox);\r\n\r\n // The mouse is not over the text\r\n if (!blocks.length) {\r\n if (this.suggestionRect) {\r\n SvgHelpers.eraseOutline(this.suggestionRect);\r\n }\r\n\r\n // If the user clicks and there was a previous selection, treat it as selected\r\n if (ev.type === 'click' && this.suggestionIndex >= 0) {\r\n if (ev.shiftKey) {\r\n this._expandSelectionToSuggestion();\r\n } else {\r\n this._setSelectionToSugggestion();\r\n }\r\n handled = true;\r\n this.rerender();\r\n }\r\n return handled;\r\n }\r\n handled = true;\r\n // outline the text that is hovered. Since mouse is a point\r\n // there should only be 1\r\n blocks.forEach((block) => {\r\n SvgHelpers.outlineRect(this._suggestionParameters(block.box, 'text-suggestion'));\r\n this.suggestionIndex = block.index;\r\n });\r\n // if the user clicked on it, add it to the selection.\r\n if (ev.type === 'click') {\r\n if (this.suggestionRect) {\r\n SvgHelpers.eraseOutline(this.suggestionRect);\r\n }\r\n if (ev.shiftKey) {\r\n this._expandSelectionToSuggestion();\r\n } else {\r\n this._setSelectionToSugggestion();\r\n }\r\n const npos = this.selectionStart + this.selectionLength;\r\n if (npos >= 0 && npos <= this.svgText.blocks.length) {\r\n this.textPos = npos;\r\n }\r\n this.rerender();\r\n }\r\n return handled;\r\n }\r\n\r\n // ### _serviceCursor\r\n // Flash the cursor as a background task\r\n _serviceCursor() {\r\n if (this.cursorState) {\r\n this.svgText?.renderCursorAt(this.textPos - 1, this.textType);\r\n } else {\r\n this.svgText?.removeCursor();\r\n }\r\n this.cursorState = !this.cursorState;\r\n }\r\n // ### _refreshCursor\r\n // If the text position changes, update the cursor position right away\r\n // don't wait for blink.\r\n _refreshCursor() {\r\n this.svgText?.removeCursor();\r\n this.cursorState = true;\r\n this._serviceCursor();\r\n }\r\n\r\n get _endCursorCondition(): boolean {\r\n return this.cursorRunning === false;\r\n }\r\n\r\n _cursorPreResolve() {\r\n this.svgText?.removeCursor();\r\n }\r\n\r\n _cursorPoll() {\r\n this._serviceCursor();\r\n }\r\n\r\n // ### startCursorPromise\r\n // Used by the calling logic to start the cursor.\r\n // returns a promise that can be pended when the editing ends.\r\n startCursorPromise(): Promise {\r\n var self = this;\r\n this.cursorRunning = true;\r\n this.cursorState = true;\r\n self.svgText?.renderCursorAt(this.textPos, SuiInlineText.textTypes.normal);\r\n return PromiseHelpers.makePromise(() => this._endCursorCondition, () => this._cursorPreResolve(), () => this._cursorPoll(), 333);\r\n }\r\n stopCursor() {\r\n this.cursorRunning = false;\r\n }\r\n\r\n // ### setTextPos\r\n // Set the text position within the editor space and update the cursor\r\n setTextPos(val: number) {\r\n this.textPos = val;\r\n this._refreshCursor();\r\n }\r\n // ### moveCursorRight\r\n // move cursor right within the block of text.\r\n moveCursorRight() {\r\n if (this.svgText === null) {\r\n return;\r\n }\r\n if (this.textPos <= this.svgText.blocks.length) {\r\n this.setTextPos(this.textPos + 1);\r\n }\r\n }\r\n // ### moveCursorRight\r\n // move cursor left within the block of text.\r\n moveCursorLeft() {\r\n if (this.textPos > 0) {\r\n this.setTextPos(this.textPos - 1);\r\n }\r\n }\r\n\r\n // ### moveCursorRight\r\n // highlight the text selections\r\n _updateSelections() {\r\n let i = 0;\r\n const end = this.selectionStart + this.selectionLength;\r\n const start = this.selectionStart;\r\n this.svgText?.blocks.forEach((block) => {\r\n const val = start >= 0 && i >= start && i < end;\r\n this.svgText!.setHighlight(block, val);\r\n ++i;\r\n });\r\n }\r\n\r\n // ### _checkGrowSelectionLeft\r\n // grow selection within the bounds\r\n _checkGrowSelectionLeft() {\r\n if (this.selectionStart > 0) {\r\n this.selectionStart -= 1;\r\n this.selectionLength += 1;\r\n }\r\n }\r\n // ### _checkGrowSelectionRight\r\n // grow selection within the bounds\r\n _checkGrowSelectionRight() {\r\n if (this.svgText === null) {\r\n return;\r\n }\r\n const end = this.selectionStart + this.selectionLength;\r\n if (end < this.svgText.blocks.length) {\r\n this.selectionLength += 1;\r\n }\r\n }\r\n\r\n // ### growSelectionLeft\r\n // handle the selection keys\r\n growSelectionLeft() {\r\n if (this.selectionStart === -1) {\r\n this.moveCursorLeft();\r\n this.selectionStart = this.textPos;\r\n this.selectionLength = 1;\r\n } else if (this.textPos === this.selectionStart) {\r\n this.moveCursorLeft();\r\n this._checkGrowSelectionLeft();\r\n }\r\n this._updateSelections();\r\n }\r\n\r\n // ### growSelectionRight\r\n // handle the selection keys\r\n growSelectionRight() {\r\n if (this.selectionStart === -1) {\r\n this.selectionStart = this.textPos;\r\n this.selectionLength = 1;\r\n this.moveCursorRight();\r\n } else if (this.selectionStart + this.selectionLength === this.textPos) {\r\n this._checkGrowSelectionRight();\r\n this.moveCursorRight();\r\n }\r\n this._updateSelections();\r\n }\r\n\r\n // ### _clearSelections\r\n // Clear selected text\r\n _clearSelections() {\r\n this.selectionStart = -1;\r\n this.selectionLength = 0;\r\n }\r\n\r\n // ### deleteSelections\r\n // delete the selected blocks of text/glyphs\r\n deleteSelections() {\r\n let i = 0;\r\n const blockPos = this.selectionStart;\r\n for (i = 0; i < this.selectionLength; ++i) {\r\n this.svgText?.removeBlockAt(blockPos); // delete shifts blocks so keep index the same.\r\n }\r\n this.setTextPos(blockPos);\r\n this.selectionStart = -1;\r\n this.selectionLength = 0;\r\n }\r\n\r\n // ### parseBlocks\r\n // THis can be overridden by the base class to create the correct combination\r\n // of text and glyph blocks based on the underlying text\r\n parseBlocks() {\r\n let i = 0;\r\n \r\n this.svgText = new SuiInlineText({\r\n context: this.context, startX: this.x, startY: this.y,\r\n fontFamily: this.fontFamily, fontSize: this.fontSize, fontWeight: this.fontWeight, scroller: this.scroller,\r\n purpose: SuiInlineText.textPurposes.edit,\r\n fontStyle: 'normal', pageMap: this.pageMap\r\n });\r\n for (i = 0; i < this.text.length; ++i) {\r\n const def = SuiInlineText.blockDefaults;\r\n def.text = this.text[i]\r\n this.svgText.addTextBlockAt(i, def);\r\n this.empty = false;\r\n }\r\n this.textPos = this.text.length;\r\n this.state = SuiTextEditor.States.RUNNING;\r\n this.rerender();\r\n }\r\n // ### evKey\r\n // Handle key events that filter down to the editor\r\n async evKey(evdata: KeyEvent): Promise {\r\n const removeCurrent = () => {\r\n if (this.svgText) {\r\n this.svgText.element?.remove();\r\n this.svgText.element = null;\r\n }\r\n }\r\n if (evdata.code === 'ArrowRight') {\r\n if (evdata.shiftKey) {\r\n this.growSelectionRight();\r\n } else {\r\n this.moveCursorRight();\r\n }\r\n this.rerender();\r\n return true;\r\n }\r\n if (evdata.code === 'ArrowLeft') {\r\n if (evdata.shiftKey) {\r\n this.growSelectionLeft();\r\n } else {\r\n this.moveCursorLeft();\r\n }\r\n this.rerender();\r\n return true;\r\n }\r\n if (evdata.code === 'Backspace') {\r\n removeCurrent();\r\n if (this.selectionStart >= 0) {\r\n this.deleteSelections();\r\n } else {\r\n if (this.textPos > 0) {\r\n this.selectionStart = this.textPos - 1;\r\n this.selectionLength = 1;\r\n this.deleteSelections();\r\n }\r\n }\r\n this.rerender();\r\n return true;\r\n }\r\n if (evdata.code === 'Delete') {\r\n removeCurrent();\r\n if (this.selectionStart >= 0) {\r\n this.deleteSelections();\r\n } else {\r\n if (this.textPos > 0 && this.svgText !== null && this.textPos < this.svgText.blocks.length) {\r\n this.selectionStart = this.textPos;\r\n this.selectionLength = 1;\r\n this.deleteSelections();\r\n }\r\n }\r\n this.rerender();\r\n return true;\r\n }\r\n if (evdata.key.charCodeAt(0) >= 33 && evdata.key.charCodeAt(0) <= 126 && evdata.key.length === 1) {\r\n removeCurrent();\r\n const isPaste = evdata.ctrlKey && evdata.key === 'v';\r\n let text = evdata.key;\r\n if (isPaste) {\r\n text = await navigator.clipboard.readText();\r\n }\r\n if (this.empty) {\r\n this.svgText?.removeBlockAt(0);\r\n this.empty = false;\r\n const def = SuiInlineText.blockDefaults;\r\n def.text = text;\r\n this.svgText?.addTextBlockAt(0, def);\r\n this.setTextPos(1);\r\n } else {\r\n if (this.selectionStart >= 0) {\r\n this.deleteSelections();\r\n }\r\n const def = SuiInlineText.blockDefaults;\r\n def.text = text;\r\n def.textType = this.textType;\r\n this.svgText?.addTextBlockAt(this.textPos, def);\r\n this.setTextPos(this.textPos + 1);\r\n }\r\n this.rerender();\r\n return true;\r\n }\r\n return false;\r\n }\r\n}\r\n\r\nexport class SuiTextBlockEditor extends SuiTextEditor {\r\n // ### ctor\r\n // ### args\r\n // params: {lyric: SmoLyric,...}\r\n constructor(params: SuiTextEditorParams) {\r\n super(params);\r\n $(this.context.svg).find('g.vf-text-highlight').remove();\r\n this.parseBlocks();\r\n }\r\n\r\n _highlightEditor() {\r\n if (this.svgText === null || this.svgText.blocks.length === 0) {\r\n return;\r\n }\r\n const bbox = this.svgText.getLogicalBox();\r\n const outlineStroke = SuiTextEditor.strokes['text-highlight'];\r\n if (this.outlineInfo && this.outlineInfo.element) {\r\n this.outlineInfo.element.remove();\r\n }\r\n this.outlineInfo = {\r\n context: this.context, box: bbox, classes: '',\r\n stroke: outlineStroke, scroll: this.scroller.scrollState,\r\n timeOff: 0\r\n };\r\n SvgHelpers.outlineRect(this.outlineInfo);\r\n }\r\n\r\n getText(): string {\r\n if (this.svgText !== null) {\r\n return this.svgText.getText();\r\n }\r\n return '';\r\n }\r\n\r\n async evKey(evdata: KeyEvent): Promise {\r\n if (evdata.key.charCodeAt(0) === 32) {\r\n if (this.empty) {\r\n this.svgText?.removeBlockAt(0);\r\n this.empty = false;\r\n const def = SuiInlineText.blockDefaults;\r\n def.text = ' ';\r\n this.svgText?.addTextBlockAt(0, def);\r\n this.setTextPos(1);\r\n } else {\r\n if (this.selectionStart >= 0) {\r\n this.deleteSelections();\r\n }\r\n const def = SuiInlineText.blockDefaults;\r\n def.text = ' ';\r\n def.textType = this.textType;\r\n this.svgText?.addTextBlockAt(this.textPos, def);\r\n this.setTextPos(this.textPos + 1);\r\n }\r\n this.rerender();\r\n return true;\r\n }\r\n const rv = super.evKey(evdata);\r\n this._highlightEditor();\r\n return rv;\r\n }\r\n\r\n stopEditor() {\r\n this.state = SuiTextEditor.States.STOPPING;\r\n $(this.context.svg).find('g.vf-text-highlight').remove();\r\n this.stopCursor();\r\n this.svgText?.unrender();\r\n }\r\n}\r\n\r\nexport class SuiLyricEditor extends SuiTextEditor {\r\n static get States() {\r\n return { RUNNING: 1, STOPPING: 2, STOPPED: 4 };\r\n }\r\n parseBlocks() {\r\n let i = 0;\r\n const def = SuiInlineText.defaults;\r\n def.context = this.context;\r\n def.startX = this.x;\r\n def.startY = this.y;\r\n def.scroller = this.scroller;\r\n this.svgText = new SuiInlineText(def);\r\n for (i = 0; i < this.text.length; ++i) {\r\n const blockP = SuiInlineText.blockDefaults;\r\n blockP.text = this.text[i];\r\n this.svgText.addTextBlockAt(i, blockP);\r\n this.empty = false;\r\n }\r\n this.textPos = this.text.length;\r\n this.state = SuiTextEditor.States.RUNNING;\r\n this.rerender();\r\n }\r\n\r\n getText(): string {\r\n if (this.svgText !== null) {\r\n return this.svgText.getText();\r\n }\r\n return '';\r\n }\r\n lyric: SmoLyric;\r\n state: number = SuiTextEditor.States.PENDING_EDITOR;\r\n\r\n // ### ctor\r\n // ### args\r\n // params: {lyric: SmoLyric,...}\r\n constructor(params: SuiLyricEditorParams) {\r\n super(params);\r\n this.text = params.lyric.getText();\r\n if (params.lyric.isHyphenated()) {\r\n this.text += '-';\r\n }\r\n this.lyric = params.lyric;\r\n this.parseBlocks();\r\n }\r\n\r\n stopEditor() {\r\n this.state = SuiTextEditor.States.STOPPING;\r\n this.stopCursor();\r\n if (this.svgText !== null) {\r\n this.svgText.unrender();\r\n }\r\n }\r\n}\r\n\r\nexport class SuiChordEditor extends SuiTextEditor {\r\n static get States() {\r\n return { RUNNING: 1, STOPPING: 2, STOPPED: 4 };\r\n }\r\n static get SymbolModifiers() {\r\n return {\r\n NONE: 1,\r\n SUBSCRIPT: 2,\r\n SUPERSCRIPT: 3\r\n };\r\n }\r\n\r\n // ### toTextTypeChar\r\n // Given an old text type and a desited new text type,\r\n // return what the new text type character should be\r\n static toTextTypeChar(oldTextType: number, newTextType: number): string {\r\n const tt = SuiInlineText.getTextTypeResult(oldTextType, newTextType);\r\n return SuiTextEditor.textTypeToChar(tt);\r\n }\r\n\r\n static toTextTypeTransition(oldTextType: number, result: number): string {\r\n const tt = SuiInlineText.getTextTypeTransition(oldTextType, result);\r\n return SuiTextEditor.textTypeToChar(tt);\r\n }\r\n\r\n setTextType(textType: number) {\r\n this.textType = textType;\r\n }\r\n\r\n // Handle the case where user changed super/subscript in the middle of the\r\n // string.\r\n _updateSymbolModifiers() {\r\n let change = this.textPos;\r\n let render = false;\r\n let i = 0;\r\n for (i = this.textPos; this.svgText !== null && i < this.svgText.blocks.length; ++i) {\r\n const block = this.svgText!.blocks[i];\r\n if (block.textType !== this.textType &&\r\n block.textType !== change) {\r\n change = block.textType;\r\n block.textType = this.textType;\r\n render = true;\r\n } else {\r\n break;\r\n }\r\n }\r\n if (render) {\r\n this.rerender();\r\n }\r\n }\r\n _setSymbolModifier(char: string): boolean {\r\n if (['^', '%'].indexOf(char) < 0) {\r\n return false;\r\n }\r\n const currentTextType = this.textType;\r\n const transitionType = SuiTextEditor.textTypeFromChar(char);\r\n this.textType = SuiInlineText.getTextTypeResult(currentTextType, transitionType);\r\n this._updateSymbolModifiers();\r\n return true;\r\n }\r\n\r\n parseBlocks() {\r\n let readGlyph = false;\r\n let curGlyph = '';\r\n let blockIx = 0; // so we skip modifier characters\r\n let i = 0;\r\n const params = SuiInlineText.defaults;\r\n params.context = this.context;\r\n params.startX = this.x;\r\n params.startY = this.y;\r\n params.scroller = this.scroller;\r\n this.svgText = new SuiInlineText(params);\r\n\r\n for (i = 0; i < this.text.length; ++i) {\r\n const char = this.text[i];\r\n const isSymbolModifier = this._setSymbolModifier(char);\r\n if (char === '@') {\r\n if (!readGlyph) {\r\n readGlyph = true;\r\n curGlyph = '';\r\n } else {\r\n this._addGlyphAt(blockIx, curGlyph);\r\n blockIx += 1;\r\n readGlyph = false;\r\n }\r\n } else if (!isSymbolModifier) {\r\n if (readGlyph) {\r\n curGlyph = curGlyph + char;\r\n } else {\r\n const blockP = SuiInlineText.blockDefaults;\r\n blockP.text = char;\r\n blockP.textType = this.textType;\r\n this.svgText.addTextBlockAt(blockIx, blockP);\r\n blockIx += 1;\r\n }\r\n }\r\n this.empty = false;\r\n }\r\n this.textPos = blockIx;\r\n this.state = SuiTextEditor.States.RUNNING;\r\n this.rerender();\r\n }\r\n\r\n // ### getText\r\n // Get the text value that we persist\r\n getText(): string {\r\n if (this.svgText === null || this.svgText.blocks.length < 1) {\r\n return '';\r\n }\r\n let text = '';\r\n let textType = this.svgText.blocks[0].textType;\r\n this.svgText.blocks.forEach((block) => {\r\n if (block.textType !== textType) {\r\n text += SuiChordEditor.toTextTypeTransition(textType, block.textType);\r\n textType = block.textType;\r\n }\r\n if (block.symbolType === SuiInlineText.symbolTypes.GLYPH) {\r\n text += '@' + block.glyphCode + '@';\r\n } else {\r\n text += block.text;\r\n }\r\n });\r\n return text;\r\n }\r\n\r\n _addGlyphAt(ix: number, code: string) {\r\n if (this.selectionStart >= 0) {\r\n this.deleteSelections();\r\n }\r\n const blockP = SuiInlineText.blockDefaults;\r\n blockP.glyphCode = code;\r\n blockP.textType = this.textType;\r\n this.svgText?.addGlyphBlockAt(ix, blockP);\r\n this.textPos += 1;\r\n }\r\n unrender() {\r\n if (this.svgText) {\r\n this.svgText.element?.remove();\r\n }\r\n }\r\n async evKey(evdata: KeyEvent): Promise {\r\n let edited = false;\r\n if (this._setSymbolModifier(evdata.key)) {\r\n return true;\r\n }\r\n // Dialog gives us a specific glyph code\r\n if (evdata.key[0] === '@' && evdata.key.length > 2) {\r\n this.unrender();\r\n const glyph = evdata.key.substr(1, evdata.key.length - 2);\r\n this._addGlyphAt(this.textPos, getChordSymbolGlyphFromCode(glyph));\r\n this.rerender();\r\n edited = true;\r\n } else if (VF.ChordSymbol.glyphs[evdata.key[0]]) { // glyph shortcut like 'b'\r\n this.unrender();\r\n // hack: vexflow 5 broke this\r\n this._addGlyphAt(this.textPos, evdata.key[0]);\r\n this.rerender();\r\n edited = true;\r\n } else {\r\n // some ordinary key\r\n edited = await super.evKey(evdata);\r\n }\r\n if (this.svgText !== null && this.svgText.blocks.length > this.textPos && this.textPos >= 0) {\r\n this.textType = this.svgText.blocks[this.textPos].textType;\r\n }\r\n return edited;\r\n }\r\n lyric: SmoLyric;\r\n\r\n // ### ctor\r\n // ### args\r\n // params: {lyric: SmoLyric,...}\r\n constructor(params: SuiLyricEditorParams) {\r\n super(params);\r\n this.text = params.lyric.text;\r\n this.lyric = params.lyric;\r\n this.textType = SuiInlineText.textTypes.normal;\r\n this.parseBlocks();\r\n }\r\n\r\n stopEditor() {\r\n this.state = SuiTextEditor.States.STOPPING;\r\n this.stopCursor();\r\n this.svgText?.unrender();\r\n }\r\n\r\n // ### _markStopped\r\n // Indicate this editor session is done running\r\n _markStopped() {\r\n this.state = SuiTextEditor.States.STOPPED;\r\n }\r\n}\r\nexport interface SuiDragSessionParams {\r\n context: SvgPageMap;\r\n scroller: SuiScroller;\r\n textGroup: SmoTextGroup;\r\n}\r\n\r\nexport class SuiDragSession {\r\n pageMap: SvgPageMap;\r\n page: SvgPage;\r\n scroller: SuiScroller;\r\n outlineBox: SvgBox;\r\n textObject: SuiTextBlock;\r\n dragging: boolean = false;\r\n outlineRect: OutlineInfo | null = null;\r\n textGroup: SmoTextGroup;\r\n constructor(params: SuiDragSessionParams) {\r\n this.textGroup = params.textGroup;\r\n this.pageMap = params.context;\r\n this.scroller = params.scroller;\r\n this.page = this.pageMap.getRendererFromModifier(this.textGroup);\r\n // create a temporary text object for dragging\r\n this.textObject = SuiTextBlock.fromTextGroup(this.textGroup, this.page, this.pageMap, this.scroller); // SuiTextBlock\r\n this.dragging = false;\r\n this.outlineBox = this.textObject.getLogicalBox();\r\n }\r\n\r\n _outlineBox() {\r\n const outlineStroke = SuiTextEditor.strokes['text-drag'];\r\n const x = this.outlineBox.x - this.page.box.x;\r\n const y = this.outlineBox.y - this.page.box.y;\r\n if (!this.outlineRect) {\r\n this.outlineRect = {\r\n context: this.page, \r\n box: SvgHelpers.boxPoints(x , y + this.outlineBox.height, this.outlineBox.width, this.outlineBox.height),\r\n classes: 'text-drag',\r\n stroke: outlineStroke, scroll: this.scroller.scrollState, timeOff: 1000\r\n };\r\n }\r\n this.outlineRect.box = SvgHelpers.boxPoints(x , y + this.outlineBox.height, this.outlineBox.width, this.outlineBox.height),\r\n SvgHelpers.outlineRect(this.outlineRect);\r\n }\r\n unrender() {\r\n this.textGroup.elements.forEach((el) => {\r\n el.remove();\r\n });\r\n this.textGroup.elements = [];\r\n this.textObject.unrender();\r\n }\r\n scrolledClientBox(x: number, y: number) {\r\n return { x: x + this.scroller.scrollState.x, y: y + this.scroller.scrollState.y, width: 1, height: 1 };\r\n }\r\n checkBounds() {\r\n if (this.outlineBox.y < this.outlineBox.height) {\r\n this.outlineBox.y = this.outlineBox.height;\r\n }\r\n if (this.outlineBox.x < 0) {\r\n this.outlineBox.x = 0;\r\n }\r\n if (this.outlineBox.x > this.page.box.x + this.page.box.width - this.outlineBox.width) {\r\n this.outlineBox.x = this.page.box.x + this.page.box.width - this.outlineBox.width;\r\n }\r\n if (this.outlineBox.y > this.page.box.y + this.page.box.height) {\r\n this.outlineBox.y = this.page.box.y + this.page.box.height;\r\n }\r\n }\r\n startDrag(e: any) {\r\n const evBox = this.scrolledClientBox(e.clientX, e.clientY);\r\n const svgMouseBox = this.pageMap.clientToSvg(evBox);\r\n svgMouseBox.y -= this.outlineBox.height;\r\n if (layoutDebug.mask & layoutDebug.values['dragDebug']) {\r\n layoutDebug.updateDragDebug(svgMouseBox, this.outlineBox, 'start');\r\n }\r\n if (!SvgHelpers.doesBox1ContainBox2(this.outlineBox, svgMouseBox)) {\r\n return;\r\n }\r\n this.dragging = true;\r\n this.outlineBox = svgMouseBox;\r\n const currentBox = this.textObject.getLogicalBox();\r\n this.outlineBox.width = currentBox.width;\r\n this.outlineBox.height = currentBox.height;\r\n this.unrender();\r\n this.checkBounds();\r\n this._outlineBox();\r\n }\r\n\r\n mouseMove(e: any) {\r\n if (!this.dragging) {\r\n return;\r\n }\r\n const evBox = this.scrolledClientBox(e.clientX, e.clientY);\r\n const svgMouseBox = this.pageMap.clientToSvg(evBox);\r\n svgMouseBox.y -= this.outlineBox.height;\r\n this.outlineBox = SvgHelpers.smoBox(svgMouseBox);\r\n const currentBox = this.textObject.getLogicalBox();\r\n this.outlineBox.width = currentBox.width;\r\n this.outlineBox.height = currentBox.height;\r\n this.checkBounds();\r\n\r\n this.textObject.offsetStartX(this.outlineBox.x - currentBox.x);\r\n this.textObject.offsetStartY(this.outlineBox.y - currentBox.y);\r\n this.textObject.render();\r\n if (layoutDebug.mask & layoutDebug.values['dragDebug']) {\r\n layoutDebug.updateDragDebug(svgMouseBox, this.outlineBox, 'drag');\r\n }\r\n if (this.outlineRect) {\r\n SvgHelpers.eraseOutline(this.outlineRect);\r\n this.outlineRect = null;\r\n }\r\n this._outlineBox();\r\n }\r\n\r\n endDrag() {\r\n // this.textObject.render();\r\n const newBox = this.textObject.getLogicalBox();\r\n const curBox = this.textGroup.logicalBox ?? SvgBox.default;\r\n if (layoutDebug.mask & layoutDebug.values['dragDebug']) {\r\n layoutDebug.updateDragDebug(curBox, newBox, 'end');\r\n }\r\n this.textGroup.offsetX(newBox.x - curBox.x);\r\n this.textGroup.offsetY(newBox.y - curBox.y + this.outlineBox.height);\r\n this.dragging = false;\r\n if (this.outlineRect) {\r\n SvgHelpers.eraseOutline(this.outlineRect);\r\n this.outlineRect = null;\r\n }\r\n }\r\n}\r\n\r\n// ## SuiTextSession\r\n// session for editing plain text\r\nexport class SuiTextSession {\r\n static get States() {\r\n return { RUNNING: 1, STOPPING: 2, STOPPED: 4, PENDING_EDITOR: 8 };\r\n }\r\n scroller: SuiScroller;\r\n scoreText: SmoScoreText;\r\n text: string;\r\n x: number;\r\n y: number;\r\n textGroup: SmoTextGroup;\r\n fontFamily: string = '';\r\n fontWeight: string = '';\r\n fontSize: number = 14;\r\n state: number = SuiTextEditor.States.PENDING_EDITOR;\r\n editor: SuiTextBlockEditor | null = null;\r\n renderer: SuiRenderState;\r\n cursorPromise: Promise | null = null;\r\n constructor(params: SuiTextSessionParams) {\r\n this.scroller = params.scroller;\r\n this.renderer = params.renderer;\r\n this.scoreText = params.scoreText;\r\n this.text = this.scoreText.text;\r\n this.x = params.x;\r\n this.y = params.y;\r\n this.textGroup = params.textGroup;\r\n this.renderer = params.renderer;\r\n\r\n // Create a text group if one was not a startup parameter\r\n if (!this.textGroup) {\r\n this.textGroup = new SmoTextGroup(SmoTextGroup.defaults);\r\n }\r\n // Create a scoreText if one was not a startup parameter, or\r\n // get it from the text group\r\n if (!this.scoreText) {\r\n if (this.textGroup && this.textGroup.textBlocks.length) {\r\n this.scoreText = this.textGroup.textBlocks[0].text;\r\n } else {\r\n const stDef = SmoScoreText.defaults;\r\n stDef.x = this.x;\r\n stDef.y = this.y;\r\n this.scoreText = new SmoScoreText(stDef);\r\n this.textGroup.addScoreText(this.scoreText, SmoTextGroup.relativePositions.RIGHT);\r\n }\r\n }\r\n this.fontFamily = SmoScoreText.familyString(this.scoreText.fontInfo.family);\r\n this.fontWeight = SmoScoreText.weightString(this.scoreText.fontInfo.weight);\r\n this.fontSize = SmoScoreText.fontPointSize(this.scoreText.fontInfo.size);\r\n this.text = this.scoreText.text;\r\n }\r\n\r\n // ### _isRefreshed\r\n // renderer has partially rendered text(promise condition)\r\n get _isRefreshed(): boolean {\r\n return this.renderer.dirty === false;\r\n }\r\n\r\n get isStopped(): boolean {\r\n return this.state === SuiTextEditor.States.STOPPED;\r\n }\r\n\r\n get isRunning(): boolean {\r\n return this.state === SuiTextEditor.States.RUNNING;\r\n }\r\n\r\n _markStopped() {\r\n this.state = SuiTextEditor.States.STOPPED;\r\n }\r\n\r\n // ### _isRendered\r\n // renderer has rendered text(promise condition)\r\n get _isRendered(): boolean {\r\n return this.renderer.passState === SuiRenderState.passStates.clean;\r\n }\r\n\r\n _removeScoreText() {\r\n const selector = '#' + this.scoreText.attrs.id;\r\n $(selector).remove();\r\n }\r\n\r\n // ### _startSessionForNote\r\n // Start the lyric session\r\n startSession() {\r\n const context = this.renderer.pageMap.getRenderer({ x: this.x, y: this.y });\r\n if (context) {\r\n this.editor = new SuiTextBlockEditor({\r\n x: this.x, y: this.y, scroller: this.scroller,\r\n context: context, text: this.scoreText.text, pageMap: this.renderer.pageMap\r\n });\r\n this.cursorPromise = this.editor.startCursorPromise();\r\n this.state = SuiTextEditor.States.RUNNING;\r\n this._removeScoreText();\r\n }\r\n }\r\n\r\n // ### _startSessionForNote\r\n // Stop the lyric session, return promise for done\r\n stopSession(): Promise {\r\n if (this.editor) {\r\n this.scoreText.text = this.editor.getText();\r\n this.scoreText.tryParseUnicode(); // convert unicode chars\r\n this.editor.stopEditor();\r\n }\r\n return PromiseHelpers.makePromise(()=> this._isRendered,() => this._markStopped(), null, 100);\r\n }\r\n\r\n // ### evKey\r\n // Key handler (pass to editor)\r\n async evKey(evdata: KeyEvent): Promise {\r\n if (this.state !== SuiTextEditor.States.RUNNING || this.editor === null) {\r\n return false;\r\n }\r\n const rv = await this.editor.evKey(evdata);\r\n if (rv) {\r\n this._removeScoreText();\r\n }\r\n return rv;\r\n }\r\n\r\n handleMouseEvent(ev: any) {\r\n if (this.isRunning && this.editor !== null) {\r\n this.editor.handleMouseEvent(ev);\r\n }\r\n }\r\n}\r\n// ## SuiLyricSession\r\n// Manage editor for lyrics, jupmping from note to note if asked\r\nexport class SuiLyricSession {\r\n static get States() {\r\n return { RUNNING: 1, STOPPING: 2, STOPPED: 4, PENDING_EDITOR: 8 };\r\n }\r\n score: SmoScore;\r\n renderer: SuiRenderState;\r\n scroller: SuiScroller;\r\n view: SuiScoreViewOperations;\r\n parser: number;\r\n verse: number;\r\n selector: SmoSelector;\r\n selection: SmoSelection | null;\r\n note: SmoNote | null = null;\r\n originalText: string;\r\n lyric: SmoLyric | null = null;\r\n text: string = '';\r\n editor: SuiLyricEditor | null = null;\r\n state: number = SuiTextEditor.States.PENDING_EDITOR;\r\n cursorPromise: Promise | null = null;\r\n constructor(params: SuiLyricSessionParams) {\r\n this.score = params.score;\r\n this.renderer = params.renderer;\r\n this.scroller = params.scroller;\r\n this.view = params.view;\r\n this.parser = SmoLyric.parsers.lyric;\r\n this.verse = params.verse;\r\n this.selector = params.selector;\r\n this.selection = SmoSelection.noteFromSelector(this.score, this.selector);\r\n if (this.selection !== null) {\r\n this.note = this.selection.note;\r\n }\r\n this.originalText = '';\r\n }\r\n\r\n // ### _setLyricForNote\r\n // Get the text from the editor and update the lyric with it.\r\n _setLyricForNote() {\r\n this.lyric = null;\r\n if (!this.note) {\r\n return;\r\n }\r\n const lar = this.note.getLyricForVerse(this.verse, SmoLyric.parsers.lyric);\r\n if (lar.length) {\r\n this.lyric = lar[0] as SmoLyric;\r\n }\r\n if (!this.lyric) {\r\n const scoreFont = this.score.fonts.find((fn) => fn.name === 'lyrics');\r\n const fontInfo = JSON.parse(JSON.stringify(scoreFont));\r\n const lyricD = SmoLyric.defaults;\r\n lyricD.text = '';\r\n lyricD.verse = this.verse;\r\n lyricD.fontInfo = fontInfo;\r\n this.lyric = new SmoLyric(lyricD);\r\n }\r\n this.text = this.lyric.text;\r\n this.originalText = this.text;\r\n // this.view.addOrUpdateLyric(this.selection.selector, this.lyric);\r\n }\r\n\r\n // ### _endLyricCondition\r\n // Lyric editor has stopped running (promise condition)\r\n get _endLyricCondition(): boolean {\r\n return this.editor !== null && this.editor.state !== SuiTextEditor.States.RUNNING;\r\n }\r\n\r\n // ### _endLyricCondition\r\n // renderer has partially rendered text(promise condition)\r\n get _isRefreshed(): boolean {\r\n return this.renderer.renderStateRendered;\r\n }\r\n\r\n // ### _isRendered\r\n // renderer has rendered text(promise condition)\r\n get _isRendered(): boolean {\r\n return this.renderer.renderStateClean;\r\n }\r\n\r\n get _pendingEditor(): boolean {\r\n return this.state !== SuiTextEditor.States.PENDING_EDITOR;\r\n }\r\n\r\n // ### _hideLyric\r\n // Hide the lyric so you only see the editor.\r\n _hideLyric() {\r\n if (this.lyric !== null && this.lyric.selector) {\r\n $(this.lyric.selector).remove();\r\n }\r\n }\r\n\r\n get isStopped(): boolean {\r\n return this.state === SuiTextEditor.States.STOPPED;\r\n }\r\n\r\n get isRunning(): boolean {\r\n return this.state === SuiTextEditor.States.RUNNING;\r\n }\r\n\r\n // ### _markStopped\r\n // Indicate this editor session is done running\r\n _markStopped() {\r\n this.state = SuiTextEditor.States.STOPPED;\r\n }\r\n\r\n // ### _startSessionForNote\r\n // Start the lyric editor for a note (current selected note)\r\n _startSessionForNote() {\r\n if (this.lyric === null || this.note === null || this.note.logicalBox === null) {\r\n return;\r\n }\r\n let startX = this.note.logicalBox.x;\r\n let startY = this.note.logicalBox.y + this.note.logicalBox.height + \r\n SmoScoreText.fontPointSize(this.lyric.fontInfo.size);\r\n this.lyric.skipRender = true;\r\n const lyricRendered = this.lyric.text.length > 0;\r\n if (this.lyric.logicalBox !== null) {\r\n startX = this.lyric.logicalBox.x;\r\n startY = this.lyric.logicalBox.y + this.lyric.logicalBox.height;\r\n }\r\n const context = this.view.renderer.pageMap.getRenderer({ x: startX, y: startY });\r\n if (context) {\r\n this.editor = new SuiLyricEditor({\r\n context,\r\n lyric: this.lyric, x: startX, y: startY, scroller: this.scroller,\r\n text: this.lyric.getText(),\r\n pageMap: this.renderer.pageMap\r\n });\r\n this.state = SuiTextEditor.States.RUNNING;\r\n if (!lyricRendered && this.editor !== null && this.editor.svgText !== null) {\r\n const delta = 2 * this.editor.svgText.maxFontHeight(1.0) * (this.lyric.verse + 1);\r\n this.editor.svgText.offsetStartY(delta);\r\n }\r\n this.cursorPromise = this.editor.startCursorPromise();\r\n this._hideLyric();\r\n \r\n }\r\n }\r\n\r\n // ### _startSessionForNote\r\n // Start the lyric session\r\n startSession() {\r\n this._setLyricForNote();\r\n this._startSessionForNote();\r\n this.state = SuiTextEditor.States.RUNNING;\r\n }\r\n\r\n // ### _startSessionForNote\r\n // Stop the lyric session, return promise for done\r\n async stopSession() {\r\n if (this.editor && !this._endLyricCondition) {\r\n await this._updateLyricFromEditor();\r\n this.editor.stopEditor();\r\n }\r\n return PromiseHelpers.makePromise(() => this._isRendered, () => this._markStopped(), null, 100);\r\n }\r\n\r\n // ### _advanceSelection\r\n // Based on a skip character, move the editor forward/back one note.\r\n async _advanceSelection(isShift: boolean) {\r\n const nextSelection = isShift ? SmoSelection.lastNoteSelectionFromSelector(this.score, this.selector)\r\n : SmoSelection.nextNoteSelectionFromSelector(this.score, this.selector);\r\n if (nextSelection) {\r\n this.selector = nextSelection.selector;\r\n this.selection = nextSelection;\r\n this.note = nextSelection.note;\r\n this._setLyricForNote();\r\n const conditionArray: any = [];\r\n this.state = SuiTextEditor.States.PENDING_EDITOR;\r\n conditionArray.push(PromiseHelpers.makePromiseObj(() => this._endLyricCondition, null, null, 100));\r\n conditionArray.push(PromiseHelpers.makePromiseObj(() => this._isRefreshed,() => this._startSessionForNote(), null, 100));\r\n await PromiseHelpers.promiseChainThen(conditionArray);\r\n }\r\n }\r\n\r\n // ### advanceSelection\r\n // external interfoace to move to next/last note\r\n async advanceSelection(isShift: boolean) {\r\n if (this.isRunning) {\r\n await this._updateLyricFromEditor();\r\n await this._advanceSelection(isShift);\r\n }\r\n }\r\n\r\n async removeLyric() {\r\n if (this.selection && this.lyric) {\r\n await this.view.removeLyric(this.selection.selector, this.lyric);\r\n this.lyric.skipRender = true;\r\n await this.advanceSelection(false);\r\n }\r\n }\r\n\r\n // ### _updateLyricFromEditor\r\n // The editor is done running, so update the lyric now.\r\n async _updateLyricFromEditor() {\r\n if (this.editor === null || this.lyric === null) {\r\n return;\r\n }\r\n const txt = this.editor.getText();\r\n this.lyric.setText(txt);\r\n this.lyric.skipRender = false;\r\n this.editor.stopEditor();\r\n if (!this.lyric.deleted && this.originalText !== txt && this.selection !== null) {\r\n await this.view.addOrUpdateLyric(this.selection.selector, this.lyric);\r\n }\r\n }\r\n // ### evKey\r\n // Key handler (pass to editor)\r\n async evKey(evdata: KeyEvent): Promise {\r\n if (this.state !== SuiTextEditor.States.RUNNING) {\r\n return false;\r\n }\r\n if (evdata.key === '-' || evdata.key === ' ') {\r\n // skip\r\n const back = evdata.shiftKey && evdata.key === ' ';\r\n if (evdata.key === '-' && this.editor !== null) {\r\n await this.editor.evKey(evdata);\r\n }\r\n this._updateLyricFromEditor();\r\n this._advanceSelection(back);\r\n } else if (this.editor !== null) {\r\n await this.editor.evKey(evdata);\r\n this._hideLyric();\r\n }\r\n return true;\r\n }\r\n get textType(): number {\r\n if (this.isRunning && this.editor !== null) {\r\n return this.editor.textType;\r\n }\r\n return SuiInlineText.textTypes.normal;\r\n }\r\n\r\n set textType(type) {\r\n if (this.editor) {\r\n this.editor.textType = type;\r\n }\r\n }\r\n // ### handleMouseEvent\r\n // Mouse event (send to editor)\r\n handleMouseEvent(ev: any) {\r\n if (this.state !== SuiTextEditor.States.RUNNING || this.editor === null) {\r\n return;\r\n }\r\n this.editor.handleMouseEvent(ev);\r\n }\r\n}\r\n\r\nexport class SuiChordSession extends SuiLyricSession {\r\n editor: SuiLyricEditor | null = null;\r\n constructor(params: SuiLyricSessionParams) {\r\n super(params);\r\n this.parser = SmoLyric.parsers.chord;\r\n }\r\n\r\n // ### evKey\r\n // Key handler (pass to editor)\r\n async evKey(evdata: KeyEvent): Promise {\r\n let edited = false;\r\n if (this.state !== SuiTextEditor.States.RUNNING) {\r\n return false;\r\n }\r\n if (evdata.code === 'Enter') {\r\n this._updateLyricFromEditor();\r\n this._advanceSelection(evdata.shiftKey);\r\n edited = true;\r\n } else if (this.editor !== null) {\r\n edited = await this.editor.evKey(evdata);\r\n }\r\n this._hideLyric();\r\n return edited;\r\n }\r\n\r\n // ### _setLyricForNote\r\n // Get the text from the editor and update the lyric with it.\r\n _setLyricForNote() {\r\n this.lyric = null;\r\n if (this.note === null) {\r\n return;\r\n }\r\n const lar = this.note.getLyricForVerse(this.verse, this.parser);\r\n if (lar.length) {\r\n this.lyric = lar[0] as SmoLyric;\r\n }\r\n if (!this.lyric) {\r\n const scoreFont = this.score.fonts.find((fn) => fn.name === 'chords');\r\n const fontInfo = JSON.parse(JSON.stringify(scoreFont));\r\n const ldef = SmoLyric.defaults;\r\n ldef.text = '';\r\n ldef.verse = this.verse;\r\n ldef.parser = this.parser;\r\n ldef.fontInfo = fontInfo;\r\n this.lyric = new SmoLyric(ldef);\r\n this.note.addLyric(this.lyric);\r\n }\r\n this.text = this.lyric.text;\r\n }\r\n // ### _startSessionForNote\r\n // Start the lyric editor for a note (current selected note)\r\n _startSessionForNote() {\r\n if (this.lyric === null) {\r\n return;\r\n }\r\n if (this.selection === null || this.note === null || this.note.logicalBox === null) {\r\n return;\r\n }\r\n let startX = this.note.logicalBox.x;\r\n let startY = this.selection.measure.svg.logicalBox.y;\r\n if (this.lyric.logicalBox !== null) {\r\n startX = this.lyric.logicalBox.x;\r\n startY = this.lyric.logicalBox.y + this.lyric.logicalBox.height;\r\n }\r\n this.selection.measure.svg.logicalBox.y + this.selection.measure.svg.logicalBox.height - 70;\r\n const context = this.renderer.pageMap.getRenderer({ x: startX, y: startY });\r\n if (context) {\r\n this.editor = new SuiChordEditor({\r\n context,\r\n lyric: this.lyric, x: startX, y: startY, scroller: this.scroller,\r\n text: this.lyric.getText(),\r\n pageMap: this.renderer.pageMap\r\n });\r\n this.state = SuiTextEditor.States.RUNNING;\r\n if (this.editor !== null && this.editor.svgText !== null) {\r\n const delta = (-1) * this.editor.svgText.maxFontHeight(1.0) * (this.lyric.verse + 1);\r\n this.editor.svgText.offsetStartY(delta);\r\n }\r\n this.cursorPromise = this.editor.startCursorPromise();\r\n this._hideLyric(); \r\n }\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SvgHelpers, OutlineInfo } from './svgHelpers';\r\nimport { SmoTextGroup, SmoScoreText } from '../../smo/data/scoreText';\r\nimport { SuiTextEditor } from './textEdit';\r\nimport { SuiScroller } from './scroller';\r\nimport { SmoAttrs, SvgBox, getId } from '../../smo/data/common';\r\nimport { SvgPage, SvgPageMap } from './svgPageMap';\r\nimport { smoSerialize } from '../../common/serializationHelpers';\r\nimport { VexFlow,\r\n chordSubscriptOffset, chordSuperscriptOffset, FontInfo, getVexGlyphFromChordCode, \r\n getChordSymbolMetricsForGlyph, blockMetricsYShift } from '../../common/vex';\r\nimport { TextFormatter } from '../../common/textformatter';\r\ndeclare var $: any;\r\nconst VF = VexFlow;\r\n\r\n// From textfont.ts in VF\r\n\r\n/**\r\n * parameters to render text\r\n * @category SuiParameters\r\n */\r\nexport interface SuiInlineTextParams {\r\n fontFamily: string,\r\n fontWeight: string,\r\n fontSize: number,\r\n fontStyle: string,\r\n startX: number,\r\n startY: number,\r\n scroller: SuiScroller,\r\n purpose: string,\r\n context: SvgPage,\r\n pageMap: SvgPageMap\r\n}\r\n/**\r\n * metrics for a single line of text. A textGroup can be composed\r\n * of multiple inline blocks.\r\n * @category SuiParameters\r\n */\r\nexport interface SuiInlineBlock {\r\n symbolType: number,\r\n textType: number,\r\n highlighted: boolean,\r\n x: number,\r\n y: number,\r\n width: number,\r\n height: number,\r\n scale: number,\r\n metrics: any,\r\n glyph: any,\r\n glyphCode: string,\r\n text: string\r\n}\r\nexport interface SuiInlineArtifact {\r\n block: SuiInlineBlock,\r\n box: SvgBox,\r\n index: number\r\n}\r\n// ## textRender.js\r\n// Classes responsible for formatting and rendering text in SVG space.\r\n\r\n/**\r\n * Inline text is a block of SVG text with the same font. Each block can\r\n * contain either text or an svg (vex) glyph. Each block in the text has its own\r\n * metrics so we can support inline svg text editors (cursor).\r\n * @category SuiRender\r\n */\r\nexport class SuiInlineText {\r\n static get textTypes() {\r\n return { normal: 0, superScript: 1, subScript: 2 };\r\n }\r\n static get symbolTypes() {\r\n return {\r\n GLYPH: 1,\r\n TEXT: 2,\r\n LINE: 3\r\n };\r\n }\r\n static get textPurposes(): Record {\r\n return {render: 'sui-inline-render', edit: 'sui-inline-edit' };\r\n }\r\n\r\n // ### textTypeTransitions\r\n // Given a current text type and a type change request, what is the result\r\n // text type? This truth table tells you.\r\n static get textTypeTransitions(): number[][] {\r\n return [\r\n [1, 1, 0],\r\n [1, 0, 1],\r\n [1, 2, 2],\r\n [2, 2, 0],\r\n [2, 0, 2],\r\n [2, 1, 1],\r\n [0, 1, 1],\r\n [0, 0, 0],\r\n [0, 2, 2]\r\n ];\r\n }\r\n\r\n static getTextTypeResult(oldType: number, newType: number): number {\r\n let rv = SuiInlineText.textTypes.normal;\r\n let i = 0;\r\n for (i = 0; i < SuiInlineText.textTypeTransitions.length; ++i) {\r\n const tt = SuiInlineText.textTypeTransitions[i];\r\n if (tt[0] === oldType && tt[1] === newType) {\r\n rv = tt[2];\r\n break;\r\n }\r\n }\r\n return rv;\r\n }\r\n\r\n static getTextTypeTransition(oldType: number, result: number): number {\r\n let rv = SuiInlineText.textTypes.normal;\r\n let i = 0;\r\n for (i = 0; i < SuiInlineText.textTypeTransitions.length; ++i) {\r\n const tt = SuiInlineText.textTypeTransitions[i];\r\n if (tt[0] === oldType && tt[2] === result) {\r\n rv = tt[1];\r\n break;\r\n }\r\n }\r\n return rv;\r\n }\r\n get spacing(): number {\r\n return VF.ChordSymbol.spacingBetweenBlocks;\r\n }\r\n\r\n static get defaults(): SuiInlineTextParams {\r\n return JSON.parse(JSON.stringify({\r\n blocks: [],\r\n fontFamily: 'Merriweather',\r\n fontSize: 14,\r\n startX: 100,\r\n startY: 100,\r\n fontWeight: 500,\r\n fontStyle: 'normal',\r\n scale: 1,\r\n activeBlock: -1,\r\n artifacts: [],\r\n purpose: 'render',\r\n classes: '',\r\n updatedMetrics: false\r\n }));\r\n }\r\n fontFamily: string;\r\n fontWeight: string;\r\n fontStyle: string;\r\n fontSize: number;\r\n width: number = -1;\r\n height: number = -1;\r\n purpose: string;\r\n\r\n attrs: SmoAttrs;\r\n textFont: TextFormatter;\r\n startX: number;\r\n startY: number;\r\n blocks: SuiInlineBlock[] = [];\r\n updatedMetrics: boolean = false;\r\n context: SvgPage;\r\n pageMap: SvgPageMap;\r\n scroller: SuiScroller;\r\n artifacts: SuiInlineArtifact[] = [];\r\n logicalBox: SvgBox = SvgBox.default;\r\n element: SVGSVGElement | null = null;\r\n\r\n updateFontInfo(): TextFormatter {\r\n const tf = TextFormatter.create({\r\n family: this.fontFamily,\r\n weight: this.fontWeight,\r\n size: this.fontSize,\r\n style: this.fontStyle\r\n });\r\n return tf;\r\n }\r\n // ### constructor just creates an empty svg\r\n constructor(params: SuiInlineTextParams) {\r\n this.fontFamily = params.fontFamily;\r\n this.fontWeight = params.fontWeight;\r\n this.fontStyle = params.fontStyle;\r\n this.fontSize = params.fontSize;\r\n this.textFont = this.updateFontInfo();\r\n this.scroller = params.scroller;\r\n this.startX = params.startX;\r\n this.startY = params.startY;\r\n this.purpose = params.purpose;\r\n this.attrs = {\r\n id: getId().toString(),\r\n type: 'SuiInlineText'\r\n };\r\n this.context = params.context;\r\n this.pageMap = params.pageMap;\r\n }\r\n\r\n static fromScoreText(scoreText: SmoScoreText, context: SvgPage, pageMap: SvgPageMap, scroller: SuiScroller): SuiInlineText {\r\n \r\n const params: SuiInlineTextParams = {\r\n fontFamily: SmoScoreText.familyString(scoreText.fontInfo.family),\r\n fontWeight: SmoScoreText.weightString(scoreText.fontInfo.weight),\r\n fontStyle: scoreText.fontInfo.style ?? 'normal',\r\n startX: scoreText.x, startY: scoreText.y,\r\n scroller,\r\n purpose: SuiInlineText.textPurposes.render,\r\n fontSize: SmoScoreText.fontPointSize(scoreText.fontInfo.size), context,\r\n pageMap\r\n };\r\n const rv = new SuiInlineText(params);\r\n rv.attrs.id = scoreText.attrs.id;\r\n const blockParams = SuiInlineText.blockDefaults;\r\n blockParams.text = scoreText.text;\r\n rv.addTextBlockAt(0, blockParams);\r\n return rv;\r\n }\r\n\r\n static get blockDefaults(): SuiInlineBlock {\r\n return JSON.parse(JSON.stringify({\r\n symbolType: SuiInlineText.symbolTypes.TEXT,\r\n textType: SuiInlineText.textTypes.normal,\r\n highlighted: false,\r\n x: 0,\r\n y: 0,\r\n width: 0,\r\n height: 0,\r\n scale: 1.0,\r\n glyph: {},\r\n text: '',\r\n glyphCode: ''\r\n }));\r\n }\r\n\r\n // ### pointsToPixels\r\n // The font size is specified in points, convert to 'pixels' in the svg space\r\n get pointsToPixels(): number {\r\n return this.textFont.fontSizeInPixels;\r\n }\r\n\r\n offsetStartX(offset: number) {\r\n this.startX += offset;\r\n this.blocks.forEach((block) => {\r\n block.x += offset;\r\n });\r\n }\r\n\r\n offsetStartY(offset: number) {\r\n this.startY += offset;\r\n this.blocks.forEach((block) => {\r\n block.y += offset;\r\n });\r\n }\r\n maxFontHeight(scale: number): number {\r\n return this.textFont.maxHeight * scale;\r\n }\r\n\r\n _glyphOffset(block: SuiInlineBlock): number {\r\n return blockMetricsYShift(block.glyph.getMetrics()) * this.pointsToPixels * block.scale;\r\n }\r\n\r\n /**\r\n * Based on the font metrics, compute the width of the strings and glyph that make up\r\n * this block\r\n */\r\n _calculateBlockIndex() {\r\n var curX = this.startX;\r\n var maxH = 0;\r\n let superXAlign = 0;\r\n let superXWidth = 0;\r\n let prevBlock: SuiInlineBlock | null = null;\r\n let i = 0;\r\n this.textFont.setFontSize(this.fontSize);\r\n this.blocks.forEach((block) => {\r\n // super/subscript\r\n const sp = this.isSuperscript(block);\r\n const sub = this.isSubcript(block);\r\n\r\n block.width = 0;\r\n block.height = 0;\r\n\r\n // coeff for sub/super script\r\n const subAdj = (sp || sub) ? VF.ChordSymbol.superSubRatio : 1.0;\r\n // offset for super/sub\r\n let subOffset = 0;\r\n if (sp) {\r\n subOffset = chordSuperscriptOffset() * this.pointsToPixels;\r\n } else if (sub) {\r\n subOffset = chordSubscriptOffset() * this.pointsToPixels;\r\n } else {\r\n subOffset = 0;\r\n }\r\n block.x = curX;\r\n if (block.symbolType === SuiInlineText.symbolTypes.TEXT) {\r\n for (i = 0; i < block.text.length; ++i) {\r\n const ch = block.text[i];\r\n const glyph = this.textFont.getGlyphMetrics(ch);\r\n block.width += ((glyph.advanceWidth ?? 0) / this.textFont.getResolution()) * this.pointsToPixels * block.scale * subAdj;\r\n const blockHeight = (glyph.ha / this.textFont.getResolution()) * this.pointsToPixels * block.scale;\r\n block.height = block.height < blockHeight ? blockHeight : block.height;\r\n block.y = this.startY + (subOffset * block.scale);\r\n }\r\n } else if (block.symbolType === SuiInlineText.symbolTypes.GLYPH) {\r\n // TODO: vexflow broke leftSideBearing and advanceWidth\r\n // vex5\r\n /*\r\n block.width = (block.glyph.getMetrics().width) * this.pointsToPixels * block.scale;\r\n block.height = (block.glyph.getMetrics().ha) * this.pointsToPixels * block.scale;\r\n block.x += block.glyph.getMetrics().xMin * this.pointsToPixels * block.scale;\r\n */\r\n block.width = (block.metrics.advanceWidth / VF.ChordSymbol.engravingFontResolution) * this.pointsToPixels * block.scale;\r\n block.height = (block.glyph.metrics.ha / VF.ChordSymbol.engravingFontResolution) * this.pointsToPixels * block.scale; \r\n block.x += block.metrics.leftSideBearing / VF.ChordSymbol.engravingFontResolution * this.pointsToPixels * block.scale;\r\n block.y = this.startY + this._glyphOffset(block) + subOffset;\r\n }\r\n // Line subscript up with super if the follow each other\r\n if (sp) {\r\n if (superXAlign === 0) {\r\n superXAlign = block.x;\r\n }\r\n } else if (sub) {\r\n if (superXAlign > 0 && prevBlock !== null) {\r\n block.x = superXAlign;\r\n superXWidth = prevBlock.x + prevBlock.width;\r\n curX = superXAlign;\r\n superXAlign = 0;\r\n } else {\r\n if (superXWidth > 0 && superXWidth < block.width + block.x) {\r\n superXWidth = block.width + block.x;\r\n }\r\n }\r\n } else if (superXWidth > 0) {\r\n block.x = superXWidth + VF.ChordSymbol.spacingBetweenBlocks;\r\n superXWidth = 0;\r\n } else {\r\n superXAlign = 0;\r\n }\r\n curX += block.width;\r\n maxH = block.height > maxH ? maxH : block.height;\r\n prevBlock = block;\r\n });\r\n this.width = curX - this.startX;\r\n this.height = maxH;\r\n this.updatedMetrics = true;\r\n }\r\n\r\n // ### getLogicalBox\r\n // return the calculated svg metrics. In SMO parlance the\r\n // logical box is in SVG space, 'renderedBox' is in client space.\r\n getLogicalBox(): SvgBox {\r\n let rv: SvgBox = SvgBox.default;\r\n if (!this.updatedMetrics) {\r\n this._calculateBlockIndex();\r\n }\r\n const adjBox = (box: SvgBox) => {\r\n const nbox = SvgHelpers.smoBox(box);\r\n nbox.y = nbox.y - nbox.height;\r\n return nbox;\r\n };\r\n this.blocks.forEach((block) => {\r\n if (!rv.x) {\r\n rv = SvgHelpers.smoBox(adjBox(block));\r\n } else {\r\n rv = SvgHelpers.unionRect(rv, adjBox(block));\r\n }\r\n });\r\n return rv;\r\n }\r\n // ### renderCursorAt\r\n // When we are using textLayout to render editor, create a cursor that adjusts it's size\r\n renderCursorAt(position: number, textType: number) {\r\n let adjH = 0;\r\n let adjY = 0;\r\n if (!this.updatedMetrics) {\r\n this._calculateBlockIndex();\r\n }\r\n const group = this.context.getContext().openGroup();\r\n group.id = 'inlineCursor';\r\n const h = this.fontSize;\r\n if (this.blocks.length <= position || position < 0) {\r\n const x = this.startX - this.context.box.x;\r\n const y = this.startY - this.context.box.y;\r\n SvgHelpers.renderCursor(group, x, y - h, h);\r\n this.context.getContext().closeGroup();\r\n return;\r\n }\r\n const block = this.blocks[position];\r\n adjH = block.symbolType === SuiInlineText.symbolTypes.GLYPH ? h / 2 : h;\r\n // For glyph, add y adj back to the cursor since it's not a glyph\r\n adjY = block.symbolType === SuiInlineText.symbolTypes.GLYPH ? block.y - this._glyphOffset(block) :\r\n block.y;\r\n if (typeof (textType) === 'number' && textType !== SuiInlineText.textTypes.normal) {\r\n const ratio = textType !== SuiInlineText.textTypes.normal ? VF.ChordSymbol.superSubRatio : 1.0;\r\n adjH = adjH * ratio;\r\n if (textType !== block.textType) {\r\n if (textType === SuiInlineText.textTypes.superScript) {\r\n adjY -= h / 2;\r\n } else {\r\n adjY += h / 2;\r\n }\r\n }\r\n }\r\n const x = block.x + block.width - this.context.box.x;\r\n const y = adjY - (adjH * block.scale) - this.context.box.y;\r\n SvgHelpers.renderCursor(group, x, y, adjH * block.scale);\r\n this.context.getContext().closeGroup();\r\n }\r\n removeCursor() {\r\n $('svg #inlineCursor').remove();\r\n }\r\n unrender() {\r\n this.element?.remove();\r\n this.element = null;\r\n }\r\n getIntersectingBlocks(box: SvgBox): SuiInlineArtifact[] {\r\n if (!this.artifacts) {\r\n return [];\r\n }\r\n return SvgHelpers.findIntersectingArtifact(box, this.artifacts) as SuiInlineArtifact[];\r\n }\r\n _addBlockAt(position: number, block: SuiInlineBlock) {\r\n if (position >= this.blocks.length) {\r\n this.blocks.push(block);\r\n } else {\r\n this.blocks.splice(position, 0, block);\r\n }\r\n }\r\n removeBlockAt(position: number) {\r\n this.blocks.splice(position, 1);\r\n this.updatedMetrics = false;\r\n }\r\n\r\n // ### addTextBlockAt\r\n // Add a text block to the line of text.\r\n // params must contain at least:\r\n // {text:'xxx'}\r\n addTextBlockAt(position: number, params: SuiInlineBlock) {\r\n const block: SuiInlineBlock = JSON.parse(JSON.stringify(SuiInlineText.blockDefaults));\r\n smoSerialize.vexMerge(block, params);\r\n block.text = params.text;\r\n block.scale = params.scale ? params.scale : 1;\r\n this._addBlockAt(position, block);\r\n this.updatedMetrics = false;\r\n }\r\n _getGlyphBlock(params: SuiInlineBlock): SuiInlineBlock {\r\n // vex 5\r\n /* const block: SuiInlineBlock = JSON.parse(JSON.stringify(SuiInlineText.blockDefaults));\r\n smoSerialize.vexMerge(block, params);\r\n params.text = params.glyphCode;\r\n block.text = params.text;\r\n block.scale = params.scale ? params.scale : 1; */\r\n const block = JSON.parse(JSON.stringify(SuiInlineText.blockDefaults));\r\n block.symbolType = SuiInlineText.symbolTypes.GLYPH;\r\n\r\n block.glyphCode = params.glyphCode;\r\n const vexCode = getVexGlyphFromChordCode(block.glyphCode);\r\n block.glyph = new VF.Glyph(vexCode, this.fontSize);\r\n // Vex 4 feature, vex 5 elimitated metrics here\r\n block.metrics = getChordSymbolMetricsForGlyph(vexCode);\r\n block.scale = (params.textType && params.textType !== SuiInlineText.textTypes.normal) ?\r\n 2 * VF.ChordSymbol.superSubRatio : 2;\r\n\r\n block.textType = params.textType ? params.textType : SuiInlineText.textTypes.normal;\r\n\r\n block.glyph.scale = block.glyph.scale * block.scale;\r\n return block;\r\n }\r\n // ### addGlyphBlockAt\r\n // Add a glyph block to the line of text. Params must include:\r\n // {glyphCode:'csymDiminished'}\r\n addGlyphBlockAt(position: number, params: SuiInlineBlock) {\r\n const block = this._getGlyphBlock(params);\r\n this._addBlockAt(position, block);\r\n this.updatedMetrics = false;\r\n }\r\n isSuperscript(block: SuiInlineBlock): boolean {\r\n return block.textType === SuiInlineText.textTypes.superScript;\r\n }\r\n isSubcript(block: SuiInlineBlock): boolean {\r\n return block.textType === SuiInlineText.textTypes.subScript;\r\n }\r\n getHighlight(block: SuiInlineBlock): boolean {\r\n return block.highlighted;\r\n }\r\n setHighlight(block: SuiInlineBlock, value: boolean) {\r\n block.highlighted = value;\r\n }\r\n\r\n rescale(scale: number) {\r\n scale = (scale * this.fontSize < 6) ? 6 / this.fontSize : scale;\r\n scale = (scale * this.fontSize > 72) ? 72 / this.fontSize : scale;\r\n this.blocks.forEach((block) => {\r\n block.scale = scale;\r\n });\r\n this.updatedMetrics = false;\r\n }\r\n\r\n render() {\r\n if (!this.updatedMetrics) {\r\n this._calculateBlockIndex();\r\n }\r\n\r\n this.context.getContext().setFont({\r\n family: this.fontFamily, size: this.fontSize, weight: this.fontWeight, style: this.fontStyle\r\n });\r\n const group = this.context.getContext().openGroup();\r\n this.element = group;\r\n const mmClass = 'suiInlineText';\r\n let ix = 0;\r\n group.classList.add('vf-' + this.attrs.id);\r\n group.classList.add(this.attrs.id);\r\n group.classList.add(mmClass);\r\n group.classList.add(this.purpose);\r\n group.id = this.attrs.id;\r\n this.artifacts = [];\r\n\r\n this.blocks.forEach((block) => {\r\n var bg = this.context.getContext().openGroup();\r\n bg.classList.add('textblock-' + this.attrs.id + ix);\r\n this._drawBlock(block);\r\n this.context.getContext().closeGroup();\r\n const artifact: SuiInlineArtifact = { block, box: SvgBox.default, index: 0 };\r\n artifact.box = this.context.offsetBbox(bg);\r\n artifact.index = ix;\r\n this.artifacts.push(artifact);\r\n ix += 1;\r\n });\r\n this.context.getContext().closeGroup();\r\n this.logicalBox = this.context.offsetBbox(group);\r\n }\r\n\r\n _drawBlock(block: SuiInlineBlock) {\r\n const sp = this.isSuperscript(block);\r\n const sub = this.isSubcript(block);\r\n const highlight = this.getHighlight(block);\r\n const y = block.y - this.context.box.y; // relative y into page\r\n\r\n if (highlight) {\r\n this.context.getContext().save();\r\n this.context.getContext().setFillStyle('#999');\r\n }\r\n\r\n // This is how svgcontext expects to get 'style'\r\n const weight = this.fontWeight;\r\n const style = this.fontStyle;\r\n const family = this.fontFamily;\r\n if (sp || sub) {\r\n this.context.getContext().save();\r\n this.context.getContext().setFont({\r\n family, size: this.fontSize * VF.ChordSymbol.superSubRatio * block.scale, weight, style\r\n });\r\n } else {\r\n this.context.getContext().setFont({ family, size: this.fontSize * block.scale, weight, style });\r\n }\r\n if (block.symbolType === SuiInlineText.symbolTypes.TEXT) {\r\n this.context.getContext().fillText(block.text, block.x, y);\r\n } else if (block.symbolType === SuiInlineText.symbolTypes.GLYPH) {\r\n block.glyph.render(this.context.getContext(), block.x, y);\r\n }\r\n if (sp || sub) {\r\n this.context.getContext().restore();\r\n }\r\n if (highlight) {\r\n this.context.getContext().restore();\r\n }\r\n }\r\n\r\n getText(): string {\r\n let rv = '';\r\n this.blocks.forEach((block) => {\r\n rv += block.text;\r\n });\r\n return rv;\r\n }\r\n}\r\n\r\nexport interface SuiTextBlockBlock {\r\n text: SuiInlineText;\r\n position: number;\r\n activeText: boolean;\r\n}\r\nexport interface SuiTextBlockParams {\r\n blocks: SuiTextBlockBlock[];\r\n scroller: SuiScroller;\r\n spacing: number;\r\n context: SvgPage;\r\n skipRender: boolean;\r\n justification: number;\r\n}\r\nexport interface SuiTextBlockJusityCalc {\r\n blocks: SuiInlineText[], minx: number, maxx: number, width: number\r\n}\r\n// ## SuiTextBlock\r\n// A text block is a set of inline blocks that can be aligned/arranged in different ways.\r\nexport class SuiTextBlock {\r\n static get relativePosition() {\r\n return {\r\n ABOVE: SmoTextGroup.relativePositions.ABOVE,\r\n BELOW: SmoTextGroup.relativePositions.BELOW,\r\n LEFT: SmoTextGroup.relativePositions.LEFT,\r\n RIGHT: SmoTextGroup.relativePositions.RIGHT\r\n };\r\n }\r\n inlineBlocks: SuiTextBlockBlock[] = [];\r\n scroller: SuiScroller;\r\n spacing: number = 0;\r\n context: SvgPage;\r\n skipRender: boolean;\r\n currentBlockIndex: number = 0;\r\n justification: number;\r\n outlineRect: OutlineInfo | null = null;\r\n currentBlock: SuiTextBlockBlock | null = null;\r\n logicalBox: SvgBox = SvgBox.default;\r\n constructor(params: SuiTextBlockParams) {\r\n this.inlineBlocks = [];\r\n this.scroller = params.scroller;\r\n this.spacing = params.spacing;\r\n this.context = params.context;\r\n this.skipRender = false; // used when editing the text\r\n if (params.blocks.length < 1) {\r\n const inlineParams = SuiInlineText.defaults;\r\n inlineParams.scroller = this.scroller;\r\n inlineParams.context = this.context;\r\n const inst = new SuiInlineText(inlineParams);\r\n params.blocks = [{ text: inst, position: SmoTextGroup.relativePositions.RIGHT, activeText: true }];\r\n }\r\n params.blocks.forEach((block) => {\r\n if (!this.currentBlock) {\r\n this.currentBlock = block;\r\n this.currentBlockIndex = 0;\r\n }\r\n this.inlineBlocks.push(block);\r\n });\r\n this.justification = params.justification ? params.justification :\r\n SmoTextGroup.justifications.LEFT;\r\n }\r\n render() {\r\n this.unrender(); \r\n this.inlineBlocks.forEach((block) => {\r\n block.text.render();\r\n if (block.activeText) {\r\n this._outlineBox(this.context, block.text.logicalBox);\r\n }\r\n if (!this.logicalBox || this.logicalBox.width < 1) {\r\n this.logicalBox = SvgHelpers.smoBox(block.text.logicalBox);\r\n } else {\r\n this.logicalBox = SvgHelpers.unionRect(this.logicalBox, block.text.logicalBox);\r\n }\r\n });\r\n }\r\n _outlineBox(context: any, box: SvgBox) {\r\n const outlineStroke = SuiTextEditor.strokes['text-highlight'];\r\n if (!this.outlineRect) {\r\n this.outlineRect = {\r\n context, box, classes: 'text-drag',\r\n stroke: outlineStroke, scroll: this.scroller.scrollState, timeOff: 1000\r\n };\r\n }\r\n this.outlineRect.box = box;\r\n this.outlineRect.context = context;\r\n this.outlineRect.scroll = this.scroller.scrollState;\r\n SvgHelpers.outlineRect(this.outlineRect);\r\n }\r\n\r\n offsetStartX(offset: number) {\r\n this.inlineBlocks.forEach((block) => {\r\n block.text.offsetStartX(offset);\r\n });\r\n }\r\n\r\n offsetStartY(offset: number) {\r\n this.inlineBlocks.forEach((block) => {\r\n block.text.offsetStartY(offset);\r\n });\r\n }\r\n\r\n rescale(scale: number) {\r\n this.inlineBlocks.forEach((block) => {\r\n block.text.rescale(scale);\r\n });\r\n }\r\n\r\n get x(): number {\r\n return this.getLogicalBox().x;\r\n }\r\n get y(): number {\r\n return this.getLogicalBox().y;\r\n }\r\n\r\n maxFontHeight(scale: number): number {\r\n let rv = 0;\r\n this.inlineBlocks.forEach((block) => {\r\n const blockHeight = block.text.maxFontHeight(scale);\r\n rv = blockHeight > rv ? blockHeight : rv;\r\n });\r\n return rv;\r\n }\r\n static blockFromScoreText(scoreText: SmoScoreText, context: SvgPage, pageMap: SvgPageMap, position: number, scroller: SuiScroller): SuiTextBlockBlock {\r\n var inlineText = SuiInlineText.fromScoreText(scoreText, context, pageMap, scroller);\r\n return { text: inlineText, position, activeText: true };\r\n }\r\n\r\n getLogicalBox(): SvgBox {\r\n return this._calculateBoundingClientRect();\r\n }\r\n _calculateBoundingClientRect(): SvgBox {\r\n let rv: SvgBox = SvgBox.default;\r\n this.inlineBlocks.forEach((block) => {\r\n if (!rv.x) {\r\n rv = block.text.getLogicalBox();\r\n } else {\r\n rv = SvgHelpers.unionRect(rv, block.text.getLogicalBox());\r\n }\r\n });\r\n rv.y = rv.y - rv.height;\r\n return rv;\r\n }\r\n static fromTextGroup(tg: SmoTextGroup, context: SvgPage, pageMap: SvgPageMap, scroller: SuiScroller): SuiTextBlock {\r\n const blocks: SuiTextBlockBlock[] = [];\r\n\r\n // Create an inline block for each ScoreText\r\n tg.textBlocks.forEach((stBlock) => {\r\n const st = stBlock.text;\r\n const newText = SuiTextBlock.blockFromScoreText(st, context, pageMap, stBlock.position, scroller);\r\n newText.activeText = stBlock.activeText;\r\n blocks.push(newText);\r\n });\r\n const rv = new SuiTextBlock({\r\n blocks, justification: tg.justification, spacing: tg.spacing, context, scroller,\r\n skipRender: false\r\n });\r\n rv._justify();\r\n return rv;\r\n }\r\n unrender() {\r\n this.inlineBlocks.forEach((block) => {\r\n if (block.text.element) {\r\n block.text.element.remove();\r\n block.text.element = null;\r\n }\r\n });\r\n }\r\n // ### _justify\r\n // justify the blocks according to the group justify policy and the\r\n // relative position of the blocks\r\n _justify() {\r\n let hIx = 0;\r\n let left = 0;\r\n let minx = 0;\r\n let maxx = 0;\r\n let lvl = 0;\r\n let maxwidth = 0;\r\n let runningWidth = 0;\r\n let runningHeight = 0;\r\n if (!this.inlineBlocks.length) {\r\n return;\r\n }\r\n minx = this.inlineBlocks[0].text.startX;\r\n // We justify relative to first block x/y.\r\n const initialX = this.inlineBlocks[0].text.startX;\r\n const initialY = this.inlineBlocks[0].text.startY;\r\n const vert: Record = {};\r\n this.inlineBlocks.forEach((inlineBlock) => {\r\n const block = inlineBlock.text;\r\n const blockBox = block.getLogicalBox();\r\n // If this is a horizontal positioning, reset to first blokc position\r\n //\r\n if (hIx > 0) {\r\n block.startX = initialX;\r\n block.startY = initialY;\r\n }\r\n minx = block.startX < minx ? block.startX : minx;\r\n maxx = (block.startX + blockBox.width) > maxx ? block.startX + blockBox.width : maxx;\r\n\r\n lvl = inlineBlock.position === SmoTextGroup.relativePositions.ABOVE ? lvl + 1 : lvl;\r\n lvl = inlineBlock.position === SmoTextGroup.relativePositions.BELOW ? lvl - 1 : lvl;\r\n if (inlineBlock.position === SmoTextGroup.relativePositions.RIGHT) {\r\n block.startX += runningWidth;\r\n if (hIx > 0) {\r\n block.startX += this.spacing;\r\n }\r\n }\r\n if (inlineBlock.position === SmoTextGroup.relativePositions.LEFT) {\r\n if (hIx > 0) {\r\n block.startX = minx - blockBox.width;\r\n minx = block.startX;\r\n block.startX -= this.spacing;\r\n }\r\n }\r\n if (inlineBlock.position === SmoTextGroup.relativePositions.BELOW) {\r\n block.startY += runningHeight;\r\n if (hIx > 0) {\r\n block.startY += this.spacing;\r\n }\r\n }\r\n if (inlineBlock.position === SmoTextGroup.relativePositions.ABOVE) {\r\n block.startY -= runningHeight;\r\n if(hIx > 0) {\r\n block.startY -= this.spacing;\r\n }\r\n }\r\n if (!vert[lvl]) {\r\n vert[lvl] = {\r\n blocks: [block], minx: block.startX, maxx: block.startX + blockBox.width,\r\n width: blockBox.width\r\n };\r\n maxwidth = vert[lvl].width;\r\n vert[lvl].blocks = [block];\r\n vert[lvl].minx = block.startX;\r\n vert[lvl].maxx = block.startX + blockBox.width;\r\n maxwidth = vert[lvl].width = blockBox.width;\r\n } else {\r\n vert[lvl].blocks.push(block);\r\n vert[lvl].minx = vert[lvl].minx < block.startX ? vert[lvl].minx : block.startX;\r\n vert[lvl].maxx = vert[lvl].maxx > (block.startX + blockBox.width) ?\r\n vert[lvl].maxx : (block.startX + blockBox.width);\r\n vert[lvl].width += blockBox.width;\r\n maxwidth = maxwidth > vert[lvl].width ? maxwidth : vert[lvl].width;\r\n }\r\n runningWidth += blockBox.width;\r\n runningHeight += blockBox.height;\r\n hIx += 1;\r\n block.updatedMetrics = false;\r\n });\r\n\r\n const levels = Object.keys(vert);\r\n\r\n // Horizontal justify the vertical blocks\r\n levels.forEach((level) => {\r\n const vobj = vert[level];\r\n if (this.justification === SmoTextGroup.justifications.LEFT) {\r\n left = minx - vobj.minx;\r\n } else if (this.justification === SmoTextGroup.justifications.RIGHT) {\r\n left = maxx - vobj.maxx;\r\n } else {\r\n left = (maxwidth / 2) - (vobj.width / 2);\r\n left += minx - vobj.minx;\r\n }\r\n vobj.blocks.forEach((block) => {\r\n block.offsetStartX(left);\r\n });\r\n });\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SuiMapper, SuiRendererBase } from './mapper';\r\nimport { SvgHelpers, StrokeInfo, OutlineInfo } from './svgHelpers';\r\nimport { SmoSelection, SmoSelector, ModifierTab } from '../../smo/xform/selections';\r\nimport { smoSerialize } from '../../common/serializationHelpers';\r\nimport { SuiOscillator } from '../audio/oscillator';\r\nimport { SmoScore } from '../../smo/data/score';\r\nimport { SvgBox, KeyEvent } from '../../smo/data/common';\r\nimport { SuiScroller } from './scroller';\r\nimport { PasteBuffer } from '../../smo/xform/copypaste';\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { SmoMeasure } from '../../smo/data/measure';\r\nimport { layoutDebug } from './layoutDebug';\r\ndeclare var $: any;\r\n\r\n/**\r\n * SuiTracker\r\n A tracker maps the UI elements to the logical elements ,and allows the user to\r\n move through the score and make selections, for navigation and editing.\r\n */\r\nexport class SuiTracker extends SuiMapper {\r\n idleTimer: number = Date.now();\r\n musicCursorGlyph: SVGSVGElement | null = null;\r\n static get strokes(): Record {\r\n return {\r\n suggestion: {\r\n strokeName: 'suggestion',\r\n stroke: '#fc9',\r\n strokeWidth: 3,\r\n strokeDasharray: '4,1',\r\n fill: 'none',\r\n opacity: 1.0\r\n },\r\n selection: {\r\n strokeName: 'selection',\r\n stroke: '#99d',\r\n strokeWidth: 3,\r\n strokeDasharray: 2,\r\n fill: 'none',\r\n opacity: 1.0\r\n },\r\n staffModifier: {\r\n strokeName: 'staffModifier',\r\n stroke: '#933',\r\n strokeWidth: 3,\r\n fill: 'none',\r\n strokeDasharray: 0,\r\n opacity: 1.0\r\n }, pitchSelection: {\r\n strokeName: 'pitchSelection',\r\n stroke: '#933',\r\n strokeWidth: 3,\r\n fill: 'none',\r\n strokeDasharray: 0,\r\n opacity: 1.0\r\n }\r\n\r\n };\r\n }\r\n constructor(renderer: SuiRendererBase, scroller: SuiScroller, pasteBuffer: PasteBuffer) {\r\n super(renderer, scroller, pasteBuffer);\r\n }\r\n // ### renderElement\r\n // the element the score is rendered on\r\n get renderElement(): Element {\r\n return this.renderer.renderElement;\r\n }\r\n\r\n get score(): SmoScore | null {\r\n return this.renderer.score;\r\n }\r\n\r\n getIdleTime(): number {\r\n return this.idleTimer;\r\n }\r\n\r\n getSelectedModifier() {\r\n if (this.modifierSelections.length) {\r\n return this.modifierSelections[0];\r\n }\r\n return null;\r\n }\r\n\r\n getSelectedModifiers() {\r\n return this.modifierSelections;\r\n }\r\n\r\n static serializeEvent(evKey: KeyEvent | null): any {\r\n if (!evKey) {\r\n return [];\r\n }\r\n const rv = {};\r\n smoSerialize.serializedMerge(['type', 'shiftKey', 'ctrlKey', 'altKey', 'key', 'keyCode'], evKey, rv);\r\n return rv;\r\n }\r\n\r\n advanceModifierSelection(score: SmoScore, keyEv: KeyEvent | null) {\r\n if (!keyEv) {\r\n return;\r\n }\r\n this.idleTimer = Date.now();\r\n const offset = keyEv.key === 'ArrowLeft' ? -1 : 1;\r\n this.modifierIndex = this.modifierIndex + offset;\r\n this.modifierIndex = (this.modifierIndex === -2 && this.localModifiers.length) ?\r\n this.localModifiers.length - 1 : this.modifierIndex;\r\n if (this.modifierIndex >= this.localModifiers.length || this.modifierIndex < 0) {\r\n this.modifierIndex = -1;\r\n this.modifierSelections = [];\r\n return;\r\n }\r\n const local: ModifierTab = this.localModifiers[this.modifierIndex];\r\n const box: SvgBox = SvgHelpers.smoBox(local.box) as SvgBox;\r\n this.modifierSelections = [{ index: 0, box, modifier: local.modifier, selection: local.selection }];\r\n this._highlightModifier();\r\n }\r\n\r\n static stringifyBox(box: SvgBox): string {\r\n return '{x:' + box.x + ',y:' + box.y + ',width:' + box.width + ',height:' + box.height + '}';\r\n }\r\n\r\n // ### _getOffsetSelection\r\n // Get the selector that is the offset of the first existing selection\r\n _getOffsetSelection(offset: number): SmoSelector {\r\n if (!this.score) {\r\n return SmoSelector.default;\r\n }\r\n let testSelection = this.getExtremeSelection(Math.sign(offset));\r\n const scopyTick = JSON.parse(JSON.stringify(testSelection.selector));\r\n const scopyMeasure = JSON.parse(JSON.stringify(testSelection.selector));\r\n scopyTick.tick += offset;\r\n scopyMeasure.measure += offset;\r\n const targetMeasure = SmoSelection.measureSelection(this.score, testSelection.selector.staff,\r\n scopyMeasure.measure);\r\n if (targetMeasure && targetMeasure.measure && targetMeasure.measure.voices.length <= scopyMeasure.voice) {\r\n scopyMeasure.voice = 0;\r\n }\r\n if (targetMeasure && targetMeasure.measure) {\r\n scopyMeasure.tick = (offset < 0) ? targetMeasure.measure.voices[scopyMeasure.voice].notes.length - 1 : 0;\r\n }\r\n\r\n if (testSelection.measure.voices.length > scopyTick.voice &&\r\n testSelection.measure.voices[scopyTick.voice].notes.length > scopyTick.tick && scopyTick.tick >= 0) {\r\n if (testSelection.selector.voice !== testSelection.measure.getActiveVoice()) {\r\n scopyTick.voice = testSelection.measure.getActiveVoice();\r\n testSelection = this._getClosestTick(scopyTick);\r\n return testSelection.selector;\r\n }\r\n return scopyTick;\r\n } else if (targetMeasure &&\r\n scopyMeasure.measure < testSelection.staff.measures.length && scopyMeasure.measure >= 0) {\r\n return scopyMeasure;\r\n }\r\n return testSelection.selector;\r\n }\r\n\r\n getSelectedGraceNotes(): ModifierTab[] {\r\n if (!this.modifierSelections.length) {\r\n return [];\r\n }\r\n const ff = this.modifierSelections.filter((mm) =>\r\n mm.modifier?.attrs?.type === 'SmoGraceNote'\r\n );\r\n return ff;\r\n }\r\n\r\n isGraceNoteSelected(): boolean {\r\n if (this.modifierSelections.length) {\r\n const ff = this.modifierSelections.findIndex((mm) => mm.modifier.attrs.type === 'SmoGraceNote');\r\n return ff >= 0;\r\n }\r\n return false;\r\n }\r\n\r\n _growGraceNoteSelections(offset: number) {\r\n this.idleTimer = Date.now();\r\n const far = this.modifierSelections.filter((mm) => mm.modifier.attrs.type === 'SmoGraceNote');\r\n if (!far.length) {\r\n return;\r\n }\r\n const ix = (offset < 0) ? 0 : far.length - 1;\r\n const sel: ModifierTab = far[ix] as ModifierTab;\r\n const left = this.localModifiers.filter((mt) =>\r\n mt.modifier?.attrs?.type === 'SmoGraceNote' && sel.selection && mt.selection &&\r\n SmoSelector.sameNote(mt.selection.selector, sel.selection.selector)\r\n );\r\n if (ix + offset < 0 || ix + offset >= left.length) {\r\n return;\r\n }\r\n const leftSel = left[ix + offset];\r\n if (!leftSel) {\r\n console.warn('bad selector in _growGraceNoteSelections');\r\n }\r\n leftSel.box = leftSel.box ?? SvgBox.default;\r\n this.modifierSelections.push(leftSel);\r\n this._highlightModifier();\r\n }\r\n get autoPlay(): boolean {\r\n return this.renderer.score ? this.renderer.score.preferences.autoPlay : false;\r\n }\r\n\r\n growSelectionRight() {\r\n this._growSelectionRight(false);\r\n }\r\n _growSelectionRight(skipPlay: boolean): number {\r\n this.idleTimer = Date.now();\r\n if (this.isGraceNoteSelected()) {\r\n this._growGraceNoteSelections(1);\r\n return 0;\r\n }\r\n const nselect = this._getOffsetSelection(1);\r\n // already selected\r\n const artifact = this._getClosestTick(nselect);\r\n if (!artifact) {\r\n return 0;\r\n }\r\n if (this.selections.find((sel) => SmoSelector.sameNote(sel.selector, artifact.selector))) {\r\n return 0;\r\n }\r\n if (!this.mapping && this.autoPlay && skipPlay === false && this.score) {\r\n SuiOscillator.playSelectionNow(artifact, this.score, 1);\r\n }\r\n this.selections.push(artifact);\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n return (artifact.note as SmoNote).tickCount;\r\n }\r\n moveHome(score: SmoScore, evKey: KeyEvent) {\r\n this.idleTimer = Date.now();\r\n const ls = this.selections[0].staff;\r\n if (evKey.ctrlKey) {\r\n const mm = ls.measures[0];\r\n const homeSel = this._getClosestTick({ staff: ls.staffId,\r\n measure: 0, voice: mm.getActiveVoice(), tick: 0, pitches: [] });\r\n if (evKey.shiftKey) {\r\n this._selectBetweenSelections(score, this.selections[0], homeSel);\r\n } else {\r\n this.selections = [homeSel];\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n if (homeSel.measure.svg.logicalBox) {\r\n this.scroller.scrollVisibleBox(homeSel.measure.svg.logicalBox);\r\n }\r\n }\r\n } else {\r\n const system = this.selections[0].measure.svg.lineIndex;\r\n const lm = ls.measures.find((mm) =>\r\n mm.svg.lineIndex === system && mm.measureNumber.systemIndex === 0);\r\n const mm = lm as SmoMeasure;\r\n const homeSel = this._getClosestTick({ staff: ls.staffId,\r\n measure: mm.measureNumber.measureIndex, voice: mm.getActiveVoice(),\r\n tick: 0, pitches: [] });\r\n if (evKey.shiftKey) {\r\n this._selectBetweenSelections(score, this.selections[0], homeSel);\r\n } else if (homeSel?.measure?.svg?.logicalBox) {\r\n this.selections = [homeSel];\r\n this.scroller.scrollVisibleBox(homeSel.measure.svg.logicalBox);\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n }\r\n }\r\n }\r\n moveEnd(score: SmoScore, evKey: KeyEvent) {\r\n this.idleTimer = Date.now();\r\n const ls = this.selections[0].staff;\r\n if (evKey.ctrlKey) {\r\n const lm = ls.measures[ls.measures.length - 1];\r\n const voiceIx = lm.getActiveVoice();\r\n const voice = lm.voices[voiceIx];\r\n const endSel = this._getClosestTick({ staff: ls.staffId,\r\n measure: ls.measures.length - 1, voice: voiceIx, tick: voice.notes.length - 1, pitches: [] });\r\n if (evKey.shiftKey) {\r\n this._selectBetweenSelections(score, this.selections[0], endSel);\r\n } else {\r\n this.selections = [endSel];\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n if (endSel.measure.svg.logicalBox) {\r\n this.scroller.scrollVisibleBox(endSel.measure.svg.logicalBox);\r\n }\r\n }\r\n } else {\r\n const system = this.selections[0].measure.svg.lineIndex;\r\n // find the largest measure index on this staff in this system\r\n const measures = ls.measures.filter((mm) =>\r\n mm.svg.lineIndex === system);\r\n const lm = measures.reduce((a, b) =>\r\n b.measureNumber.measureIndex > a.measureNumber.measureIndex ? b : a);\r\n const ticks = lm.voices[lm.getActiveVoice()].notes.length;\r\n const endSel = this._getClosestTick({ staff: ls.staffId,\r\n measure: lm.measureNumber.measureIndex, voice: lm.getActiveVoice(), tick: ticks - 1, pitches: [] });\r\n if (evKey.shiftKey) {\r\n this._selectBetweenSelections(score, this.selections[0], endSel);\r\n } else {\r\n this.selections = [endSel];\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n if (endSel.measure.svg.logicalBox) {\r\n this.scroller.scrollVisibleBox(endSel.measure.svg.logicalBox);\r\n }\r\n }\r\n }\r\n }\r\n growSelectionRightMeasure() {\r\n let toSelect = 0;\r\n const rightmost = this.getExtremeSelection(1);\r\n const ticksLeft = rightmost.measure.voices[rightmost.measure.activeVoice]\r\n .notes.length - rightmost.selector.tick;\r\n if (ticksLeft === 0) {\r\n if (rightmost.selector.measure < rightmost.staff.measures.length) {\r\n const mix = rightmost.selector.measure + 1;\r\n rightmost.staff.measures[mix].setActiveVoice(rightmost.selector.voice);\r\n toSelect = rightmost.staff.measures[mix]\r\n .voices[rightmost.staff.measures[mix].activeVoice].notes.length;\r\n }\r\n } else {\r\n toSelect = ticksLeft;\r\n }\r\n while (toSelect > 0) {\r\n this._growSelectionRight(true);\r\n toSelect -= 1;\r\n }\r\n }\r\n\r\n growSelectionLeft(): number {\r\n if (this.isGraceNoteSelected()) {\r\n this._growGraceNoteSelections(-1);\r\n return 0;\r\n }\r\n this.idleTimer = Date.now();\r\n const nselect = this._getOffsetSelection(-1);\r\n // already selected\r\n const artifact = this._getClosestTick(nselect);\r\n if (!artifact) {\r\n return 0;\r\n }\r\n if (this.selections.find((sel) => SmoSelector.sameNote(sel.selector, artifact.selector))) {\r\n return 0;\r\n }\r\n artifact.measure.setActiveVoice(nselect.voice);\r\n this.selections.push(artifact);\r\n if (this.autoPlay && this.score) {\r\n SuiOscillator.playSelectionNow(artifact, this.score, 1);\r\n }\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n return (artifact.note as SmoNote).tickCount;\r\n }\r\n\r\n // if we are being moved right programmatically, avoid playing the selected note.\r\n moveSelectionRight(score: SmoScore, evKey: KeyEvent | null, skipPlay: boolean) {\r\n if (this.selections.length === 0 || this.score === null) {\r\n return;\r\n }\r\n // const original = JSON.parse(JSON.stringify(this.getExtremeSelection(-1).selector));\r\n const nselect = this._getOffsetSelection(1);\r\n // skip any measures that are not displayed due to rest or repetition\r\n const mselect = SmoSelection.measureSelection(this.score, nselect.staff, nselect.measure); \r\n if (mselect?.measure.svg.multimeasureLength) {\r\n nselect.measure += mselect?.measure.svg.multimeasureLength;\r\n }\r\n if (mselect) {\r\n mselect.measure.setActiveVoice(nselect.voice);\r\n }\r\n this._replaceSelection(nselect, skipPlay);\r\n }\r\n\r\n moveSelectionLeft() {\r\n if (this.selections.length === 0 || this.score === null) {\r\n return;\r\n }\r\n const nselect = this._getOffsetSelection(-1);\r\n // Skip multimeasure rests in parts\r\n const mselect = SmoSelection.measureSelection(this.score, nselect.staff, nselect.measure);\r\n while (nselect.measure > 0 && mselect && (mselect.measure.svg.hideMultimeasure || mselect.measure.svg.multimeasureLength > 0)) {\r\n nselect.measure -= 1;\r\n }\r\n if (mselect) {\r\n mselect.measure.setActiveVoice(nselect.voice);\r\n } \r\n this._replaceSelection(nselect, false);\r\n }\r\n moveSelectionLeftMeasure() {\r\n this._moveSelectionMeasure(-1);\r\n }\r\n moveSelectionRightMeasure() {\r\n this._moveSelectionMeasure(1);\r\n }\r\n _moveSelectionMeasure(offset: number) {\r\n const selection = this.getExtremeSelection(Math.sign(offset));\r\n this.idleTimer = Date.now();\r\n const selector = JSON.parse(JSON.stringify(selection.selector));\r\n selector.measure += offset;\r\n selector.tick = 0;\r\n const selObj = this._getClosestTick(selector);\r\n if (selObj) {\r\n this.selections = [selObj];\r\n }\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n }\r\n\r\n _moveStaffOffset(offset: number) {\r\n if (this.selections.length === 0 || this.score === null) {\r\n return;\r\n }\r\n this.idleTimer = Date.now();\r\n const nselector = JSON.parse(JSON.stringify(this.selections[0].selector));\r\n nselector.staff = this.score.incrementActiveStaff(offset);\r\n \r\n this.selections = [this._getClosestTick(nselector)];\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n }\r\n removePitchSelection() {\r\n if (this.outlines['pitchSelection']) {\r\n if (this.outlines['pitchSelection'].element) {\r\n this.outlines['pitchSelection'].element.remove();\r\n }\r\n delete this.outlines['pitchSelection'];\r\n }\r\n }\r\n\r\n // ### _moveSelectionPitch\r\n // Suggest a specific pitch in a chord, so we can transpose just the one note vs. the whole chord.\r\n _moveSelectionPitch(index: number) {\r\n this.idleTimer = Date.now();\r\n if (!this.selections.length) {\r\n return;\r\n }\r\n const sel = this.selections[0];\r\n const note = sel.note as SmoNote;\r\n if (note.pitches.length < 2) {\r\n this.pitchIndex = -1;\r\n this.removePitchSelection();\r\n return;\r\n }\r\n this.pitchIndex = (this.pitchIndex + index) % note.pitches.length;\r\n sel.selector.pitches = [];\r\n sel.selector.pitches.push(this.pitchIndex);\r\n this._highlightPitchSelection(note, this.pitchIndex);\r\n }\r\n moveSelectionPitchUp() {\r\n this._moveSelectionPitch(1);\r\n }\r\n moveSelectionPitchDown() {\r\n if (!this.selections.length) {\r\n return;\r\n }\r\n this._moveSelectionPitch((this.selections[0].note as SmoNote).pitches.length - 1);\r\n }\r\n\r\n moveSelectionUp() {\r\n this._moveStaffOffset(-1);\r\n }\r\n moveSelectionDown() {\r\n this._moveStaffOffset(1);\r\n }\r\n\r\n containsArtifact(): boolean {\r\n return this.selections.length > 0;\r\n }\r\n\r\n _replaceSelection(nselector: SmoSelector, skipPlay: boolean) {\r\n if (this.score === null) {\r\n return;\r\n }\r\n var artifact = SmoSelection.noteSelection(this.score, nselector.staff, nselector.measure, nselector.voice, nselector.tick);\r\n if (!artifact) {\r\n artifact = SmoSelection.noteSelection(this.score, nselector.staff, nselector.measure, 0, nselector.tick);\r\n }\r\n if (!artifact) {\r\n artifact = SmoSelection.noteSelection(this.score, nselector.staff, nselector.measure, 0, 0);\r\n }\r\n if (!artifact) {\r\n // disappeared - default to start\r\n artifact = SmoSelection.noteSelection(this.score, 0, 0, 0, 0);\r\n }\r\n if (!skipPlay && this.autoPlay && artifact) {\r\n SuiOscillator.playSelectionNow(artifact, this.score, 1);\r\n }\r\n if (!artifact) {\r\n return;\r\n }\r\n artifact.measure.setActiveVoice(nselector.voice);\r\n\r\n // clear modifier selections\r\n this.clearModifierSelections();\r\n this.score.setActiveStaff(nselector.staff);\r\n const mapKey = Object.keys(this.measureNoteMap).find((k) =>\r\n artifact && SmoSelector.sameNote(this.measureNoteMap[k].selector, artifact.selector)\r\n );\r\n if (!mapKey) {\r\n return;\r\n }\r\n const mapped = this.measureNoteMap[mapKey];\r\n // If this is a new selection, remove pitch-specific and replace with note-specific\r\n if (!nselector.pitches || nselector.pitches.length === 0) {\r\n this.pitchIndex = -1;\r\n }\r\n\r\n this.selections = [mapped];\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n }\r\n\r\n getFirstMeasureOfSelection() {\r\n if (this.selections.length) {\r\n return this.selections[0].measure;\r\n }\r\n return null;\r\n }\r\n // ## measureIterator\r\n // Description: iterate over the any measures that are part of the selection\r\n getSelectedMeasures(): SmoSelection[] {\r\n const set: number[] = [];\r\n const rv: SmoSelection[] = [];\r\n if (!this.score) {\r\n return [];\r\n }\r\n this.selections.forEach((sel) => {\r\n const measure = SmoSelection.measureSelection(this.score!, sel.selector.staff, sel.selector.measure);\r\n if (measure) {\r\n const ix = measure.selector.measure;\r\n if (set.indexOf(ix) === -1) {\r\n set.push(ix);\r\n rv.push(measure);\r\n }\r\n }\r\n });\r\n return rv;\r\n }\r\n\r\n _addSelection(selection: SmoSelection) {\r\n const ar: SmoSelection[] = this.selections.filter((sel) =>\r\n SmoSelector.neq(sel.selector, selection.selector)\r\n );\r\n if (this.autoPlay && this.score) {\r\n SuiOscillator.playSelectionNow(selection, this.score, 1);\r\n }\r\n ar.push(selection);\r\n this.selections = ar;\r\n }\r\n\r\n _selectFromToInStaff(score: SmoScore, sel1: SmoSelection, sel2: SmoSelection) {\r\n const selections = SmoSelection.innerSelections(score, sel1.selector, sel2.selector);\r\n /* .filter((ff) => \r\n ff.selector.voice === sel1.measure.activeVoice\r\n ); */\r\n this.selections = [];\r\n // Get the actual selections from our map, since the client bounding boxes are already computed\r\n selections.forEach((sel) => {\r\n const key = SmoSelector.getNoteKey(sel.selector);\r\n sel.measure.setActiveVoice(sel.selector.voice);\r\n // Skip measures that are not rendered because they are part of a multi-rest\r\n if (this.measureNoteMap && this.measureNoteMap[key]) {\r\n this.selections.push(this.measureNoteMap[key]);\r\n }\r\n });\r\n\r\n if (this.selections.length === 0) {\r\n this.selections = [sel1];\r\n }\r\n this.idleTimer = Date.now();\r\n }\r\n _selectBetweenSelections(score: SmoScore, s1: SmoSelection, s2: SmoSelection) {\r\n const min = SmoSelector.gt(s1.selector, s2.selector) ? s2 : s1;\r\n const max = SmoSelector.lt(min.selector, s2.selector) ? s2 : s1;\r\n this._selectFromToInStaff(score, min, max);\r\n this._createLocalModifiersList();\r\n this.highlightQueue.selectionCount = this.selections.length;\r\n this.deferHighlight();\r\n }\r\n selectSuggestion(score: SmoScore,ev: KeyEvent) {\r\n if (!this.suggestion || !this.suggestion.measure || this.score === null) {\r\n return;\r\n }\r\n this.idleTimer = Date.now();\r\n\r\n if (this.modifierSuggestion) {\r\n this.modifierIndex = -1;\r\n this.modifierSelections = [this.modifierSuggestion];\r\n this.modifierSuggestion = null;\r\n this.createLocalModifiersFromModifierTabs(this.modifierSelections);\r\n // If we selected due to a mouse click, move the selection to the\r\n // selected modifier\r\n this._highlightModifier();\r\n return;\r\n } else if (ev.type === 'click') {\r\n this.clearModifierSelections(); // if we click on a non-modifier, clear the\r\n // modifier selections\r\n }\r\n\r\n if (ev.shiftKey) {\r\n const sel1 = this.getExtremeSelection(-1);\r\n if (sel1.selector.staff === this.suggestion.selector.staff) {\r\n this._selectBetweenSelections(score, sel1, this.suggestion);\r\n return;\r\n }\r\n }\r\n\r\n if (ev.ctrlKey) {\r\n this._addSelection(this.suggestion);\r\n this._createLocalModifiersList();\r\n this.deferHighlight();\r\n return;\r\n }\r\n if (this.autoPlay) {\r\n SuiOscillator.playSelectionNow(this.suggestion, this.score, 1);\r\n }\r\n\r\n const preselected = this.selections[0] ?\r\n SmoSelector.sameNote(this.suggestion.selector, this.selections[0].selector) && this.selections.length === 1 : false;\r\n\r\n if (this.selections.length === 0) {\r\n this.selections.push(this.suggestion);\r\n }\r\n const note = this.selections[0].note as SmoNote;\r\n if (preselected && note.pitches.length > 1) {\r\n this.pitchIndex = (this.pitchIndex + 1) % note.pitches.length;\r\n this.selections[0].selector.pitches = [this.pitchIndex];\r\n } else {\r\n const selection = SmoSelection.noteFromSelector(this.score, this.suggestion.selector);\r\n if (selection) {\r\n selection.box = JSON.parse(JSON.stringify(this.suggestion.box));\r\n selection.scrollBox = JSON.parse(JSON.stringify(this.suggestion.scrollBox));\r\n this.selections = [selection];\r\n }\r\n }\r\n if (preselected && this.modifierSelections.length) {\r\n const mods = this.modifierSelections.filter((mm) => mm.selection && SmoSelector.sameNote(mm.selection.selector, this.selections[0].selector));\r\n if (mods.length) {\r\n const modToAdd = mods[0];\r\n if (!modToAdd) {\r\n console.warn('bad modifier selection in selectSuggestion 2');\r\n }\r\n this.modifierSelections[0] = modToAdd;\r\n this.modifierIndex = mods[0].index;\r\n this._highlightModifier();\r\n return;\r\n }\r\n }\r\n this.score.setActiveStaff(this.selections[0].selector.staff);\r\n this.deferHighlight();\r\n this._createLocalModifiersList();\r\n }\r\n _setModifierAsSuggestion(artifact: ModifierTab): void {\r\n if (!artifact.box) {\r\n return;\r\n }\r\n this.modifierSuggestion = artifact;\r\n this._drawRect(artifact.box, 'suggestion');\r\n }\r\n\r\n _setArtifactAsSuggestion(artifact: SmoSelection) {\r\n let sameSel: SmoSelection | null = null;\r\n let i = 0;\r\n for (i = 0; i < this.selections.length; ++i) {\r\n const ss = this.selections[i];\r\n if (ss && SmoSelector.sameNote(ss.selector, artifact.selector)) {\r\n sameSel = ss;\r\n break;\r\n }\r\n }\r\n if (sameSel || !artifact.box) {\r\n return;\r\n }\r\n this.modifierSuggestion = null;\r\n\r\n this.suggestion = artifact;\r\n this._drawRect(artifact.box, 'suggestion');\r\n }\r\n _highlightModifier() {\r\n let box: SvgBox | null = null;\r\n if (!this.modifierSelections.length) {\r\n return;\r\n }\r\n this.modifierSelections.forEach((artifact) => {\r\n if (box === null) {\r\n box = artifact.modifier.logicalBox ?? null;\r\n } else {\r\n box = SvgHelpers.unionRect(box, SvgHelpers.smoBox(artifact.modifier.logicalBox));\r\n }\r\n });\r\n if (box === null) {\r\n return;\r\n }\r\n this._drawRect(box, 'staffModifier');\r\n }\r\n\r\n _highlightPitchSelection(note: SmoNote, index: number) {\r\n const noteDiv = $(this.renderElement).find('#' + note.renderId);\r\n const heads = noteDiv.find('.vf-notehead');\r\n if (!heads.length) {\r\n return;\r\n }\r\n const headEl = heads[index];\r\n const pageContext = this.renderer.pageMap.getRendererFromModifier(note);\r\n $(pageContext.svg).find('.vf-pitchSelection').remove();\r\n const box = pageContext.offsetBbox(headEl);\r\n this._drawRect(box, 'pitchSelection');\r\n }\r\n\r\n _highlightActiveVoice(selection: SmoSelection) {\r\n let i = 0;\r\n const selector = selection.selector;\r\n for (i = 1; i <= 4; ++i) {\r\n const cl = 'v' + i.toString() + '-active';\r\n $('body').removeClass(cl);\r\n }\r\n const c2 = 'v' + (selector.voice + 1).toString() + '-active';\r\n $('body').addClass(c2);\r\n }\r\n // The user has just switched voices, select the active voice\r\n selectActiveVoice() {\r\n const selection = this.selections[0];\r\n const selector = JSON.parse(JSON.stringify(selection.selector));\r\n selector.voice = selection.measure.activeVoice;\r\n this.selections = [this._getClosestTick(selector)];\r\n this.deferHighlight();\r\n }\r\n\r\n highlightSelection() {\r\n let i = 0;\r\n let prevSel: SmoSelection | null = null;\r\n let curBox: SvgBox = SvgBox.default;\r\n this.idleTimer = Date.now();\r\n const grace = this.getSelectedGraceNotes();\r\n // If this is not a note with grace notes, logically unselect the grace notes\r\n if (grace && grace.length && grace[0].selection && this.selections.length) {\r\n if (!SmoSelector.sameNote(grace[0].selection.selector, this.selections[0].selector)) {\r\n this.clearModifierSelections();\r\n } else {\r\n this._highlightModifier();\r\n return;\r\n }\r\n }\r\n // If there is a race condition with a change, avoid referencing null note\r\n if (!this.selections[0].note) {\r\n return;\r\n }\r\n const note = this.selections[0].note as SmoNote;\r\n if (this.pitchIndex >= 0 && this.selections.length === 1 &&\r\n this.pitchIndex < note.pitches.length) {\r\n this._highlightPitchSelection(note, this.pitchIndex);\r\n this._highlightActiveVoice(this.selections[0]);\r\n return;\r\n }\r\n this.removePitchSelection();\r\n this.pitchIndex = -1;\r\n if (this.selections.length === 1 && note.logicalBox) {\r\n this._drawRect(note.logicalBox, 'selection');\r\n this._highlightActiveVoice(this.selections[0]);\r\n return;\r\n }\r\n const sorted = this.selections.sort((a, b) => SmoSelector.gt(a.selector, b.selector) ? 1 : -1);\r\n prevSel = sorted[0];\r\n // rendered yet?\r\n if (!prevSel || !prevSel.box) {\r\n return;\r\n }\r\n curBox = SvgHelpers.smoBox(prevSel.box);\r\n const boxes: SvgBox[] = [];\r\n for (i = 1; i < sorted.length; ++i) {\r\n const sel = sorted[i];\r\n if (!sel.box || !prevSel.box) {\r\n continue;\r\n }\r\n // const ydiff = Math.abs(prevSel.box.y - sel.box.y);\r\n if (sel.selector.staff === prevSel.selector.staff && sel.measure.svg.lineIndex === prevSel.measure.svg.lineIndex) {\r\n curBox = SvgHelpers.unionRect(curBox, sel.box);\r\n } else if (curBox) {\r\n boxes.push(curBox);\r\n curBox = SvgHelpers.smoBox(sel.box);\r\n }\r\n this._highlightActiveVoice(sel);\r\n prevSel = sel;\r\n }\r\n boxes.push(curBox);\r\n if (this.modifierSelections.length) {\r\n boxes.push(this.modifierSelections[0].box);\r\n }\r\n this._drawRect(boxes, 'selection');\r\n }\r\n /**\r\n * Boxes are divided up into lines/systems already. But we need\r\n * to put the correct box on the correct page.\r\n * @param boxes \r\n */\r\n drawSelectionRects(boxes: SvgBox[]) {\r\n const keys = Object.keys(this.selectionRects);\r\n // erase any old selections\r\n keys.forEach((key) => {\r\n const oon = this.selectionRects[parseInt(key)];\r\n oon.forEach((outline) => {\r\n if (outline.element) {\r\n outline.element.remove();\r\n outline.element = undefined;\r\n \r\n }\r\n })\r\n });\r\n this.selectionRects = {};\r\n // Create an OutlineInfo for each page\r\n const pages: number[] = [];\r\n const stroke: StrokeInfo = (SuiTracker.strokes as any)['selection'];\r\n boxes.forEach((box) => {\r\n let testBox: SvgBox = SvgHelpers.smoBox(box);\r\n let context = this.renderer.pageMap.getRenderer(testBox);\r\n testBox.y -= context.box.y;\r\n if (!this.selectionRects[context.pageNumber]) {\r\n this.selectionRects[context.pageNumber] = [];\r\n pages.push(context.pageNumber);\r\n }\r\n this.selectionRects[context.pageNumber].push({\r\n context: context, box: testBox, classes: '',\r\n stroke, scroll: this.scroller.scrollState,\r\n timeOff: 0\r\n });\r\n });\r\n pages.forEach((pageNo) => {\r\n const outlineInfos = this.selectionRects[pageNo];\r\n outlineInfos.forEach((info) => {\r\n SvgHelpers.outlineRect(info);\r\n });\r\n });\r\n }\r\n _drawRect(pBox: SvgBox | SvgBox[], strokeName: string) { \r\n const stroke: StrokeInfo = (SuiTracker.strokes as any)[strokeName];\r\n const boxes = Array.isArray(pBox) ? pBox : [pBox];\r\n if (strokeName === 'selection') {\r\n this.drawSelectionRects(boxes);\r\n return;\r\n }\r\n boxes.forEach((box) => {\r\n let testBox: SvgBox = SvgHelpers.smoBox(box);\r\n let context = this.renderer.pageMap.getRenderer(testBox);\r\n const timeOff = strokeName === 'suggestion' ? 1000 : 0; \r\n if (context) {\r\n testBox.y -= context.box.y;\r\n if (!this.outlines[strokeName]) {\r\n this.outlines[strokeName] = {\r\n context: context, box: testBox, classes: '',\r\n stroke, scroll: this.scroller.scrollState,\r\n timeOff\r\n };\r\n }\r\n this.outlines[strokeName].box = testBox;\r\n this.outlines[strokeName].context = context;\r\n SvgHelpers.outlineRect(this.outlines[strokeName]);\r\n }\r\n });\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SmoBarline } from '../../smo/data/measureModifiers';\r\nimport { SmoMusic } from '../../smo/data/music';\r\nimport { VexFlow, GlyphInfo, getGlyphWidth } from '../../common/vex';\r\n\r\nexport class vexGlyph {\r\n static width(smoGlyph: GlyphInfo) {\r\n return getGlyphWidth(smoGlyph);\r\n }\r\n static accidental(a: string): GlyphInfo {\r\n return vexGlyph.accidentals[a];\r\n }\r\n static barWidth(b: SmoBarline): number {\r\n const str = SmoBarline.barlineString(b);\r\n const cc = vexGlyph.dimensions[str];\r\n return cc.width + cc.spacingRight;\r\n }\r\n static accidentalWidth(accidental: string): number {\r\n return vexGlyph.width(vexGlyph.accidentals[accidental]);\r\n }\r\n static get accidentals(): Record {\r\n return {\r\n 'b': vexGlyph.dimensions.flat,\r\n '#': vexGlyph.dimensions.sharp,\r\n 'bb': vexGlyph.dimensions.doubleFlat,\r\n '##': vexGlyph.dimensions.doubleSharp,\r\n 'n': vexGlyph.dimensions.natural\r\n };\r\n }\r\n\r\n static repeatSymbolWidth(): number {\r\n return vexGlyph.width(vexGlyph.dimensions['repeatSymbol']);\r\n }\r\n static get tempo(): GlyphInfo {\r\n return vexGlyph.dimensions.tempo;\r\n }\r\n static keySignatureLength(key: string) {\r\n return SmoMusic.getSharpsInKeySignature(key) * vexGlyph.width(vexGlyph.dimensions.sharp) +\r\n SmoMusic.getFlatsInKeySignature(key) * vexGlyph.width(vexGlyph.dimensions.flat) +\r\n vexGlyph.dimensions.keySignature.spacingRight;\r\n }\r\n static get timeSignature() {\r\n return vexGlyph.dimensions.timeSignature;\r\n }\r\n static get dot() {\r\n return vexGlyph.dimensions.dot;\r\n }\r\n\r\n static get tupletBeam() {\r\n return vexGlyph.dimensions.tupletBeam;\r\n }\r\n static get stem() {\r\n return vexGlyph.dimensions.stem;\r\n }\r\n static get flag() {\r\n return vexGlyph.dimensions.flag;\r\n }\r\n static clef(c: string): GlyphInfo {\r\n const key = c.toLowerCase() + 'Clef';\r\n if (!vexGlyph.dimensions[key]) {\r\n return vexGlyph.dimensions.tenorClef;\r\n }\r\n if (vexGlyph.dimensions[key].vexGlyph) {\r\n const width = vexGlyph.width(vexGlyph.dimensions[key]);\r\n return {\r\n width,\r\n height: 68.32,\r\n yTop: 3,\r\n yBottom: 3,\r\n spacingRight: 10,\r\n vexGlyph: 'gClef'\r\n };\r\n }\r\n return vexGlyph.dimensions[key];\r\n }\r\n static get dimensions(): Record {\r\n return {\r\n tupletBeam: {\r\n width: 5,\r\n height: 6,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 5,\r\n vexGlyph: null\r\n }, repeatSymbol: {\r\n width: 25,\r\n height: 6,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 5,\r\n vexGlyph: 'repeat1Bar'\r\n },\r\n singleBar: {\r\n width: 1,\r\n height: 41,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 1,\r\n vexGlyph: null\r\n },\r\n endBar: {\r\n width: 5.22,\r\n height: 40.99,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 5,\r\n vexGlyph: null\r\n },\r\n doubleBar: {\r\n width: 3.22,\r\n height: 40.99,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: null\r\n },\r\n endRepeat: {\r\n width: 6,\r\n height: 40.99,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: null\r\n },\r\n startRepeat: {\r\n width: 6,\r\n height: 40.99,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 5,\r\n vexGlyph: null\r\n },\r\n noteHead: {\r\n width: 15.3,\r\n height: 10.48,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 5,\r\n vexGlyph: 'noteheadBlack'\r\n },\r\n dot: {\r\n width: 15,\r\n height: 5,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 5,\r\n vexGlyph: 'augmentationDot'\r\n }, // This isn't accurate, but I don't\r\n // want to add extra space just for clef.\r\n trebleClef: {\r\n width: 35,\r\n height: 68.32,\r\n yTop: 3,\r\n yBottom: 3,\r\n spacingRight: 5,\r\n vexGlyph: 'gClef'\r\n }, // This isn't accurate, but I don't\r\n // want to add extra space just for clef.\r\n tab: {\r\n width: 27.3,\r\n height: 39,\r\n yTop: 3,\r\n yBottom: 3,\r\n spacingRight: 5,\r\n vexGlyph: 'tab'\r\n },\r\n bassClef: {\r\n width: 36,\r\n height: 31.88,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 5,\r\n vexGlyph: 'fClef'\r\n },\r\n altoClef: {\r\n width: 31.5,\r\n yTop: 0,\r\n yBottom: 0,\r\n height: 85.5,\r\n spacingRight: 5,\r\n vexGlyph: 'cClef'\r\n },\r\n tenorClef: {\r\n width: 31.5,\r\n yTop: 10,\r\n yBottom: 0,\r\n height: 41,\r\n spacingRight: 5,\r\n vexGlyph: 'cClef'\r\n },\r\n timeSignature: {\r\n width: 22.36,\r\n height: 85,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 11,\r\n vexGlyph: 'timeSig4'\r\n },\r\n tempo: {\r\n width: 10,\r\n height: 37,\r\n yTop: 37,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: null\r\n },\r\n flat: {\r\n width: 15,\r\n height: 23.55,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: 'accidentalFlat'\r\n },\r\n keySignature: {\r\n width: 0,\r\n height: 85.5,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 10,\r\n vexGlyph: null\r\n },\r\n sharp: {\r\n width: 17,\r\n height: 62,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: 'accidentalSharp',\r\n },\r\n natural: {\r\n width: 15,\r\n height: 53.35,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: 'accidentalNatural',\r\n },\r\n doubleSharp: {\r\n height: 10.04,\r\n width: 21.63,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: 'accidentalDoubleSharp'\r\n },\r\n doubleFlat: {\r\n width: 13.79,\r\n height: 49.65,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: 'accidentalDoubleFlat'\r\n }, stem: {\r\n width: 1,\r\n height: 35,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: null\r\n }, flag: {\r\n width: 10,\r\n height: 35,\r\n yTop: 0,\r\n yBottom: 0,\r\n spacingRight: 0,\r\n vexGlyph: 'flag8thUp' // use for width measurements all flags\r\n }\r\n };\r\n }\r\n}\r\n","import { SmoSystemGroup } from '../../smo/data/scoreModifiers';\r\nimport { SmoBarline, SmoMeasureText, SmoRepeatSymbol, SmoVolta } from '../../smo/data/measureModifiers';\r\nimport { SmoTabStave, SmoTie } from '../../smo/data/staffModifiers';\r\nimport { SmoLyric, VexAnnotationParams, SmoTabNote, SmoFretPosition } from '../../smo/data/noteModifiers';\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { TabNotePosition, VexFlow } from '../../common/vex';\r\nconst VF = VexFlow;\r\n/**\r\n * convert from Smo library values to Vex values\r\n * @module\r\n * \r\n **/\r\nexport function VexTabNotePositions(stave: SmoTabStave, tabNote: SmoTabNote, smoNote: SmoNote): TabNotePosition[] {\r\n const rv = tabNote.positions.map((pp) => { \r\n return { str: pp.string, fret: pp.fret }\r\n });\r\n return rv;\r\n}\r\n/**\r\n *\r\n *\r\n * @export\r\n * @param {SmoSystemGroup} athis\r\n * @return {*} \r\n */\r\nexport function leftConnectorVx(athis: SmoSystemGroup) {\r\n switch (athis.leftConnector) {\r\n case SmoSystemGroup.connectorTypes.single:\r\n return VF.StaveConnector.type.SINGLE_LEFT;\r\n case SmoSystemGroup.connectorTypes.double:\r\n return VF.StaveConnector.type.DOUBLE_LEFT;\r\n case SmoSystemGroup.connectorTypes.brace:\r\n return VF.StaveConnector.type.BRACE;\r\n case SmoSystemGroup.connectorTypes.bracket:\r\n default:\r\n return VF.StaveConnector.type.BRACKET;\r\n }\r\n}\r\n/**\r\n * convert from a SmoSystemGroup connector to Vex enumeration\r\n * @param athis \r\n * @returns \r\n */\r\nexport function rightConnectorVx(athis: SmoSystemGroup) {\r\n switch (athis.rightConnector) {\r\n case SmoSystemGroup.connectorTypes.single:\r\n return VF.StaveConnector.type.SINGLE_RIGHT;\r\n case SmoSystemGroup.connectorTypes.double:\r\n default:\r\n return VF.StaveConnector.type.DOUBLE_RIGHT;\r\n }\r\n}\r\nexport const vexBarlineType = [VF.Barline.type.SINGLE, VF.Barline.type.DOUBLE, VF.Barline.type.END,\r\n VF.Barline.type.REPEAT_BEGIN, VF.Barline.type.REPEAT_END, VF.Barline.type.NONE];\r\n\r\nexport const vexBarlinePosition = [ VF.StaveModifierPosition.BEGIN, VF.StaveModifierPosition.END ];\r\n\r\nexport function toVexBarlineType(athis: SmoBarline): number {\r\n return vexBarlineType[athis.barline];\r\n}\r\nexport function toVexBarlinePosition(athis: SmoBarline): number {\r\n return vexBarlinePosition[athis.position];\r\n}\r\n\r\nexport const vexSymbol = [VF.Repetition.type.NONE, VF.Repetition.type.CODA_LEFT, VF.Repetition.type.SEGNO_LEFT, VF.Repetition.type.DC,\r\n VF.Repetition.type.DC_AL_CODA, VF.Repetition.type.DC_AL_FINE, VF.Repetition.type.DS,\r\n VF.Repetition.type.DS_AL_CODA, VF.Repetition.type.DS_AL_FINE, VF.Repetition.type.FINE];\r\n\r\nexport function toVexSymbol(athis: SmoRepeatSymbol) {\r\n return vexSymbol[athis.symbol];\r\n}\r\nexport function toVexVolta(volta: SmoVolta, measureNumber: number) {\r\n if (volta.startBar === measureNumber && volta.startBar === volta.endBar) {\r\n return VF.Volta.type.BEGIN_END;\r\n }\r\n if (volta.startBar === measureNumber) {\r\n return VF.Volta.type.BEGIN;\r\n }\r\n if (volta.endBar === measureNumber) {\r\n return VF.Volta.type.END;\r\n }\r\n if (volta.startBar < measureNumber && volta.endBar > measureNumber) {\r\n return VF.Volta.type.MID;\r\n }\r\n return VF.Volta.type.NONE;\r\n}\r\n\r\nexport const vexTextPosition = [VF.Modifier.Position.ABOVE, VF.Modifier.Position.BELOW, VF.Modifier.Position.LEFT, VF.Modifier.Position.RIGHT];\r\nexport const vexTextJustification = [VF.TextJustification.LEFT, VF.TextJustification.RIGHT, VF.TextJustification.CENTER];\r\n\r\nexport function toVexTextJustification(athis: SmoMeasureText) {\r\n return vexTextJustification[athis.justification];\r\n}\r\nexport function toVexTextPosition(athis: SmoMeasureText) {\r\n return vexTextPosition[parseInt(athis.position as any, 10)];\r\n}\r\n\r\nexport function vexOptions(athis: SmoTie) {\r\n const rv: any = {};\r\n rv.direction = athis.invert ? VF.Stem.DOWN : VF.Stem.UP;\r\n SmoTie.vexParameters.forEach((p) => {\r\n rv[p] = (athis as any)[p];\r\n });\r\n return rv;\r\n}\r\nexport function vexAnnotationPosition(chordPos: number) {\r\n if (chordPos === SmoLyric.symbolPosition.NORMAL) {\r\n return VF.ChordSymbol.symbolModifiers.NONE;\r\n } else if (chordPos === SmoLyric.symbolPosition.SUPERSCRIPT) {\r\n return VF.ChordSymbol.symbolModifiers.SUPERSCRIPT;\r\n }\r\n return VF.ChordSymbol.symbolModifiers.SUBSCRIPT;\r\n}\r\n\r\n/**\r\n * Parse the SmoLyric text and convert it to a VEX chord symbol\r\n * @param athis \r\n * @returns \r\n */\r\nexport function getVexChordBlocks(athis: SmoLyric) {\r\n let mod = VF.ChordSymbol.symbolModifiers.NONE;\r\n let isGlyph = false;\r\n const tokens = SmoLyric._tokenizeChordString(athis.text);\r\n const blocks: VexAnnotationParams[] = [];\r\n tokens.forEach((token) => {\r\n if (token === '^') {\r\n mod = (mod === VF.ChordSymbol.symbolModifiers.SUPERSCRIPT) ?\r\n VF.ChordSymbol.symbolModifiers.NONE : VF.ChordSymbol.symbolModifiers.SUPERSCRIPT;\r\n } else if (token === '%') {\r\n mod = (mod === VF.ChordSymbol.symbolModifiers.SUBSCRIPT) ?\r\n VF.ChordSymbol.symbolModifiers.NONE : VF.ChordSymbol.symbolModifiers.SUBSCRIPT;\r\n } else if (token === '@') {\r\n isGlyph = !isGlyph;\r\n } else if (token.length) {\r\n if (isGlyph) {\r\n const glyph = SmoLyric._chordGlyphFromCode(token);\r\n blocks.push({\r\n glyph, symbolModifier: mod\r\n });\r\n } else {\r\n blocks.push({\r\n text: token, symbolModifier: mod\r\n });\r\n }\r\n }\r\n });\r\n return blocks;\r\n}\r\n\r\nexport function toVexStemDirection(note: SmoNote) {\r\n return (note.flagState === SmoNote.flagStates.up ? VF.Stem.UP : VF.Stem.DOWN);\r\n}","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { SmoMusic } from '../../smo/data/music';\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { SmoMeasure, SmoVoice, MeasureTickmaps } from '../../smo/data/measure';\r\nimport { SmoScore } from '../../smo/data/score';\r\nimport { SmoArticulation, SmoLyric, SmoOrnament } from '../../smo/data/noteModifiers';\r\nimport { VexFlow, StaveNoteStruct, TupletOptions, vexOrnaments } from '../../common/vex';\r\nimport { SmoBarline, SmoRehearsalMark } from '../../smo/data/measureModifiers';\r\nimport { SmoSelection, SmoSelector } from '../../smo/xform/selections';\r\nimport { SmoSystemStaff } from '../../smo/data/systemStaff';\r\nimport { getId } from '../../smo/data/common';\r\nimport { SmoSystemGroup } from '../../smo/data/scoreModifiers';\r\nimport { StaffModifierBase, SmoStaffHairpin, SmoSlur, SmoTie, SmoStaffTextBracket } from '../../smo/data/staffModifiers';\r\nimport { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, leftConnectorVx, rightConnectorVx,\r\n toVexVolta, getVexChordBlocks } from '../../render/vex/smoAdapter';\r\n\r\n\r\n\r\nconst VF = VexFlow;\r\nexport const fontStacks: Record = {\r\n Bravura: ['\"Bravura\"', '\"Gonville\"', '\"Custom\"'],\r\n Gonville: ['\"Gonville\"', '\"Bravura\"', '\"Custom\"'],\r\n Petaluma: ['\"Petaluma\"', '\"Bravura\"', '\"Gonville\"', '\"Custom\"'],\r\n Leland: ['\"Leland\"', '\"Bravura\"', '\"Gonville\"', '\"Custom\"'] \r\n}\r\ninterface LyricAdjust {\r\n verse: number, lyric: SmoLyric, \r\n}\r\ninterface VexNoteRenderInfo {\r\n smoNote: SmoNote,voiceIx: number, noteIx: number, tickmapObject: MeasureTickmaps, lyricAdj: string[]\r\n}\r\ninterface VexStaveGroupMusic {\r\n formatter: string, measures: SmoMeasure[], voiceStrings: string[], heightOffset: number, \r\n systemGroup?: SmoSystemGroup\r\n}\r\nfunction smoNoteToVexKeys(smoNote: SmoNote) {\r\n const noteHead = smoNote.isRest() ? 'r' : smoNote.noteHead;\r\n const keys = SmoMusic.smoPitchesToVexKeys(smoNote.pitches, 0, noteHead);\r\n return keys;\r\n}\r\nfunction smoNoteToGraceNotes(smoNote: SmoNote, strs: string[]) {\r\n const gar = smoNote.getGraceNotes();\r\n var toBeam = true;\r\n if (gar && gar.length) {\r\n const grGroup: string[] = [];\r\n gar.forEach((g) => {\r\n const grid = g.attrs.id;\r\n const args = JSON.stringify(g.toVexGraceNote());\r\n strs.push(`const ${grid} = new VF.GraceNote(JSON.parse('${args}'))`);\r\n strs.push(`${grid}.setAttribute('id', '${grid}');`);\r\n for (var i = 0; i < g.pitches.length; ++i) {\r\n const pitch = g.pitches[i];\r\n if (!pitch.accidental) {\r\n console.warn('no accidental in grace note');\r\n }\r\n if (pitch.accidental && pitch.accidental !== 'n' || pitch.cautionary) {\r\n const acid = 'acc' + i.toString() + grid;\r\n strs.push(`const ${acid} = new VF.Accidental('${pitch.accidental}');`);\r\n if (pitch.cautionary) {\r\n strs.push(`${acid}.setAsCautionary();`);\r\n }\r\n strs.push(`${grid}.addModifier(${acid}, ${i})`);\r\n }\r\n }\r\n if (g.tickCount() >= 4096) {\r\n toBeam = false;\r\n }\r\n grGroup.push(grid);\r\n });\r\n const ggid = 'ggrp' + smoNote.attrs.id;\r\n const grString = '[' + grGroup.join(',') + ']';\r\n strs.push(`const ${ggid} = new VF.GraceNoteGroup(${grString});`);\r\n if (toBeam) {\r\n strs.push(`${ggid}.beamNotes();`);\r\n }\r\n strs.push(`${smoNote.attrs.id}.addModifier(${ggid}, 0);`);\r\n }\r\n}\r\nfunction smoNoteToStaveNote(smoNote: SmoNote) {\r\n const duration =\r\n smoNote.isTuplet ?\r\n SmoMusic.closestVexDuration(smoNote.tickCount) :\r\n SmoMusic.ticksToDuration[smoNote.tickCount];\r\n const sn: StaveNoteStruct = {\r\n clef: smoNote.clef,\r\n duration,\r\n dots: smoNote.dots,\r\n type: smoNote.noteType\r\n };\r\n if (smoNote.flagState !== SmoNote.flagStates.auto) {\r\n sn.stem_direction = smoNote.flagState === SmoNote.flagStates.up ? 1 : -1;\r\n sn.auto_stem = false; \r\n } else {\r\n sn.auto_stem = true;\r\n }\r\n sn.keys = smoNoteToVexKeys(smoNote);\r\n return sn;\r\n}\r\nexport const getVoiceId = (smoMeasure:SmoMeasure, voiceIx: number) => {\r\n return smoMeasure.id + 'v' + voiceIx.toString();\r\n}\r\nfunction lastNoteInSystem(smoScore: SmoScore, selection: SmoSelection) {\r\n let rv = selection;\r\n let next: SmoSelection | null = null;\r\n next = SmoSelection.nextNoteSelection(smoScore, selection.selector.staff,\r\n selection.selector.measure, selection.selector.voice, selection.selector.tick);\r\n while (next) {\r\n if (next.measure.svg.rowInSystem !== selection.measure.svg.rowInSystem) {\r\n return rv;\r\n break;\r\n }\r\n rv = next;\r\n next = SmoSelection.nextNoteSelection(smoScore, next.selector.staff,\r\n next.selector.measure, next.selector.voice, next.selector.tick);\r\n }\r\n return rv;\r\n}\r\nfunction createMeasureModifiers(smoMeasure: SmoMeasure, strs: string[]) {\r\n const sb = smoMeasure.getStartBarline();\r\n const eb = smoMeasure.getEndBarline();\r\n const sym = smoMeasure.getRepeatSymbol();\r\n const vxStave = 'stave' + smoMeasure.id;\r\n if (smoMeasure.measureNumber.systemIndex !== 0 && sb.barline === SmoBarline.barlines.singleBar\r\n && smoMeasure.format.padLeft === 0) {\r\n strs.push(`${vxStave}.setBegBarType(VF.Barline.type.NONE);`);\r\n } else {\r\n strs.push(`${vxStave}.setBegBarType(${toVexBarlineType(sb)});`);\r\n }\r\n if (smoMeasure.svg.multimeasureLength > 0 && !smoMeasure.svg.hideMultimeasure) {\r\n const bl = vexBarlineType[smoMeasure.svg.multimeasureEndBarline];\r\n strs.push(`${vxStave}.setEndBarType(${bl});`);\r\n } else if (eb.barline !== SmoBarline.barlines.singleBar) {\r\n const bl = toVexBarlineType(eb);\r\n strs.push(`${vxStave}.setEndBarType(${bl});`);\r\n }\r\n if (smoMeasure.svg.rowInSystem === 0) {\r\n const rmb = smoMeasure.getRehearsalMark();\r\n const rm = rmb as SmoRehearsalMark;\r\n if (rm) {\r\n strs.push(`${vxStave}.setSection('${rm.symbol}', 0);`);\r\n }\r\n }\r\n const tempo = smoMeasure.getTempo();\r\n if (tempo && smoMeasure.svg.forceTempo) {\r\n const vexTempo = tempo.toVexTempo();\r\n const tempoString = JSON.stringify(vexTempo);\r\n strs.push(`${vxStave}.setTempo(JSON.parse('${tempoString}'), -1 * ${tempo.yOffset});`);\r\n }\r\n}\r\nexport function renderVoltas(smoScore: SmoScore, startMeasure: number, endMeasure: number, strs: string[]) {\r\n const voltas = smoScore.staves[0].getVoltaMap(startMeasure, endMeasure);\r\n for (var i = 0; i < voltas.length; ++i) {\r\n const ending = voltas[i];\r\n for (var j = ending.startBar; j <= ending.endBar; ++j) {\r\n const smoMeasure = smoScore.staves[0].measures[j];\r\n const vtype = toVexVolta(ending, smoMeasure.measureNumber.measureIndex);\r\n const vx = smoMeasure.staffX + ending.xOffsetStart;\r\n const vxStave = 'stave' + smoMeasure.id;\r\n const endingName = ending.attrs.id + smoMeasure.id;\r\n strs.push(`const ${endingName} = new VF.Volta(${vtype}, '${ending.number.toString()}', ${vx}, ${ending.yOffset});`);\r\n strs.push(`${endingName}.setContext(context).draw(${vxStave}, -1 * ${ending.xOffsetEnd});`);\r\n }\r\n }\r\n}\r\nfunction renderModifier(modifier: StaffModifierBase, startNote: SmoNote | null, endNote: SmoNote | null, strs: string[]) {\r\n const modifierName = getId();\r\n const startKey = SmoSelector.getNoteKey(modifier.startSelector);\r\n const endKey = SmoSelector.getNoteKey(modifier.endSelector);\r\n strs.push(`// modifier from ${startKey} to ${endKey}`);\r\n if (modifier.ctor === 'SmoStaffHairpin' && startNote && endNote) {\r\n const hp = modifier as SmoStaffHairpin; \r\n const vxStart = startNote.attrs.id;\r\n const vxEnd = startNote.attrs.id;\r\n const hpParams = { first_note: vxStart, last_note: vxEnd };\r\n strs.push(`const ${modifierName} = new VF.StaveHairpin({ first_note: ${vxStart}, last_note: ${vxEnd},\r\n firstNote: ${vxStart}, lastNote: ${vxEnd} });`);\r\n strs.push(`${modifierName}.setRenderOptions({ height: ${hp.height}, y_shift: ${hp.yOffset}, left_shift_px: ${hp.xOffsetLeft},right_shift_px: ${hp.xOffsetRight} });`);\r\n strs.push(`${modifierName}.setContext(context).setPosition(${hp.position}).draw();`);\r\n } else if (modifier.ctor === 'SmoSlur') {\r\n const slur = modifier as SmoSlur; \r\n const vxStart = startNote?.attrs?.id ?? 'null';\r\n const vxEnd = endNote?.attrs?.id ?? 'null'; \r\n const svgPoint: SVGPoint[] = JSON.parse(JSON.stringify(slur.controlPoints));\r\n let slurX = 0;\r\n if (startNote === null || endNote === null) {\r\n slurX = -5;\r\n svgPoint[0].y = 10;\r\n svgPoint[1].y = 10;\r\n }\r\n if (modifier.startSelector.staff === modifier.endSelector.staff) {\r\n const hpParams = {\r\n thickness: slur.thickness,\r\n xShift: slurX,\r\n yShift: slur.yOffset,\r\n cps: svgPoint,\r\n invert: slur.invert,\r\n position: slur.position,\r\n positionEnd: slur.position_end\r\n };\r\n const paramStrings = JSON.stringify(hpParams);\r\n strs.push(`const ${modifierName} = new VF.Curve(${vxStart}, ${vxEnd}, JSON.parse('${paramStrings}'));`);\r\n strs.push(`${modifierName}.setContext(context).draw();`);\r\n }\r\n } else if (modifier.ctor === 'SmoTie') {\r\n const ctie = modifier as SmoTie;\r\n const vxStart = startNote?.attrs?.id ?? 'null';\r\n const vxEnd = endNote?.attrs?.id ?? 'null'; \r\n // TODO: handle case of overlap\r\n if (modifier.startSelector.staff === modifier.endSelector.staff) {\r\n if (ctie.lines.length > 0) {\r\n // Hack: if a chord changed, the ties may no longer be valid. We should check\r\n // this when it changes.\r\n const fromLines = ctie.lines.map((ll) => ll.from);\r\n const toLines = ctie.lines.map((ll) => ll.to);\r\n strs.push(`const ${modifierName} = new VF.StaveTie({ first_note: ${vxStart}, last_note: ${vxEnd}, \r\n firstNote: ${vxStart}, lastNote: ${vxEnd}, first_indices: [${fromLines}], last_indices: [${toLines}]});`);\r\n strs.push(`${modifierName}.setContext(context).draw();`);\r\n }\r\n }\r\n } else if (modifier.ctor === 'SmoStaffTextBracket' && startNote && endNote) {\r\n const ctext = modifier as SmoStaffTextBracket;\r\n const vxStart = startNote.attrs.id;\r\n const vxEnd = endNote.attrs.id;\r\n if (vxStart && vxEnd) {\r\n strs.push(`const ${modifierName} = new VF.TextBracket({ start: ${vxStart}, stop: ${vxEnd}, text: '${ctext.text}', position: ${ctext.position} });`);\r\n strs.push(`${modifierName}.setLine(${ctext.line}).setContext(context).draw();`);\r\n }\r\n }\r\n}\r\nfunction renderModifiers(smoScore: SmoScore, staff: SmoSystemStaff, \r\n startMeasure: number, endMeasure: number, strs: string[]) {\r\n const modifiers = staff.renderableModifiers.filter((mm) => mm.startSelector.measure >= startMeasure && mm.endSelector.measure <= endMeasure);\r\n modifiers.forEach((modifier) => {\r\n const startNote = SmoSelection.noteSelection(smoScore,\r\n modifier.startSelector.staff, modifier.startSelector.measure, modifier.startSelector.voice, modifier.startSelector.tick);\r\n const endNote = SmoSelection.noteSelection(smoScore,\r\n modifier.endSelector.staff, modifier.endSelector.measure, modifier.endSelector.voice, modifier.endSelector.tick);\r\n // TODO: handle case of multiple line slur/tie\r\n if (startNote && startNote.note && endNote && endNote.note) {\r\n if (endNote.measure.svg.lineIndex !== startNote.measure.svg.lineIndex) {\r\n const endFirst = lastNoteInSystem(smoScore, startNote);\r\n if (endFirst && endFirst.note) {\r\n const startLast = SmoSelection.noteSelection(smoScore, endNote.selector.staff,\r\n endNote.selector.measure, 0, 0);\r\n if (startLast && startLast.note) {\r\n renderModifier(modifier, startNote.note, null, strs);\r\n renderModifier(modifier, null, endNote.note, strs);\r\n }\r\n }\r\n } else {\r\n renderModifier(modifier, startNote.note, endNote.note, strs);\r\n }\r\n }\r\n });\r\n}\r\nfunction createStaveNote(renderInfo: VexNoteRenderInfo, key: string, row: number, strs: string[]) {\r\n const { smoNote, voiceIx, noteIx, tickmapObject, lyricAdj } = { ...renderInfo };\r\n const id = smoNote.attrs.id;\r\n const ctorInfo = smoNoteToStaveNote(smoNote);\r\n const ctorString = JSON.stringify(ctorInfo);\r\n if (smoNote.noteType === '/') {\r\n strs.push(`const ${id} = new VF.GlyphNote(new VF.Glyph('repeatBarSlash', 40), { duration: '${ctorInfo.duration}' });`)\r\n } else {\r\n strs.push(`const ${id} = new VF.StaveNote(JSON.parse('${ctorString}'))`);\r\n }\r\n smoNoteToGraceNotes(smoNote, strs);\r\n strs.push(`${id}.setAttribute('id', '${id}');`);\r\n if (smoNote.fillStyle) {\r\n strs.push(`${id}.setStyle({ fillStyle: '${smoNote.fillStyle}' });`);\r\n } else if (voiceIx > 0) {\r\n strs.push(`${id}.setStyle({ fillStyle: \"#115511\" });`);\r\n } else if (smoNote.isHidden()) {\r\n strs.push(`${id}.setStyle({ fillStyle: \"#ffffff00\" });`);\r\n }\r\n if (smoNote.noteType === 'n') {\r\n smoNote.pitches.forEach((pitch, ix) => {\r\n const zz = SmoMusic.accidentalDisplay(pitch, key,\r\n tickmapObject.tickmaps[voiceIx].durationMap[noteIx], tickmapObject.accidentalArray);\r\n if (zz) {\r\n const aname = id + ix.toString() + 'acc';\r\n strs.push(`const ${aname} = new VF.Accidental('${zz.symbol}');`);\r\n if (zz.courtesy) {\r\n strs.push(`${aname}.setAsCautionary();`);\r\n }\r\n strs.push(`${id}.addModifier(${aname}, ${ix});`);\r\n }\r\n }); \r\n }\r\n for (var i = 0; i < smoNote.dots; ++i) {\r\n for (var j = 0; j < smoNote.pitches.length; ++j) {\r\n strs.push(`${id}.addModifier(new VF.Dot(), ${j});`); \r\n }\r\n }\r\n smoNote.articulations.forEach((aa) => {\r\n const position: number = SmoArticulation.positionToVex[aa.position];\r\n const vexArt = SmoArticulation.articulationToVex[aa.articulation];\r\n const sn = getId();\r\n strs.push(`const ${sn} = new VF.Articulation('${vexArt}').setPosition(${position});`);\r\n strs.push(`${id}.addModifier(${sn}, 0);`);\r\n });\r\n smoNote.getJazzOrnaments().forEach((ll) => {\r\n const vexCode = ll.toVex();\r\n strs.push(`const ${ll.attrs.id} = new VF.Ornament('${vexCode}');`)\r\n strs.push(`${id}.addModifier(${ll.attrs.id}, 0);`);\r\n });\r\n smoNote.getOrnaments().forEach((ll) => {\r\n const vexCode = vexOrnaments[ll.ornament];\r\n strs.push(`const ${ll.attrs.id} = new VF.Ornament('${vexCode}');`);\r\n if (ll.offset === SmoOrnament.offsets.after) {\r\n strs.push(`${ll.attrs.id}.setDelayed(true);`);\r\n }\r\n strs.push(`${id}.addModifier(${ll.attrs.id}, 0);`);\r\n });\r\n const lyrics = smoNote.getTrueLyrics();\r\n if (smoNote.noteType !== '/') {\r\n lyrics.forEach((bll) => {\r\n const ll = bll as SmoLyric;\r\n let classString = 'lyric lyric-' + ll.verse;\r\n let text = ll.getText();\r\n if (!ll.skipRender) {\r\n if (!text.length && ll.isHyphenated()) {\r\n text = '-';\r\n }\r\n // no text, no hyphen, don't add it.\r\n if (text.length) {\r\n const sn = ll.attrs.id;\r\n text = text.replace(\"'\",\"\\\\'\");\r\n strs.push(`const ${sn} = new VF.Annotation('${text}');`);\r\n strs.push(`${sn}.setAttribute('id', '${sn}');`);\r\n const weight = ll.fontInfo.weight ?? 'normal';\r\n strs.push(`${sn}.setFont('${ll.fontInfo.family}', ${ll.fontInfo.size}, '${weight}');`)\r\n strs.push(`${sn}.setVerticalJustification(VF.Annotation.VerticalJustify.BOTTOM);`);\r\n strs.push(`${id}.addModifier(${sn});`);\r\n if (ll.adjY > 0) {\r\n const adjy = Math.round(ll.adjY);\r\n lyricAdj.push(`context.svg.getElementById('vf-${sn}').setAttributeNS('', 'transform', 'translate(0 ${adjy})');`);\r\n }\r\n if (ll.isHyphenated()) {\r\n classString += ' lyric-hyphen';\r\n }\r\n strs.push(`${sn}.addClass('${classString}');`);\r\n }\r\n }\r\n });\r\n }\r\n const chords = smoNote.getChords();\r\n chords.forEach((chord) => {\r\n strs.push(`const ${chord.attrs.id} = new VF.ChordSymbol();`);\r\n strs.push(`${chord.attrs.id}.setAttribute('id', '${chord.attrs.id}');`);\r\n const vblocks = getVexChordBlocks(chord);\r\n vblocks.forEach((vblock) => {\r\n const glyphParams = JSON.stringify(vblock);\r\n if (vblock.glyph) {\r\n strs.push(`${chord.attrs.id}.addGlyphOrText('${vblock.glyph}', JSON.parse('${glyphParams}'));`);\r\n } else {\r\n const btext = vblock.text ?? '';\r\n if (btext.trim().length) {\r\n strs.push(`${chord.attrs.id}.addGlyphOrText('${btext}', JSON.parse('${glyphParams}'));`);\r\n }\r\n }\r\n });\r\n strs.push(`${chord.attrs.id}.setFont('${chord.fontInfo.family}', ${chord.fontInfo.size}).setReportWidth(${chord.adjustNoteWidth});`);\r\n strs.push(`${id}.addModifier(${chord.attrs.id}, 0);`);\r\n });\r\n return id;\r\n}\r\nfunction createColumn(groups: Record, strs: string[]) {\r\n const groupKeys = Object.keys(groups);\r\n let maxXAdj = 0;\r\n groupKeys.forEach((groupKey) => {\r\n const music = groups[groupKey];\r\n // Need to create beam groups before formatting\r\n strs.push(`// create beam groups and tuplets for format grp ${groupKey} before formatting`);\r\n music.measures.forEach((smoMeasure) => {\r\n maxXAdj = Math.max(maxXAdj, smoMeasure.svg.adjX);\r\n createBeamGroups(smoMeasure, strs);\r\n createTuplets(smoMeasure, strs);\r\n });\r\n strs.push(' ');\r\n strs.push(`// formatting measures in staff group ${groupKey}`);\r\n // set x offset for alignment before format\r\n music.measures.forEach((smoMeasure) => {\r\n smoMeasure.voices.forEach((vv) => {\r\n vv.notes.forEach((nn) => {\r\n const id = nn.attrs.id;\r\n const offset = maxXAdj - smoMeasure.svg.adjX;\r\n strs.push(`${id}.setXShift(${offset});`);\r\n });\r\n });\r\n });\r\n const joinVoiceStr = '[' + music.voiceStrings.join(',') + ']';\r\n const widthMeasure = music.measures[0];\r\n const staffWidth = Math.round(widthMeasure.staffWidth -\r\n (widthMeasure.svg.maxColumnStartX + widthMeasure.svg.adjRight + widthMeasure.format.padLeft) - 10);\r\n strs.push(`${music.formatter}.format(${joinVoiceStr}, ${staffWidth});`);\r\n music.measures.forEach((smoMeasure) => {\r\n createMeasure(smoMeasure, music.heightOffset, strs);\r\n });\r\n });\r\n}\r\nfunction createBeamGroups(smoMeasure: SmoMeasure, strs: string[]) {\r\n smoMeasure.voices.forEach((voice, voiceIx) => {\r\n const bgs = smoMeasure.beamGroups.filter((bb) => bb.voice === voiceIx);\r\n for (var i = 0; i < bgs.length; ++i) {\r\n const bg = bgs[i];\r\n let keyNoteIx = bg.notes.findIndex((nn) => nn.noteType === 'n');\r\n keyNoteIx = (keyNoteIx >= 0) ? keyNoteIx : 0;\r\n const sdName = 'dir' + bg.attrs.id;\r\n strs.push(`const ${sdName} = ${bg.notes[keyNoteIx].attrs.id}.getStemDirection();`);\r\n const nar: string[] = [];\r\n for (var j = 0; j < bg.notes.length; ++j) {\r\n const note = bg.notes[j];\r\n const vexNote = `${note.attrs.id}`;\r\n if (note.noteType !== '/') {\r\n nar.push(vexNote);\r\n }\r\n if (note.noteType !== 'n') {\r\n continue;\r\n }\r\n strs.push(`${vexNote}.setStemDirection(${sdName});`);\r\n }\r\n const narString = '[' + nar.join(',') + ']';\r\n strs.push(`const ${bg.attrs.id} = new VF.Beam(${narString});`);\r\n }\r\n });\r\n}\r\nfunction createTuplets(smoMeasure: SmoMeasure, strs: string[]) {\r\n smoMeasure.voices.forEach((voice, voiceIx) => {\r\n const tps = smoMeasure.tuplets.filter((tp) => tp.voice === voiceIx);\r\n for (var i = 0; i < tps.length; ++i) {\r\n const tp = tps[i];\r\n const nar: string[] = [];\r\n for (var j = 0; j < tp.notes.length; ++j) {\r\n const note = tp.notes[j];\r\n const vexNote = `${note.attrs.id}`;\r\n nar.push(vexNote);\r\n }\r\n const direction = tp.getStemDirection(smoMeasure.clef) === SmoNote.flagStates.up ?\r\n VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM;\r\n const tpParams: TupletOptions = {\r\n num_notes: tp.num_notes,\r\n notes_occupied: tp.notes_occupied,\r\n ratioed: false,\r\n bracketed: true,\r\n location: direction\r\n };\r\n const tpParamString = JSON.stringify(tpParams);\r\n const narString = '[' + nar.join(',') + ']';\r\n strs.push(`const ${tp.id} = new VF.Tuplet(${narString}, JSON.parse('${tpParamString}'));`);\r\n }\r\n });\r\n}\r\nfunction createMeasure(smoMeasure: SmoMeasure, heightOffset: number, strs: string[]) {\r\n const ssid = 'stave' + smoMeasure.id;\r\n const staffY = smoMeasure.svg.staffY + heightOffset;\r\n const staffWidth = Math.round(smoMeasure.svg.staffWidth);\r\n strs.push(`const ${ssid} = new VF.Stave(${smoMeasure.svg.staffX}, ${staffY}, ${staffWidth});`);\r\n strs.push(`${ssid}.setAttribute('id', '${ssid}');`);\r\n createMeasureModifiers(smoMeasure, strs);\r\n if (smoMeasure.svg.forceClef) {\r\n strs.push(`${ssid}.addClef('${smoMeasure.clef}');`);\r\n }\r\n if (smoMeasure.svg.forceTimeSignature) {\r\n const ts = smoMeasure.timeSignature;\r\n let tsString = ts.timeSignature;\r\n if (smoMeasure.timeSignature.useSymbol && ts.actualBeats === 4 && ts.beatDuration === 4) {\r\n tsString = 'C';\r\n } else if (smoMeasure.timeSignature.useSymbol && ts.actualBeats === 2 && ts.beatDuration === 4) {\r\n tsString = 'C|';\r\n } else if (smoMeasure.timeSignature.displayString.length) {\r\n tsString = smoMeasure.timeSignature.displayString;\r\n }\r\n strs.push(`${ssid}.addTimeSignature('${tsString}');`);\r\n }\r\n if (smoMeasure.svg.forceKeySignature) {\r\n const key = SmoMusic.vexKeySignatureTranspose(smoMeasure.keySignature, 0);\r\n const ksid = 'key' + smoMeasure.id;\r\n strs.push(`const ${ksid} = new VF.KeySignature('${key}');`);\r\n if (smoMeasure.canceledKeySignature) {\r\n const canceledKey = SmoMusic.vexKeySignatureTranspose(smoMeasure.canceledKeySignature, 0);\r\n strs.push(`${ksid}.cancelKey('${canceledKey}');`);\r\n }\r\n strs.push(`${ksid}.addToStave(${ssid});`);\r\n }\r\n strs.push(`${ssid}.setContext(context);`);\r\n strs.push(`${ssid}.draw();`);\r\n smoMeasure.voices.forEach((voice, voiceIx) => {\r\n const vs = getVoiceId(smoMeasure, voiceIx);\r\n strs.push(`${vs}.draw(context, ${ssid});`);\r\n });\r\n smoMeasure.beamGroups.forEach((bg) => {\r\n strs.push(`${bg.attrs.id}.setContext(context);`);\r\n strs.push(`${bg.attrs.id}.draw();`)\r\n });\r\n smoMeasure.tuplets.forEach((tp) => {\r\n strs.push(`${tp.id}.setContext(context).draw();`)\r\n })\r\n}\r\n// ## SmoToVex\r\n// Simple serialize class that produced VEX note and voice objects\r\n// for vex EasyScore (for easier bug reports and test cases)\r\nexport class SmoToVex {\r\n static convert(smoScore: SmoScore, options: any): string {\r\n let div = 'boo';\r\n let page = 0;\r\n options = options ?? {};\r\n if (typeof(options['div']) === 'string') {\r\n div = options.div\r\n }\r\n if (typeof(options['page']) === 'number') {\r\n page = options.page;\r\n }\r\n let startMeasure = -1;\r\n let endMeasure = -1;\r\n const strs: string[] = [];\r\n const pageHeight = smoScore.layoutManager?.getGlobalLayout().pageHeight ?? 1056;\r\n const pageWidth = smoScore.layoutManager?.getGlobalLayout().pageWidth ?? 816;\r\n const pageLength = smoScore.staves[0].measures[smoScore.staves[0].measures.length - 1].svg.pageIndex + 1;\r\n let scoreName = smoScore.scoreInfo.title + ' p ' + (page + 1).toString() + '/' + pageLength.toString();\r\n const scoreSub = smoScore.scoreInfo.subTitle?.length ? `(${smoScore.scoreInfo.subTitle})` : '';\r\n scoreName = `${scoreName} ${scoreSub} by ${smoScore.scoreInfo.composer}`;\r\n strs.push(`// @@ ${scoreName}`);\r\n strs.push('function main() {');\r\n strs.push('// create the div and svg element for the music');\r\n strs.push(`const div = document.getElementById('${div}');`);\r\n strs.push('const VF = Vex.Flow;');\r\n strs.push(`const renderer = new VF.Renderer(div, VF.Renderer.Backends.SVG);`);\r\n const zoomScale = (smoScore.layoutManager?.getZoomScale() ?? 1.0);\r\n const svgScale = (smoScore.layoutManager?.getGlobalLayout().svgScale ?? 1.0);\r\n const width = zoomScale * pageWidth;\r\n const height = zoomScale * pageHeight;\r\n const scale = svgScale * zoomScale;\r\n const heightOffset = -1 * (height * page) / scale;\r\n const vbWidth = Math.round(width / scale);\r\n const vbHeight = Math.round(height / scale);\r\n strs.push('const context = renderer.getContext();');\r\n strs.push('const svg = context.svg');\r\n strs.push(`svg.setAttributeNS('', 'width', '${width}');`);\r\n strs.push(`svg.setAttributeNS('', 'height', '${height}');`);\r\n strs.push(`svg.setAttributeNS('', 'viewBox', '0 0 ${vbWidth} ${vbHeight}');`);\r\n strs.push('//');\r\n strs.push('// create the musical objects');\r\n const font = smoScore.fonts.find((x) => x.purpose === SmoScore.fontPurposes.ENGRAVING);\r\n if (font) {\r\n const fs = fontStacks[font.family].join(',');\r\n strs.push(`VF.setMusicFont(${fs});`);\r\n }\r\n const measureCount = smoScore.staves[0].measures.length;\r\n const lyricAdj: string[] = [];\r\n for (var k = 0; k < measureCount; ++k) {\r\n const groupMap: Record = {};\r\n if (smoScore.staves[0].measures[k].svg.pageIndex < page) {\r\n continue;\r\n }\r\n if (smoScore.staves[0].measures[k].svg.pageIndex > page) {\r\n break;\r\n }\r\n startMeasure = startMeasure < 0 ? k : startMeasure;\r\n endMeasure = Math.max(k, endMeasure);\r\n smoScore.staves.forEach((smoStaff, staffIx) => {\r\n const smoMeasure = smoStaff.measures[k];\r\n const selection = SmoSelection.measureSelection(smoScore, smoStaff.staffId, smoMeasure.measureNumber.measureIndex);\r\n if (!selection) {\r\n throw('ouch no selection');\r\n }\r\n const systemGroup = smoScore.getSystemGroupForStaff(selection);\r\n const justifyGroup: string = (systemGroup && smoMeasure.format.autoJustify) ? systemGroup.attrs.id : selection.staff.attrs.id;\r\n const tickmapObject = smoMeasure.createMeasureTickmaps();\r\n const measureIx = smoMeasure.measureNumber.measureIndex;\r\n const voiceStrings: string[] = [];\r\n const fmtid = 'fmt' + smoMeasure.id + measureIx.toString();\r\n strs.push(`const ${fmtid} = new VF.Formatter();`);\r\n if (!groupMap[justifyGroup]) {\r\n groupMap[justifyGroup] = {\r\n formatter: fmtid,\r\n measures: [],\r\n heightOffset,\r\n voiceStrings: [],\r\n systemGroup\r\n }\r\n }\r\n groupMap[justifyGroup].measures.push(smoMeasure);\r\n strs.push('//');\r\n strs.push(`// voices and notes for stave ${smoStaff.staffId} ${smoMeasure.measureNumber.measureIndex}`);\r\n smoMeasure.voices.forEach((smoVoice: SmoVoice, voiceIx: number) => { \r\n const vn = getVoiceId(smoMeasure, voiceIx);\r\n groupMap[justifyGroup].voiceStrings.push(vn);\r\n const vc = vn + 'ar';\r\n const ts = JSON.stringify({\r\n numBeats: smoMeasure.timeSignature.actualBeats,\r\n beatValue: smoMeasure.timeSignature.beatDuration\r\n });\r\n strs.push(`const ${vn} = new VF.Voice(JSON.parse('${ts}')).setMode(VF.Voice.Mode.SOFT);`);\r\n strs.push(`const ${vc} = [];`);\r\n smoVoice.notes.forEach((smoNote: SmoNote, noteIx: number) => {\r\n const renderInfo: VexNoteRenderInfo = { smoNote, voiceIx, noteIx, tickmapObject, lyricAdj };\r\n const noteId = createStaveNote(renderInfo, smoMeasure.keySignature, smoMeasure.svg.rowInSystem, strs);\r\n strs.push(`${vc}.push(${noteId});`);\r\n });\r\n strs.push(`${vn}.addTickables(${vc})`);\r\n voiceStrings.push(vn);\r\n strs.push(`${fmtid}.joinVoices([${vn}]);`);\r\n });\r\n if (smoMeasure.svg.rowInSystem === smoScore.staves.length - 1) {\r\n createColumn(groupMap, strs);\r\n const mapKeys = Object.keys(groupMap);\r\n mapKeys.forEach((mapKey) => {\r\n const tmpGroup = groupMap[mapKey];\r\n if (tmpGroup.systemGroup) {\r\n const systemIndex = smoMeasure.measureNumber.systemIndex;\r\n const startMeasure = 'stave' + smoScore.staves[tmpGroup.systemGroup.startSelector.staff].measures[k].id;\r\n const endMeasure = 'stave' + smoScore.staves[tmpGroup.systemGroup.endSelector.staff].measures[k].id;\r\n const leftConnector = leftConnectorVx(tmpGroup.systemGroup);\r\n const rightConnector = rightConnectorVx(tmpGroup.systemGroup);\r\n const jgname = justifyGroup + startMeasure + staffIx.toString();\r\n if (systemIndex === 0 && smoScore.staves.length > 1) {\r\n strs.push(`const left${jgname} = new VF.StaveConnector(${startMeasure}, ${endMeasure}).setType(${leftConnector});`);\r\n strs.push(`left${jgname}.setContext(context).draw();`);\r\n }\r\n let endStave = false;\r\n if (smoMeasure.measureNumber.systemIndex !== 0) {\r\n if (smoMeasure.measureNumber.systemIndex === smoScore.staves[0].measures.length - 1) {\r\n endStave = true;\r\n } else if (smoScore.staves[0].measures.length > k + 1 &&\r\n smoScore.staves[0].measures[k + 1].measureNumber.systemIndex === 0) {\r\n endStave = true;\r\n }\r\n }\r\n if (endStave) {\r\n strs.push(`const right${jgname} = new VF.StaveConnector(${startMeasure}, ${endMeasure}).setType(${rightConnector});`);\r\n strs.push(`right${jgname}.setContext(context).draw();`);\r\n } \r\n }\r\n });\r\n }\r\n });\r\n }\r\n smoScore.staves.forEach((staff) => {\r\n renderModifiers(smoScore, staff, startMeasure, endMeasure, strs);\r\n });\r\n renderVoltas(smoScore, startMeasure, endMeasure, strs);\r\n if (lyricAdj.length) {\r\n strs.push('// ');\r\n strs.push('// Align lyrics on different measures, once they are rendered.');\r\n }\r\n const render = strs.concat(lyricAdj);\r\n render.push('}');\r\n return render.join(`\\n`);\r\n // console.log(render.join(`\\n`));\r\n }\r\n}","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\n// ## Description:\r\n// This file calls the vexflow routines that actually render a\r\n// measure of music. If multiple measures are justified in a\r\n// column, the rendering is deferred until all the measures have been\r\n// preformatted.\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { SmoMusic } from '../../smo/data/music';\r\nimport { layoutDebug } from '../sui/layoutDebug';\r\nimport { SmoRepeatSymbol, SmoMeasureText, SmoBarline, SmoMeasureModifierBase, SmoRehearsalMark } from '../../smo/data/measureModifiers';\r\nimport { SourceSerifProFont } from '../../styles/font_metrics/ssp-serif-metrics';\r\nimport { SmoOrnament, SmoArticulation, SmoDynamicText, SmoLyric, \r\n SmoArpeggio, SmoNoteModifierBase, VexAnnotationParams, SmoTabNote } from '../../smo/data/noteModifiers';\r\nimport { SmoSelection } from '../../smo/xform/selections';\r\nimport { SmoMeasure, MeasureTickmaps } from '../../smo/data/measure';\r\nimport { SvgHelpers } from '../sui/svgHelpers';\r\nimport { Clef, IsClef } from '../../smo/data/common';\r\nimport { SvgPage } from '../sui/svgPageMap';\r\nimport { SmoTabStave } from '../../smo/data/staffModifiers';\r\nimport { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, toVexSymbol,\r\n toVexTextJustification, toVexTextPosition, getVexChordBlocks, toVexStemDirection,\r\n VexTabNotePositions } from './smoAdapter';\r\nimport { VexFlow, Stave,StemmableNote, Note, Beam, Tuplet, Voice,\r\n Formatter, Accidental, Annotation, StaveNoteStruct, StaveText, StaveModifier,\r\n createStaveText, renderDynamics, applyStemDirection,\r\n getVexNoteParameters, defaultNoteScale, defaultCueScale, getVexTuplets,\r\n createStave, createVoice, getOrnamentGlyph, getSlashGlyph, getRepeatBar, getMultimeasureRest,\r\n createTextNote, TabStave, createTabStave, TabNotePosition, TabNoteStruct,\r\n CreateVexNoteParams, TabNote\r\n } from '../../common/vex';\r\n\r\nimport { VxMeasureIf, VexNoteModifierIf, VxNote } from './vxNote';\r\nimport { vexGlyph } from './glyphDimensions';\r\nconst VF = VexFlow;\r\n\r\ndeclare var $: any;\r\n// const VF = eval('Vex.Flow');\r\n\r\n/**\r\n * This is the interface for VexFlow library that actually does the engraving.\r\n * @category SuiRender\r\n */\r\nexport class VxMeasure implements VxMeasureIf {\r\n context: SvgPage;\r\n printing: boolean;\r\n selection: SmoSelection;\r\n softmax: number;\r\n smoMeasure: SmoMeasure;\r\n smoTabStave?: SmoTabStave;\r\n tabStave?: TabStave;\r\n rendered: boolean = false;\r\n noteToVexMap: Record = {};\r\n beamToVexMap: Record = {};\r\n tupletToVexMap: Record = {};\r\n multimeasureRest: any | null = null;\r\n vexNotes: Note[] = [];\r\n vexBeamGroups: Beam[] = [];\r\n vexTuplets: Tuplet[] = [];\r\n tickmapObject: MeasureTickmaps | null = null;\r\n stave: Stave | null = null; // vex stave\r\n voiceNotes: Note[] = []; // notes for current voice, as rendering\r\n tabNotes: Note[] = [];\r\n voiceAr: Voice[] = [];\r\n tabVoice: Voice | null = null;\r\n formatter: Formatter | null = null;\r\n allCues: boolean = false;\r\n modifiersToBox: SmoNoteModifierBase[] = [];\r\n collisionMap: Record = {};\r\n dbgLeftX: number = 0;\r\n dbgWidth: number = 0;\r\n\r\n constructor(context: SvgPage, selection: SmoSelection, printing: boolean, softmax: number) {\r\n this.context = context;\r\n this.rendered = false;\r\n this.selection = selection;\r\n this.smoMeasure = this.selection.measure;\r\n this.printing = printing;\r\n this.allCues = selection.staff.partInfo.displayCues;\r\n this.tupletToVexMap = {};\r\n this.vexNotes = [];\r\n this.vexBeamGroups = [];\r\n this.vexBeamGroups = [];\r\n this.beamToVexMap = {};\r\n this.softmax = softmax;\r\n this.smoTabStave = selection.staff.getTabStaveForMeasure(selection.selector);\r\n }\r\n\r\n static get fillStyle() {\r\n return '#000';\r\n }\r\n\r\n isWholeRest() {\r\n return (this.smoMeasure.voices.length === 1 &&\r\n this.smoMeasure.voices[0].notes.length === 1 &&\r\n this.smoMeasure.voices[0].notes[0].isRest()\r\n );\r\n }\r\n createCollisionTickmap() {\r\n let i = 0;\r\n let j = 0;\r\n if (!this.tickmapObject) {\r\n return;\r\n }\r\n for (i = 0; i < this.smoMeasure.voices.length; ++i) {\r\n const tm = this.tickmapObject.tickmaps[i];\r\n for (j = 0; j < tm.durationMap.length; ++j) {\r\n if (typeof(this.collisionMap[tm.durationMap[j]]) === 'undefined') {\r\n this.collisionMap[tm.durationMap[j]] = [];\r\n }\r\n this.collisionMap[tm.durationMap[j]].push(this.smoMeasure.voices[i].notes[j]);\r\n }\r\n }\r\n }\r\n isCollision(voiceIx: number, tickIx: number): boolean {\r\n let i = 0;\r\n let j = 0;\r\n let k = 0;\r\n let staffLines: number[] = [];\r\n if (!this.tickmapObject) {\r\n return false;\r\n }\r\n const tick = this.tickmapObject.tickmaps[voiceIx].durationMap[tickIx];\r\n // Just one note, no collision\r\n if (this.collisionMap[tick].length < 2) {\r\n return false;\r\n }\r\n for (i = 0; i < this.collisionMap[tick].length; ++i) {\r\n const note = this.collisionMap[tick][i];\r\n for (j = 0; j < note.pitches.length; ++j) {\r\n const clef: Clef = IsClef(note.clef) ? note.clef : 'treble';\r\n const pitch = note.pitches[j];\r\n const curLine = SmoMusic.pitchToStaffLine(clef, pitch);\r\n for (k = 0;k < staffLines.length; ++k) {\r\n if (Math.abs(curLine - staffLines[k]) < 1) {\r\n return true;\r\n }\r\n }\r\n staffLines.push(curLine);\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * convert a smoNote into a vxNote so it can be rasterized\r\n * @param smoNote \r\n * @param tickIndex - used to calculate accidental\r\n * @param voiceIx \r\n * @returns \r\n */\r\n createVexNote(smoNote: SmoNote, tickIndex: number, voiceIx: number) {\r\n let vexNote: Note | null = null;\r\n let smoTabNote: SmoTabNote | null = null;\r\n let timestamp = new Date().valueOf();\r\n let tabNote: StemmableNote | null = null;\r\n const closestTicks = SmoMusic.closestVexDuration(smoNote.tickCount);\r\n const exactTicks = SmoMusic.ticksToDuration[smoNote.tickCount];\r\n const noteHead = smoNote.isRest() ? 'r' : smoNote.noteHead;\r\n const keys = SmoMusic.smoPitchesToVexKeys(smoNote.pitches, 0, noteHead);\r\n const smoNoteParams: CreateVexNoteParams = {\r\n isTuplet: smoNote.isTuplet, measureIndex: this.smoMeasure.measureNumber.measureIndex,\r\n clef: smoNote.clef,\r\n closestTicks, exactTicks, keys,\r\n noteType: smoNote.noteType };\r\n const { noteParams, duration } = getVexNoteParameters(smoNoteParams);\r\n if (this.tabStave && this.smoTabStave) {\r\n smoTabNote = this.smoTabStave.getTabNoteFromNote(smoNote, this.smoMeasure.transposeIndex);\r\n if (smoTabNote) {\r\n const positions: TabNotePosition[] = VexTabNotePositions(this.smoTabStave, smoTabNote, smoNote);\r\n if (positions.length) {\r\n if (!smoNote.isRest()) {\r\n tabNote = new VF.TabNote({ positions, duration: duration });\r\n if (this.smoTabStave.showStems) {\r\n tabNote.render_options.draw_stem = true;\r\n tabNote.render_options.draw_dots = true;\r\n tabNote.render_options.draw_stem_through_stave = smoTabNote.flagThrough;\r\n }\r\n } else {\r\n tabNote = new VF.StaveNote(noteParams);\r\n }\r\n }\r\n } \r\n }\r\n if (smoNote.noteType === '/') {\r\n // vexNote = new VF.GlyphNote('\\uE504', { duration });\r\n vexNote = getSlashGlyph();\r\n smoNote.renderId = 'vf-' + vexNote.getAttribute('id'); // where does 'vf' come from?\r\n } else {\r\n const smoVexStemParams = {\r\n voiceCount: this.smoMeasure.voices.length,\r\n voiceIx,\r\n isAuto: smoNote.flagState === SmoNote.flagStates.auto,\r\n isUp: smoNote.flagState === SmoNote.flagStates.up\r\n }\r\n applyStemDirection(smoVexStemParams, noteParams);\r\n if (smoTabNote && tabNote) {\r\n tabNote.setStemDirection(noteParams.stem_direction);\r\n }\r\n layoutDebug.setTimestamp(layoutDebug.codeRegions.PREFORMATA, new Date().valueOf() - timestamp);\r\n timestamp = new Date().valueOf();\r\n vexNote = new VF.StaveNote(noteParams);\r\n if (voiceIx > 0 && this.isCollision(voiceIx, tickIndex)) {\r\n vexNote.setXShift(-10);\r\n }\r\n if (this.isWholeRest()) {\r\n noteParams.duration = 'wr';\r\n vexNote = new VF.StaveNote(noteParams);\r\n vexNote.setCenterAlignment(true);\r\n }\r\n layoutDebug.setTimestamp(layoutDebug.codeRegions.PREFORMATB, new Date().valueOf() - timestamp);\r\n timestamp = new Date().valueOf();\r\n if (smoNote.fillStyle && !this.printing) {\r\n vexNote.setStyle({ fillStyle: smoNote.fillStyle });\r\n } else if (voiceIx > 0 && !this.printing) {\r\n vexNote.setStyle({ fillStyle: \"#115511\" });\r\n } else if (smoNote.isHidden() && this.printing) {\r\n vexNote.setStyle({ fillStyle: \"#ffffff00\" });\r\n }\r\n smoNote.renderId = 'vf-' + vexNote.getAttribute('id'); // where does 'vf' come from?\r\n }\r\n const noteData: VexNoteModifierIf = {\r\n smoMeasure: this.smoMeasure,\r\n vxMeasure: this,\r\n smoNote: smoNote,\r\n staveNote: vexNote,\r\n voiceIndex: voiceIx,\r\n tickIndex: tickIndex\r\n }\r\n if (tabNote) {\r\n noteData.tabNote = tabNote;\r\n this.tabNotes.push(tabNote);\r\n }\r\n const modObj = new VxNote(noteData);\r\n modObj.addModifiers();\r\n layoutDebug.setTimestamp(layoutDebug.codeRegions.PREFORMATC, new Date().valueOf() - timestamp);\r\n\r\n return modObj;\r\n }\r\n\r\n renderNoteGlyph(smoNote: SmoNote, textObj: SmoDynamicText) {\r\n var x = this.noteToVexMap[smoNote.attrs.id].getAbsoluteX() + textObj.xOffset;\r\n // the -3 is copied from vexflow textDynamics\r\n var y = this.stave!.getYForLine(textObj.yOffsetLine - 3) + textObj.yOffsetPixels;\r\n let maxh = 0;\r\n const minx = x;\r\n var group = this.context.getContext().openGroup();\r\n group.classList.add(textObj.attrs.id + '-' + smoNote.attrs.id);\r\n group.classList.add(textObj.attrs.id);\r\n // const duration = SmoMusic.closestVexDuration(smoNote.tickCount);\r\n for (var i = 0; i < textObj.text.length; i += 1 ) {\r\n const { width , height } = renderDynamics(this.context.getContext(), VF.TextDynamics.GLYPHS[textObj.text[i]].code,\r\n textObj.fontSize, x, y);\r\n /* const { width , height } = renderDynamics(this.context.getContext(), VF.TextDynamics.GLYPHS[textObj.text[i]],\r\n textObj.fontSize, x, y); */\r\n x += width;\r\n maxh = Math.max(height, maxh); \r\n }\r\n textObj.logicalBox = SvgHelpers.boxPoints(minx, y + this.context.box.y, x - minx, maxh);\r\n textObj.element = group;\r\n this.modifiersToBox.push(textObj);\r\n this.context.getContext().closeGroup();\r\n }\r\n\r\n renderDynamics() {\r\n this.smoMeasure.voices.forEach((voice) => {\r\n voice.notes.forEach((smoNote) => {\r\n const mods = smoNote.textModifiers.filter((mod) =>\r\n mod.attrs.type === 'SmoDynamicText'\r\n );\r\n mods.forEach((btm) => {\r\n const tm = btm as SmoDynamicText;\r\n this.renderNoteGlyph(smoNote, tm);\r\n });\r\n });\r\n });\r\n }\r\n createRepeatSymbol() {\r\n this.voiceNotes = [];\r\n // const vexNote = new VF.GlyphNote('\\uE500', { duration: 'w' }, { line: 2 });\r\n const vexNote = getRepeatBar();\r\n vexNote.setCenterAlignment(true);\r\n this.vexNotes.push(vexNote);\r\n this.voiceNotes.push(vexNote);\r\n }\r\n /**\r\n * create an a array of VF.StaveNote objects to render the active voice.\r\n * @param voiceIx \r\n */\r\n createVexNotes(voiceIx: number) {\r\n let i = 0;\r\n this.voiceNotes = [];\r\n const voice = this.smoMeasure.voices[voiceIx];\r\n let clefNoteAdded = false;\r\n \r\n for (i = 0;\r\n i < voice.notes.length; ++i) {\r\n const smoNote = voice.notes[i];\r\n const textNotes = smoNote.getTextOrnaments();\r\n const vexNote = this.createVexNote(smoNote, i, voiceIx);\r\n this.noteToVexMap[smoNote.attrs.id] = vexNote.noteData.staveNote;\r\n this.vexNotes.push(vexNote.noteData.staveNote);\r\n\r\n if (vexNote.noteData.smoNote.clefNote && !clefNoteAdded) {\r\n const cf = new VF.ClefNote(vexNote.noteData.smoNote.clefNote.clef, 'small');\r\n this.voiceNotes.push(cf);\r\n clefNoteAdded = true; // ignore 2nd in a measure\r\n }\r\n this.voiceNotes.push(vexNote.noteData.staveNote);\r\n textNotes.forEach((tn) => {\r\n this.voiceNotes.push(createTextNote(SmoOrnament.textNoteOrnaments[tn.ornament]));\r\n });\r\n if (isNaN(smoNote.ticks.numerator) || isNaN(smoNote.ticks.denominator)\r\n || isNaN(smoNote.ticks.remainder)) {\r\n throw ('vxMeasure: NaN in ticks');\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Group the notes for beaming and create Vex beam objects\r\n * @param vix - voice index\r\n * @returns \r\n */\r\n createVexBeamGroups(vix: number) {\r\n let keyNoteIx = -1;\r\n let i = 0;\r\n let j = 0;\r\n let stemDirection = VF.Stem.DOWN;\r\n for (i = 0; i < this.smoMeasure.beamGroups.length; ++i) {\r\n const bg = this.smoMeasure.beamGroups[i];\r\n if (bg.voice !== vix) {\r\n continue;\r\n }\r\n const vexNotes: StemmableNote[] = [];\r\n keyNoteIx = bg.notes.findIndex((nn) => nn.noteType === 'n');\r\n\r\n // Fix stem bug: key off first non-rest note.\r\n keyNoteIx = (keyNoteIx >= 0) ? keyNoteIx : 0;\r\n for (j = 0; j < bg.notes.length; ++j) {\r\n const note = bg.notes[j];\r\n if (note.noteType === '/') {\r\n continue;\r\n }\r\n const vexNote = this.noteToVexMap[note.attrs.id];\r\n // some type of redraw condition?\r\n if (!(vexNote instanceof VF.StaveNote || vexNote instanceof VF.GraceNote)) {\r\n return;\r\n }\r\n if (note.tickCount >= 4096 || vexNote.getIntrinsicTicks() >= 4096) {\r\n console.warn('bad length in beam group');\r\n return;\r\n }\r\n if (keyNoteIx === j) {\r\n stemDirection = note.flagState === SmoNote.flagStates.auto ?\r\n vexNote.getStemDirection() : toVexStemDirection(note);\r\n }\r\n vexNote.setStemDirection(stemDirection);\r\n vexNotes.push(vexNote); \r\n }\r\n const vexBeam = new VF.Beam(vexNotes);\r\n this.beamToVexMap[bg.attrs.id] = vexBeam;\r\n this.vexBeamGroups.push(vexBeam);\r\n }\r\n }\r\n\r\n /**\r\n * Create the VF tuplet objects based on the smo tuplet objects\r\n * @param vix \r\n */\r\n // \r\n createVexTuplets(vix: number) {\r\n var j = 0;\r\n var i = 0;\r\n this.vexTuplets = [];\r\n this.tupletToVexMap = {};\r\n for (i = 0; i < this.smoMeasure.tuplets.length; ++i) {\r\n const tp = this.smoMeasure.tuplets[i];\r\n if (tp.voice !== vix) {\r\n continue;\r\n }\r\n const vexNotes: Note[] = [];\r\n for (j = 0; j < tp.notes.length; ++j) {\r\n const smoNote = tp.notes[j];\r\n vexNotes.push(this.noteToVexMap[smoNote.attrs.id]);\r\n }\r\n const location = tp.getStemDirection(this.smoMeasure.clef) === SmoNote.flagStates.up ?\r\n VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM;\r\n const smoTupletParams = {\r\n vexNotes,\r\n numNotes: tp.numNotes,\r\n notesOccupied: tp.note_ticks_occupied,\r\n location\r\n }\r\n const vexTuplet = getVexTuplets(smoTupletParams);\r\n this.tupletToVexMap[tp.id] = vexTuplet;\r\n this.vexTuplets.push(vexTuplet);\r\n }\r\n }\r\n\r\n /**\r\n * create the modifiers for the stave itself, bar lines etc.\r\n */\r\n createMeasureModifiers() {\r\n const sb = this.smoMeasure.getStartBarline();\r\n const eb = this.smoMeasure.getEndBarline();\r\n const sym = this.smoMeasure.getRepeatSymbol();\r\n if (!this.stave) {\r\n return;\r\n }\r\n\r\n // don't create a begin bar for any but the 1st measure.\r\n if (this.smoMeasure.measureNumber.systemIndex !== 0 && sb.barline === SmoBarline.barlines.singleBar\r\n && this.smoMeasure.format.padLeft === 0) {\r\n this.stave.setBegBarType(VF.Barline.type.NONE);\r\n } else {\r\n this.stave.setBegBarType(toVexBarlineType(sb));\r\n }\r\n if (this.smoMeasure.svg.multimeasureLength > 0 && !this.smoMeasure.svg.hideMultimeasure) {\r\n this.stave.setEndBarType(vexBarlineType[this.smoMeasure.svg.multimeasureEndBarline]);\r\n } else if (eb.barline !== SmoBarline.barlines.singleBar) {\r\n this.stave.setEndBarType(toVexBarlineType(eb));\r\n }\r\n if (sym && sym.symbol !== SmoRepeatSymbol.symbols.None) {\r\n const rep = new VF.Repetition(toVexSymbol(sym), sym.xOffset + this.smoMeasure.staffX, sym.yOffset);\r\n this.stave.getModifiers().push(rep);\r\n }\r\n const tms = this.smoMeasure.getMeasureText();\r\n // TODO: set font\r\n tms.forEach((tmb: SmoMeasureModifierBase) => {\r\n const tm = tmb as SmoMeasureText;\r\n const offset = tm.position === SmoMeasureText.positions.left ? this.smoMeasure.format.padLeft : 0;\r\n const staveText = createStaveText(tm.text, toVexTextPosition(tm), \r\n {\r\n shiftX: tm.adjustX + offset, shiftY: tm.adjustY, justification: toVexTextJustification(tm)\r\n }\r\n );\r\n this.stave?.addModifier(staveText);\r\n\r\n // hack - we can't create staveText directly so this is the only way I could set the font\r\n const ar = this.stave!.getModifiers();\r\n const vm = ar[ar.length - 1];\r\n vm.setFont(tm.fontInfo);\r\n });\r\n if (this.smoMeasure.svg.rowInSystem === 0) {\r\n const rmb = this.smoMeasure.getRehearsalMark();\r\n const rm = rmb as SmoRehearsalMark;\r\n if (rm) {\r\n this.stave.setSection(rm.symbol, 0);\r\n }\r\n }\r\n\r\n const tempo = this.smoMeasure.getTempo();\r\n if (tempo && this.smoMeasure.svg.forceTempo) {\r\n this.stave.setTempo(tempo.toVexTempo(), -1 * tempo.yOffset);\r\n const vexTempo = this.stave.getModifiers().find((mod: StaveModifier) => mod.getAttribute('type') === 'StaveTempo');\r\n if (vexTempo) {\r\n vexTempo.setFont({ family: SourceSerifProFont.fontFamily, size: 13, weight: 'bold' });\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Create all Vex notes and modifiers. We defer the format and rendering so\r\n * we can align across multiple staves\r\n */\r\n preFormat() {\r\n var j = 0;\r\n if (this.smoMeasure.svg.element !== null) {\r\n this.smoMeasure.svg.element.remove();\r\n this.smoMeasure.svg.element = null;\r\n if (this.smoMeasure.svg.tabElement) {\r\n this.smoMeasure.svg.tabElement.remove();\r\n this.smoMeasure.svg.tabElement = undefined;\r\n }\r\n }\r\n if (this.smoMeasure.svg.hideEmptyMeasure) {\r\n return;\r\n }\r\n // Note: need to do this to get it into VEX KS format\r\n const staffX = this.smoMeasure.staffX + this.smoMeasure.format.padLeft;\r\n const staffY = this.smoMeasure.staffY - this.context.box.y;\r\n const key = SmoMusic.vexKeySignatureTranspose(this.smoMeasure.keySignature, 0);\r\n const canceledKey = SmoMusic.vexKeySignatureTranspose(this.smoMeasure.canceledKeySignature, 0);\r\n const smoVexStaveParams = {\r\n x: staffX,\r\n y: staffY,\r\n padLeft: this.smoMeasure.format.padLeft,\r\n id: this.smoMeasure.id,\r\n staffX: this.smoMeasure.staffX,\r\n staffY: this.smoMeasure.staffY,\r\n staffWidth: this.smoMeasure.staffWidth,\r\n forceClef: this.smoMeasure.svg.forceClef,\r\n clef: this.smoMeasure.clef,\r\n forceKey: this.smoMeasure.svg.forceKeySignature,\r\n key,\r\n canceledKey,\r\n startX: this.smoMeasure.svg.maxColumnStartX,\r\n adjX: this.smoMeasure.svg.adjX,\r\n context: this.context.getContext()\r\n }\r\n this.stave = createStave(smoVexStaveParams);\r\n if (this.smoMeasure.svg.forceTimeSignature) {\r\n const ts = this.smoMeasure.timeSignature;\r\n let tsString = ts.timeSignature;\r\n if (this.smoMeasure.timeSignature.useSymbol && ts.actualBeats === 4 && ts.beatDuration === 4) {\r\n tsString = 'C';\r\n } else if (this.smoMeasure.timeSignature.useSymbol && ts.actualBeats === 2 && ts.beatDuration === 4) {\r\n tsString = 'C|';\r\n } else if (this.smoMeasure.timeSignature.displayString.length) {\r\n tsString = this.smoMeasure.timeSignature.displayString;\r\n }\r\n this.stave.addTimeSignature(tsString);\r\n }\r\n // Connect it to the rendering context and draw!\r\n this.stave.setContext(this.context.getContext());\r\n if (this.smoTabStave && this.smoMeasure.svg.tabStaveBox?.width) {\r\n const box = this.smoMeasure.svg.tabStaveBox;\r\n let tabWidth = 0;\r\n box.y -= this.context.box.y;\r\n box.x = staffX - this.context.box.x;\r\n box.width = this.smoMeasure.staffWidth;\r\n this.tabStave = createTabStave(box, this.smoTabStave.spacing, this.smoTabStave.numLines);\r\n if (this.smoMeasure.svg.forceClef) {\r\n this.tabStave.addTabGlyph();\r\n tabWidth = vexGlyph.dimensions['tab'].width;\r\n }\r\n this.tabStave.setNoteStartX(this.tabStave.getNoteStartX() + this.smoMeasure.svg.adjX - tabWidth);\r\n this.tabStave.setContext(this.context.getContext());\r\n }\r\n\r\n this.createMeasureModifiers();\r\n\r\n this.tickmapObject = this.smoMeasure.createMeasureTickmaps();\r\n this.createCollisionTickmap();\r\n\r\n this.voiceAr = [];\r\n this.vexNotes = [];\r\n this.noteToVexMap = {};\r\n\r\n // If there are multiple voices, add them all to the formatter at the same time so they don't collide\r\n for (j = 0; j < this.smoMeasure.voices.length; ++j) {\r\n const smoVexVoiceParams = {\r\n actualBeats: this.smoMeasure.timeSignature.actualBeats,\r\n beatDuration: this.smoMeasure.timeSignature.beatDuration,\r\n notes: this.vexNotes\r\n }\r\n if (!this.smoMeasure.svg.multimeasureLength && !this.smoMeasure.repeatSymbol) {\r\n this.createVexNotes(j);\r\n smoVexVoiceParams.notes = this.voiceNotes;\r\n this.createVexTuplets(j);\r\n this.createVexBeamGroups(j);\r\n\r\n // Create a voice in 4/4 and add above notes\r\n const voice = createVoice(smoVexVoiceParams);\r\n this.voiceAr.push(voice);\r\n }\r\n if (this.smoMeasure.repeatSymbol) {\r\n this.createRepeatSymbol();\r\n // Create a voice in 4/4 and add above notes\r\n const voice = createVoice(smoVexVoiceParams);\r\n this.voiceAr.push(voice);\r\n }\r\n }\r\n // Need to format for x position, then set y position before drawing dynamics.\r\n this.formatter = new VF.Formatter({ softmaxFactor: this.softmax, globalSoftmax: false });\r\n this.formatter.joinVoices(this.voiceAr);\r\n if (this.tabStave) {\r\n this.tabVoice = createVoice({\r\n actualBeats: this.smoMeasure.timeSignature.actualBeats,\r\n beatDuration: this.smoMeasure.timeSignature.beatDuration,\r\n notes: this.tabNotes\r\n });\r\n this.formatter.joinVoices([this.tabVoice]);\r\n }\r\n }\r\n /**\r\n * Create the Vex formatter that calculates the X and Y positions of the notes. A formatter\r\n * may actually span multiple staves for justified staves. The notes are drawn in their\r\n * individual vxMeasure objects but formatting is done once for all justified staves\r\n * @param voices Voice objects from VexFlow\r\n * @returns \r\n */\r\n format(voices: Voice[]) {\r\n if (this.smoMeasure.svg.hideEmptyMeasure) {\r\n return;\r\n }\r\n\r\n if (this.smoMeasure.svg.multimeasureLength > 0) {\r\n this.multimeasureRest = getMultimeasureRest(this.smoMeasure.svg.multimeasureLength);\r\n this.multimeasureRest.setContext(this.context.getContext());\r\n this.multimeasureRest.setStave(this.stave);\r\n return;\r\n }\r\n if (!this.formatter) {\r\n return;\r\n }\r\n const timestamp = new Date().valueOf();\r\n const staffWidth = this.smoMeasure.staffWidth -\r\n (this.smoMeasure.svg.maxColumnStartX + this.smoMeasure.svg.adjRight + this.smoMeasure.format.padLeft) - 10;\r\n this.dbgLeftX = this.smoMeasure.staffX + this.smoMeasure.format.padLeft + this.smoMeasure.svg.adjX;\r\n this.dbgWidth = staffWidth;\r\n this.formatter.format(voices, staffWidth);\r\n if (this.tabVoice && this.tabNotes.length) {\r\n this.formatter.format([this.tabVoice], staffWidth);\r\n }\r\n layoutDebug.setTimestamp(layoutDebug.codeRegions.FORMAT, new Date().valueOf() - timestamp);\r\n }\r\n /**\r\n * render is called after format. Actually draw the things.\r\n */\r\n render() {\r\n if (this.smoMeasure.svg.hideEmptyMeasure) {\r\n return;\r\n }\r\n\r\n var group = this.context.getContext().openGroup() as SVGSVGElement;\r\n var mmClass = this.smoMeasure.getClassId();\r\n var j = 0;\r\n try {\r\n // bound each measure in its own SVG group for easy deletion and mapping to screen coordinate\r\n group.classList.add(this.smoMeasure.id);\r\n group.classList.add(mmClass);\r\n group.id = this.smoMeasure.id;\r\n this.stave!.draw();\r\n this.smoMeasure.svg.element = group;\r\n\r\n for (j = 0; j < this.voiceAr.length; ++j) {\r\n this.voiceAr[j].draw(this.context.getContext(), this.stave!);\r\n }\r\n\r\n this.vexBeamGroups.forEach((b) => {\r\n b.setContext(this.context.getContext()).draw();\r\n });\r\n\r\n this.vexTuplets.forEach((tuplet) => {\r\n tuplet.setContext(this.context.getContext()).draw();\r\n });\r\n if (this.multimeasureRest) {\r\n this.multimeasureRest.draw();\r\n }\r\n // this._updateLyricDomSelectors();\r\n this.renderDynamics();\r\n // this.smoMeasure.adjX = this.stave.start_x - (this.smoMeasure.staffX);\r\n\r\n this.context.getContext().closeGroup();\r\n if (this.tabStave) {\r\n const tabStaveId = `${this.smoMeasure.id}-tab`;\r\n const tabGroup = this.context.getContext().openGroup() as SVGSVGElement;\r\n tabGroup.classList.add(tabStaveId);\r\n this.tabStave.draw();\r\n this.tabVoice?.draw(this.context.getContext(), this.tabStave);\r\n this.context.getContext().closeGroup();\r\n this.smoMeasure.svg.tabElement = tabGroup;\r\n }\r\n // layoutDebug.setTimestamp(layoutDebug.codeRegions.RENDER, new Date().valueOf() - timestamp);\r\n\r\n this.rendered = true;\r\n if (layoutDebug.mask & layoutDebug.values['adjust']) {\r\n SvgHelpers.debugBoxNoText(this.context.getContext().svg,\r\n SvgHelpers.boxPoints(this.dbgLeftX, \r\n this.smoMeasure.svg.staffY, this.dbgWidth, 40), 'render-x-dbg', 0);\r\n }\r\n } catch (exc) {\r\n console.warn('unable to render measure ' + this.smoMeasure.measureNumber.measureIndex);\r\n this.context.getContext().closeGroup();\r\n }\r\n }\r\n}\r\n","\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { SmoMusic } from '../../smo/data/music';\r\nimport { layoutDebug } from '../sui/layoutDebug';\r\nimport { SmoRepeatSymbol, SmoMeasureText, SmoBarline, SmoMeasureModifierBase, SmoRehearsalMark, SmoMeasureFormat } from '../../smo/data/measureModifiers';\r\nimport { SourceSerifProFont } from '../../styles/font_metrics/ssp-serif-metrics';\r\nimport { SmoOrnament, SmoArticulation, SmoDynamicText, SmoLyric, \r\n SmoArpeggio, SmoNoteModifierBase, VexAnnotationParams, SmoTabNote } from '../../smo/data/noteModifiers';\r\nimport { SmoSelection } from '../../smo/xform/selections';\r\nimport { SmoMeasure, MeasureTickmaps } from '../../smo/data/measure';\r\nimport { SvgHelpers } from '../sui/svgHelpers';\r\nimport { Clef, IsClef } from '../../smo/data/common';\r\nimport { SvgPage } from '../sui/svgPageMap';\r\nimport { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, toVexSymbol,\r\n toVexTextJustification, toVexTextPosition, getVexChordBlocks, toVexStemDirection } from './smoAdapter';\r\nimport { VexFlow, Stave,StemmableNote, Note, Beam, Tuplet, Voice,\r\n Formatter, Accidental, Annotation, StaveNoteStruct, StaveText, StaveModifier, \r\n createStaveText, renderDynamics, applyStemDirection,\r\n getVexNoteParameters, defaultNoteScale, defaultCueScale, getVexTuplets,\r\n createStave, createVoice, getOrnamentGlyph, getSlashGlyph, getRepeatBar, getMultimeasureRest,\r\n addChordGlyph, StaveNote,\r\n TabNote} from '../../common/vex';\r\n\r\nconst VF = VexFlow;\r\n\r\nexport interface VxMeasureIf {\r\n isWholeRest(): boolean;\r\n noteToVexMap: Record;\r\n smoMeasure: SmoMeasure;\r\n tickmapObject: MeasureTickmaps | null\r\n}\r\n\r\nexport interface VexNoteModifierIf {\r\n smoMeasure: SmoMeasure,\r\n vxMeasure: VxMeasureIf,\r\n smoNote: SmoNote,\r\n staveNote: Note,\r\n voiceIndex: number,\r\n tickIndex: number,\r\n tabNote?: StemmableNote | TabNote\r\n}\r\n\r\nexport class VxNote {\r\n noteData: VexNoteModifierIf;\r\n constructor(noteData: VexNoteModifierIf) {\r\n this.noteData = noteData;\r\n }\r\n createMicrotones(smoNote: SmoNote, vexNote: Note) {\r\n const tones = smoNote.getMicrotones();\r\n tones.forEach((tone) => {\r\n const acc: Accidental = new VF.Accidental(tone.toVex);\r\n vexNote.addModifier(acc, tone.pitchIndex);\r\n });\r\n }\r\n createDots() {\r\n for (var i = 0; i < this.noteData.smoNote.dots; ++i) {\r\n for (var j = 0; j < this.noteData.smoNote.pitches.length; ++j) {\r\n if (!this.noteData.vxMeasure.isWholeRest()) {\r\n this.noteData.staveNote.addModifier(new VF.Dot(), j);\r\n if (this.noteData.tabNote) {\r\n const tabDot = new VF.Dot();\r\n if (this.noteData.tabNote.getCategory() === VF.TabNote.CATEGORY && j === 0) {\r\n tabDot.setDotShiftY(this.noteData.tabNote.glyphProps.dot_shiftY);\r\n }\r\n this.noteData.tabNote.addModifier(tabDot, 0);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n /**\r\n * Create accidentals based on the active key and previous accidentals in this voice\r\n * @param smoNote \r\n * @param vexNote \r\n * @param tickIndex \r\n * @param voiceIx \r\n * @returns \r\n */\r\n createAccidentals() {\r\n let i = 0;\r\n if (this.noteData.smoNote.noteType === '/') {\r\n return;\r\n }\r\n if (this.noteData.smoNote.noteType !== 'n') {\r\n this.createDots();\r\n return;\r\n }\r\n this.noteData.smoNote.accidentalsRendered = [];\r\n for (i = 0; i < this.noteData.smoNote.pitches.length && this.noteData.vxMeasure.tickmapObject !== null; ++i) {\r\n const pitch = this.noteData.smoNote.pitches[i];\r\n const zz = SmoMusic.accidentalDisplay(pitch, this.noteData.smoMeasure.keySignature,\r\n this.noteData.vxMeasure.tickmapObject.tickmaps[this.noteData.voiceIndex].durationMap[this.noteData.tickIndex], \r\n this.noteData.vxMeasure.tickmapObject.accidentalArray);\r\n if (zz) {\r\n const acc = new VF.Accidental(zz.symbol);\r\n if (zz.courtesy) {\r\n acc.setAsCautionary();\r\n }\r\n this.noteData.smoNote.accidentalsRendered.push(pitch.accidental);\r\n this.noteData.staveNote.addModifier(acc, i);\r\n } else {\r\n this.noteData.smoNote.accidentalsRendered.push('');\r\n }\r\n }\r\n this.createDots();\r\n this.createMicrotones(this.noteData.smoNote, this.noteData.staveNote);\r\n if (this.noteData.smoNote.arpeggio) {\r\n this.noteData.staveNote.addModifier(new VF.Stroke(this.noteData.smoNote.arpeggio.typeCode));\r\n }\r\n }\r\n createJazzOrnaments() {\r\n const smoNote = this.noteData.smoNote;\r\n const vexNote = this.noteData.staveNote;\r\n const o = smoNote.getJazzOrnaments();\r\n o.forEach((ll) => {\r\n const mod = new VF.Ornament(ll.toVex());\r\n vexNote.addModifier(mod, 0);\r\n });\r\n }\r\n\r\n createOrnaments() {\r\n const o = this.noteData.smoNote.getOrnaments();\r\n o.forEach((ll) => {\r\n if (!SmoOrnament.textNoteOrnaments[ll.ornament]) {\r\n const ornamentCode = getOrnamentGlyph(ll.ornament);\r\n const mod = new VF.Ornament(ornamentCode);\r\n if (ll.offset === SmoOrnament.offsets.after) {\r\n mod.setDelayed(true);\r\n }\r\n this.noteData.staveNote.addModifier(mod, 0);\r\n }\r\n });\r\n }\r\n addLyricAnnotationToNote(vexNote: Note, lyric: SmoLyric) {\r\n let classString = 'lyric lyric-' + lyric.verse;\r\n let text = lyric.getText();\r\n if (lyric.skipRender) {\r\n return;\r\n }\r\n if (!text.length && lyric.isHyphenated()) {\r\n text = '-';\r\n }\r\n // no text, no hyphen, don't add it.\r\n if (!text.length) {\r\n return;\r\n }\r\n const vexL: Annotation = new VF.Annotation(text); // .setReportWidth(lyric.adjustNoteWidth);\r\n vexL.setAttribute('id', lyric.attrs.id); //\r\n\r\n // If we adjusted this note for the lyric, adjust the lyric as well.\r\n vexL.setFont(lyric.fontInfo.family, lyric.fontInfo.size, lyric.fontInfo.weight);\r\n vexL.setVerticalJustification(VF.Annotation.VerticalJustify.BOTTOM);\r\n vexNote.addModifier(vexL);\r\n if (lyric.isHyphenated()) {\r\n classString += ' lyric-hyphen';\r\n }\r\n vexL.addClass(classString);\r\n }\r\n addChordChangeToNote(vexNote: Note, lyric: SmoLyric) {\r\n const cs = new VF.ChordSymbol();\r\n cs.setAttribute('id', lyric.attrs.id);\r\n const blocks = getVexChordBlocks(lyric);\r\n blocks.forEach((block) => {\r\n if (block.glyph) {\r\n // Vex 5 broke this, does not distinguish between glyph and text\r\n // the reverse is for vex4 which expects the non-mangled identifier here,\r\n // e.g. 'diminished' and not 'csymDiminished'\r\n addChordGlyph(cs, block.glyph);\r\n } else {\r\n cs.addGlyphOrText(block.text ?? '', block);\r\n }\r\n });\r\n cs.setFont(lyric.fontInfo.family, lyric.fontInfo.size).setReportWidth(lyric.adjustNoteWidth);\r\n vexNote.addModifier(cs, 0);\r\n const classString = 'chord chord-' + lyric.verse;\r\n cs.addClass(classString);\r\n }\r\n createLyric() {\r\n const lyrics = this.noteData.smoNote.getTrueLyrics();\r\n if (this.noteData.smoNote.noteType !== '/') {\r\n lyrics.forEach((bll) => {\r\n const ll = bll as SmoLyric;\r\n this.addLyricAnnotationToNote(this.noteData.staveNote, ll);\r\n });\r\n }\r\n const chords = this.noteData.smoNote.getChords();\r\n chords.forEach((chord) => {\r\n this.addChordChangeToNote(this.noteData.staveNote, chord);\r\n });\r\n }\r\n createGraceNotes() {\r\n const smoNote = this.noteData.smoNote;\r\n const vexNote = this.noteData.staveNote;\r\n let i = 0;\r\n const gar = smoNote.getGraceNotes();\r\n var toBeam = true;\r\n if (gar && gar.length) {\r\n const group: any[] = [];\r\n gar.forEach((g) => {\r\n const gr = new VF.GraceNote(g.toVexGraceNote());\r\n gr.setAttribute('id', g.attrs.id);\r\n for (i = 0; i < g.pitches.length; ++i) {\r\n const pitch = g.pitches[i];\r\n if (!pitch.accidental) {\r\n console.warn('no accidental in grace note');\r\n }\r\n if (pitch.accidental && pitch.accidental !== 'n' || pitch.cautionary) {\r\n const accidental = new VF.Accidental(pitch.accidental);\r\n if (pitch.cautionary) {\r\n accidental.setAsCautionary();\r\n }\r\n gr.addModifier(accidental, i);\r\n }\r\n }\r\n if (g.tickCount() >= 4096) {\r\n toBeam = false;\r\n }\r\n gr.addClass('grace-note'); // note: this doesn't work :(\r\n\r\n g.renderId = gr.getAttribute('id');\r\n group.push(gr);\r\n });\r\n const grace: any = new VF.GraceNoteGroup(group);\r\n if (toBeam) {\r\n grace.beamNotes();\r\n }\r\n vexNote.addModifier(grace, 0);\r\n }\r\n }\r\n addArticulations() {\r\n const smoNote = this.noteData.smoNote;\r\n smoNote.articulations.forEach((art) => {\r\n if (smoNote.noteType === 'n') {\r\n const vx = this.noteData.staveNote;\r\n const position: number = SmoArticulation.positionToVex[art.position];\r\n const vexArt = SmoArticulation.articulationToVex[art.articulation];\r\n const vxArt = new VF.Articulation(vexArt).setPosition(position);\r\n vx.addModifier(vxArt, this.noteData.voiceIndex);\r\n }\r\n });\r\n }\r\n\r\n addModifiers() {\r\n this.createAccidentals();\r\n this.createLyric();\r\n this.createOrnaments();\r\n this.createJazzOrnaments();\r\n this.createGraceNotes();\r\n this.addArticulations();\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\nimport { VxMeasure } from './vxMeasure';\r\nimport { SmoSelection } from '../../smo/xform/selections';\r\nimport { SvgHelpers } from '../sui/svgHelpers';\r\nimport { SmoLyric } from '../../smo/data/noteModifiers';\r\nimport { SmoStaffHairpin, SmoSlur, StaffModifierBase, SmoTie, SmoStaffTextBracket } from '../../smo/data/staffModifiers';\r\nimport { SmoScore } from '../../smo/data/score';\r\nimport { leftConnectorVx, rightConnectorVx } from './smoAdapter';\r\nimport { SmoMeasure, SmoVoice } from '../../smo/data/measure';\r\nimport { SvgBox } from '../../smo/data/common';\r\nimport { SmoNote } from '../../smo/data/note';\r\nimport { SmoSystemStaff } from '../../smo/data/systemStaff';\r\nimport { SmoVolta } from '../../smo/data/measureModifiers';\r\nimport { SmoMeasureFormat } from '../../smo/data/measureModifiers';\r\nimport { SmoScoreText } from '../../smo/data/scoreText'\r\nimport { SvgPage } from '../sui/svgPageMap';\r\nimport { SuiScroller } from '../sui/scroller';\r\nimport { VexFlow, Voice, Note, createHairpin, createSlur, createTie } from '../../common/vex';\r\nimport { toVexVolta, vexOptions } from './smoAdapter';\r\nconst VF = VexFlow;\r\n\r\nexport interface VoltaInfo {\r\n smoMeasure: SmoMeasure,\r\n ending: SmoVolta\r\n}\r\nexport interface SuiSystemGroup {\r\n firstMeasure: VxMeasure,\r\n voices: Voice[]\r\n}\r\n/**\r\n * Create a system of staves and draw music on it. This calls the Vex measure\r\n * rendering methods, and also draws all the score and system level stuff like slurs, \r\n * text, aligns the lyrics.\r\n * */\r\nexport class VxSystem {\r\n context: SvgPage;\r\n leftConnector: any[] = [null, null];\r\n score: SmoScore;\r\n vxMeasures: VxMeasure[] = [];\r\n smoMeasures: SmoMeasure[] = [];\r\n lineIndex: number;\r\n maxStaffIndex: number;\r\n maxSystemIndex: number;\r\n minMeasureIndex: number = -1;\r\n maxMeasureIndex: number = 0;\r\n width: number;\r\n staves: SmoSystemStaff[] = [];\r\n box: SvgBox = SvgBox.default;\r\n currentY: number;\r\n topY: number;\r\n clefWidth: number;\r\n ys: number[] = [];\r\n measures: VxMeasure[] = [];\r\n modifiers: any[] = [];\r\n constructor(context: SvgPage, topY: number, lineIndex: number, score: SmoScore) {\r\n this.context = context;\r\n this.lineIndex = lineIndex;\r\n this.score = score;\r\n this.maxStaffIndex = -1;\r\n this.maxSystemIndex = -1;\r\n this.width = -1;\r\n this.staves = [];\r\n this.currentY = 0;\r\n this.topY = topY;\r\n this.clefWidth = 70;\r\n this.ys = [];\r\n }\r\n\r\n getVxMeasure(smoMeasure: SmoMeasure) {\r\n let i = 0;\r\n for (i = 0; i < this.vxMeasures.length; ++i) {\r\n const vm = this.vxMeasures[i];\r\n if (vm.smoMeasure.id === smoMeasure.id) {\r\n return vm;\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n\r\n getVxNote(smoNote: SmoNote): Note | null {\r\n let i = 0;\r\n if (!smoNote) {\r\n return null;\r\n }\r\n for (i = 0; i < this.measures.length; ++i) {\r\n const mm = this.measures[i];\r\n if (mm.noteToVexMap[smoNote.attrs.id]) {\r\n return mm.noteToVexMap[smoNote.attrs.id];\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n _updateChordOffsets(note: SmoNote) {\r\n var i = 0;\r\n for (i = 0; i < 3; ++i) {\r\n const chords = note.getLyricForVerse(i, SmoLyric.parsers.chord);\r\n chords.forEach((bchord) => {\r\n const chord = bchord as SmoLyric;\r\n const dom = this.context.svg.getElementById('vf-' + chord.attrs.id);\r\n if (dom) {\r\n dom.setAttributeNS('', 'transform', 'translate(' + chord.translateX + ' ' + (-1 * chord.translateY) + ')');\r\n }\r\n });\r\n }\r\n }\r\n _lowestYLowestVerse(lyrics: SmoLyric[], vxMeasures: VxMeasure[]) {\r\n // Move each verse down, according to the lowest lyric on that line/verse,\r\n // and the accumulation of the verses above it\r\n let lowestY = 0;\r\n for (var lowVerse = 0; lowVerse < 4; ++lowVerse) {\r\n let maxVerseHeight = 0;\r\n const verseLyrics = lyrics.filter((ll) => ll.verse === lowVerse);\r\n if (lowVerse === 0) {\r\n // first verse, go through list twice. first find lowest points\r\n verseLyrics.forEach((lyric: SmoLyric) => {\r\n if (lyric.logicalBox) {\r\n // 'lowest' Y on screen is Y with largest value...\r\n const ly = lyric.logicalBox.y - this.context.box.y;\r\n lowestY = Math.max(ly + lyric.musicYOffset, lowestY);\r\n }\r\n });\r\n // second offset all to that point\r\n verseLyrics.forEach((lyric: SmoLyric) => {\r\n if (lyric.logicalBox) {\r\n const ly = lyric.logicalBox.y - this.context.box.y;\r\n const offset = Math.max(0, lowestY - ly);\r\n lyric.adjY = offset + lyric.translateY;\r\n }\r\n });\r\n } else {\r\n // subsequent verses, first find the tallest lyric\r\n verseLyrics.forEach((lyric: SmoLyric)=> {\r\n if (lyric.logicalBox) {\r\n maxVerseHeight = Math.max(lyric.logicalBox.height, maxVerseHeight);\r\n }\r\n });\r\n // adjust lowestY to be the verse height below the previous verse\r\n lowestY = lowestY + maxVerseHeight * 1.1; // 1.1 magic number?\r\n // and offset these lyrics\r\n verseLyrics.forEach((lyric: SmoLyric)=> {\r\n if (lyric.logicalBox) {\r\n const ly = lyric.logicalBox.y - this.context.box.y;\r\n const offset = Math.max(0, lowestY - ly);\r\n lyric.adjY = offset + lyric.translateY;\r\n }\r\n });\r\n }\r\n }\r\n }\r\n\r\n // ### updateLyricOffsets\r\n // Adjust the y position for all lyrics in the line so they are even.\r\n // Also replace '-' with a longer dash do indicate 'until the next measure'\r\n updateLyricOffsets() {\r\n let i = 0;\r\n for (i = 0; i < this.score.staves.length; ++i) {\r\n const tmpI = i;\r\n const lyricsDash: SmoLyric[] = [];\r\n const lyricHyphens: SmoLyric[] = [];\r\n const lyricVerseMap: Record = {};\r\n const lyrics: SmoLyric[] = [];\r\n // is this necessary? They should all be from the current line\r\n const vxMeasures = this.vxMeasures.filter((vx) =>\r\n vx.smoMeasure.measureNumber.staffId === tmpI\r\n );\r\n\r\n // All the lyrics on this line\r\n // The vertical bounds on each line\r\n vxMeasures.forEach((mm) => {\r\n var smoMeasure = mm.smoMeasure;\r\n\r\n // Get lyrics from any voice.\r\n smoMeasure.voices.forEach((voice) => {\r\n voice.notes.forEach((note) => {\r\n this._updateChordOffsets(note);\r\n note.getTrueLyrics().forEach((ll: SmoLyric) => {\r\n const hasLyric = ll.getText().length > 0 || ll.isHyphenated();\r\n if (hasLyric && ll.logicalBox && !lyricVerseMap[ll.verse]) {\r\n lyricVerseMap[ll.verse] = [];\r\n }else if (hasLyric && !ll.logicalBox) {\r\n console.warn(\r\n `unrendered lyric for note ${note.attrs.id} measure ${smoMeasure.measureNumber.staffId}-${smoMeasure.measureNumber.measureIndex}`);\r\n }\r\n if (hasLyric && ll.logicalBox) {\r\n lyricVerseMap[ll.verse].push(ll);\r\n lyrics.push(ll);\r\n }\r\n });\r\n });\r\n });\r\n });\r\n // calculate y offset so the lyrics all line up\r\n this._lowestYLowestVerse(lyrics, vxMeasures);\r\n const vkey: string[] = Object.keys(lyricVerseMap).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));\r\n vkey.forEach((sverse) => {\r\n const verse = parseInt(sverse, 10);\r\n let hyphenLyric: SmoLyric | null = null;\r\n const lastVerse = lyricVerseMap[verse][lyricVerseMap[verse].length - 1].attrs.id;\r\n lyricVerseMap[verse].forEach((ll: SmoLyric) => {\r\n if (hyphenLyric !== null && hyphenLyric.logicalBox !== null && ll.logicalBox !== null) {\r\n const x = ll.logicalBox.x - (ll.logicalBox.x -\r\n (hyphenLyric.logicalBox.x + hyphenLyric.logicalBox.width)) / 2;\r\n ll.hyphenX = x;\r\n lyricHyphens.push(ll);\r\n }\r\n if (ll.isHyphenated() && ll.logicalBox !== null) {\r\n if (ll.attrs.id === lastVerse) {\r\n // Last word on the system, place the hyphen after the word\r\n const fontSize = SmoScoreText.fontPointSize(ll.fontInfo.size);\r\n ll.hyphenX = ll.logicalBox.x + ll.logicalBox.width + fontSize / 2;\r\n lyricHyphens.push(ll);\r\n } else if (ll.getText().length) {\r\n // place the hyphen 1/2 between next word and this one.\r\n hyphenLyric = ll;\r\n }\r\n } else {\r\n hyphenLyric = null;\r\n }\r\n });\r\n });\r\n lyrics.forEach((lyric) => {\r\n const dom = this.context.svg.getElementById('vf-' + lyric.attrs.id) as SVGSVGElement;\r\n if (dom) {\r\n dom.setAttributeNS('', 'transform', 'translate(' + lyric.adjX + ' ' + lyric.adjY + ')');\r\n // Keep track of lyrics that are 'dash'\r\n if (lyric.isDash()) {\r\n lyricsDash.push(lyric);\r\n }\r\n }\r\n });\r\n lyricHyphens.forEach((lyric) => {\r\n const parent = this.context.svg.getElementById('vf-' + lyric.attrs.id);\r\n if (parent && lyric.logicalBox !== null) {\r\n const ly = lyric.logicalBox.y - this.context.box.y;\r\n const text = document.createElementNS(SvgHelpers.namespace, 'text');\r\n text.textContent = '-';\r\n const fontSize = SmoScoreText.fontPointSize(lyric.fontInfo.size);\r\n text.setAttributeNS('', 'x', (lyric.hyphenX - fontSize / 3).toString());\r\n text.setAttributeNS('', 'y', (ly + (lyric.logicalBox.height * 2) / 3).toString());\r\n text.setAttributeNS('', 'font-size', '' + fontSize + 'pt');\r\n parent.appendChild(text);\r\n }\r\n });\r\n lyricsDash.forEach((lyric) => {\r\n const parent = this.context.svg.getElementById('vf-' + lyric.attrs.id);\r\n if (parent && lyric.logicalBox !== null) {\r\n const ly = lyric.logicalBox.y - this.context.box.y;\r\n const line = document.createElementNS(SvgHelpers.namespace, 'line');\r\n const ymax = Math.round(ly + lyric.logicalBox.height / 2);\r\n const offset = Math.round(lyric.logicalBox.width / 2);\r\n line.setAttributeNS('', 'x1', (lyric.logicalBox.x - offset).toString());\r\n line.setAttributeNS('', 'y1', ymax.toString());\r\n line.setAttributeNS('', 'x2', (lyric.logicalBox.x + lyric.logicalBox.width + offset).toString());\r\n line.setAttributeNS('', 'y2', ymax.toString());\r\n line.setAttributeNS('', 'stroke-width', '1');\r\n line.setAttributeNS('', 'fill', 'none');\r\n line.setAttributeNS('', 'stroke', '#999999');\r\n parent.appendChild(line);\r\n const texts = parent.getElementsByTagName('text');\r\n // hide hyphen and replace with dash\r\n if (texts && texts.length) {\r\n const text = texts[0];\r\n text.setAttributeNS('', 'fill', '#fff');\r\n }\r\n }\r\n });\r\n }\r\n }\r\n\r\n // ### renderModifier\r\n // render a line-type modifier that is associated with a staff (e.g. slur)\r\n renderModifier(scroller: SuiScroller, modifier: StaffModifierBase,\r\n vxStart: Note | null, vxEnd: Note | null, smoStart: SmoSelection, smoEnd: SmoSelection) {\r\n let xoffset = 0;\r\n const setSameIfNull = (a: any, b: any) => {\r\n if (typeof (a) === 'undefined' || a === null) {\r\n return b;\r\n }\r\n return a;\r\n };\r\n if (smoStart && smoStart.note && smoStart.note.noteType === '/') {\r\n return;\r\n } if (smoEnd && smoEnd.note && smoEnd.note.noteType === '/') {\r\n return;\r\n }\r\n // if it is split between lines, render one artifact for each line, with a common class for\r\n // both if it is removed.\r\n if (vxStart) {\r\n const toRemove = this.context.svg.getElementById('vf-' + modifier.attrs.id);\r\n if (toRemove) {\r\n toRemove.remove();\r\n }\r\n }\r\n const artifactId = modifier.attrs.id + '-' + this.lineIndex;\r\n const group = this.context.getContext().openGroup('slur', artifactId);\r\n group.classList.add(modifier.attrs.id);\r\n const measureMod = 'mod-' + smoStart.selector.staff + '-' + smoStart.selector.measure;\r\n const staffMod = 'mod-' + smoStart.selector.staff;\r\n group.classList.add(measureMod);\r\n group.classList.add(staffMod);\r\n if (modifier.ctor === 'SmoStaffHairpin') {\r\n const hp = modifier as SmoStaffHairpin;\r\n if (!vxStart && !vxEnd) {\r\n this.context.getContext().closeGroup();\r\n }\r\n vxStart = setSameIfNull(vxStart, vxEnd);\r\n vxEnd = setSameIfNull(vxEnd, vxStart);\r\n const smoVexHairpinParams = {\r\n vxStart,\r\n vxEnd,\r\n hairpinType: hp.hairpinType,\r\n height: hp.height,\r\n yOffset: hp.yOffset,\r\n leftShiftPx: hp.xOffsetLeft,\r\n rightShiftPx: hp.xOffsetRight\r\n };\r\n const hairpin = createHairpin(smoVexHairpinParams);\r\n hairpin.setContext(this.context.getContext()).setPosition(hp.position).draw();\r\n } else if (modifier.ctor === 'SmoSlur') {\r\n const startNote: SmoNote = smoStart!.note as SmoNote;\r\n const slur = modifier as SmoSlur;\r\n let slurX = slur.xOffset;\r\n const svgPoint: SVGPoint[] = JSON.parse(JSON.stringify(slur.controlPoints));\r\n const lyric = startNote.longestLyric() as SmoLyric;\r\n if (lyric && lyric.getText()) {\r\n // If there is a lyric, the bounding box of the start note is stretched to the right.\r\n // slide the slur left, and also make it a bit wider.\r\n const xtranslate = (-1 * lyric.getText().length * 6);\r\n xoffset += (xtranslate / 2) - SmoSlur.defaults.xOffset;\r\n }\r\n if (vxStart === null || vxEnd === null) {\r\n slurX = -5;\r\n svgPoint[0].y = 10;\r\n svgPoint[1].y = 10;\r\n }\r\n const smoVexSlurParams = {\r\n vxStart, vxEnd,\r\n thickness: slur.thickness,\r\n xShift: slur.xOffset,\r\n yShift: slur.yOffset,\r\n cps: svgPoint,\r\n invert: slur.invert,\r\n position: slur.position,\r\n positionEnd: slur.position_end\r\n };\r\n const curve = createSlur(smoVexSlurParams);\r\n curve.setContext(this.context.getContext()).draw();\r\n } else if (modifier.ctor === 'SmoTie') {\r\n const ctie = modifier as SmoTie;\r\n const startNote: SmoNote = smoStart!.note as SmoNote;\r\n const endNote: SmoNote = smoEnd!.note as SmoNote;\r\n ctie.checkLines(startNote, endNote);\r\n if (ctie.lines.length > 0) {\r\n const fromLines = ctie.lines.map((ll) => ll.from);\r\n const toLines = ctie.lines.map((ll) => ll.to);\r\n const smoVexTieParams = {\r\n fromLines,\r\n toLines,\r\n firstNote: vxStart,\r\n lastNote: vxEnd,\r\n vexOptions: vexOptions(ctie)\r\n }\r\n const tie = createTie(smoVexTieParams);\r\n tie.setContext(this.context.getContext()).draw();\r\n }\r\n } else if (modifier.ctor === 'SmoStaffTextBracket') {\r\n if (vxStart && !vxEnd) {\r\n vxEnd = vxStart;\r\n } else if (vxEnd && !vxStart) {\r\n vxStart = vxEnd;\r\n }\r\n if (vxStart && vxEnd) {\r\n const smoBracket = (modifier as SmoStaffTextBracket);\r\n const bracket = new VF.TextBracket({\r\n start: vxStart, stop: vxEnd, text: smoBracket.text, superscript: smoBracket.superscript, position: smoBracket.position\r\n });\r\n bracket.setLine(smoBracket.line).setContext(this.context.getContext()).draw();\r\n }\r\n }\r\n\r\n this.context.getContext().closeGroup();\r\n if (xoffset) {\r\n const slurBox = this.context.svg.getElementById('vf-' + artifactId) as SVGSVGElement;\r\n if (slurBox) {\r\n SvgHelpers.translateElement(slurBox, xoffset, 0);\r\n }\r\n }\r\n modifier.element = group;\r\n }\r\n\r\n renderEndings(scroller: SuiScroller) {\r\n let j = 0;\r\n let i = 0;\r\n if (this.staves.length < 1) {\r\n return;\r\n }\r\n const voltas = this.staves[0].getVoltaMap(this.minMeasureIndex, this.maxMeasureIndex);\r\n voltas.forEach((ending) => {\r\n ending.elements.forEach((element: SVGSVGElement) => {\r\n element.remove();\r\n });\r\n ending.elements = [];\r\n });\r\n for (j = 0; j < this.smoMeasures.length; ++j) {\r\n let pushed = false;\r\n const smoMeasure = this.smoMeasures[j];\r\n // Only draw volta on top staff of system\r\n if (smoMeasure.svg.rowInSystem > 0) {\r\n continue;\r\n }\r\n const vxMeasure = this.getVxMeasure(smoMeasure);\r\n const voAr: VoltaInfo[] = [];\r\n for (i = 0; i < voltas.length && vxMeasure !== null; ++i) {\r\n const ending = voltas[i];\r\n const mix = smoMeasure.measureNumber.measureIndex;\r\n if ((ending.startBar <= mix) && (ending.endBar >= mix) && vxMeasure.stave !== null) {\r\n const group = this.context.getContext().openGroup(null, ending.attrs.id);\r\n group.classList.add(ending.attrs.id);\r\n group.classList.add(ending.endingId);\r\n ending.elements.push(group);\r\n const vtype = toVexVolta(ending, smoMeasure.measureNumber.measureIndex);\r\n const vxVolta = new VF.Volta(vtype, ending.number.toString(), smoMeasure.staffX + ending.xOffsetStart, ending.yOffset);\r\n vxVolta.setContext(this.context.getContext()).draw(vxMeasure.stave, -1 * ending.xOffsetEnd);\r\n this.context.getContext().closeGroup();\r\n const height = parseInt(vxVolta.getFontSize(), 10) * 2;\r\n const width = smoMeasure.staffWidth;\r\n const y = smoMeasure.svg.logicalBox.y - (height + ending.yOffset);\r\n ending.logicalBox = { x: smoMeasure.svg.staffX, y, width, height };\r\n if (!pushed) {\r\n voAr.push({ smoMeasure, ending });\r\n pushed = true;\r\n }\r\n vxMeasure.stave.getModifiers().push(vxVolta);\r\n }\r\n }\r\n // Adjust real height of measure to match volta height\r\n for (i = 0; i < voAr.length; ++i) {\r\n const mm = voAr[i].smoMeasure;\r\n const ending = voAr[i].ending;\r\n if (ending.logicalBox !== null) {\r\n const delta = mm.svg.logicalBox.y - ending.logicalBox.y;\r\n if (delta > 0) {\r\n mm.setBox(SvgHelpers.boxPoints(\r\n mm.svg.logicalBox.x, mm.svg.logicalBox.y - delta, mm.svg.logicalBox.width, mm.svg.logicalBox.height + delta),\r\n 'vxSystem adjust for volta');\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n getMeasureByIndex(measureIndex: number, staffId: number) {\r\n let i = 0;\r\n for (i = 0; i < this.smoMeasures.length; ++i) {\r\n const mm = this.smoMeasures[i];\r\n if (measureIndex === mm.measureNumber.measureIndex && staffId === mm.measureNumber.staffId) {\r\n return mm;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n // ## renderMeasure\r\n // ## Description:\r\n // Create the graphical (VX) notes and render them on svg. Also render the tuplets and beam\r\n // groups\r\n renderMeasure(smoMeasure: SmoMeasure, printing: boolean) {\r\n if (smoMeasure.svg.hideMultimeasure) {\r\n return;\r\n }\r\n const measureIndex = smoMeasure.measureNumber.measureIndex;\r\n if (this.minMeasureIndex < 0 || this.minMeasureIndex > measureIndex) {\r\n this.minMeasureIndex = measureIndex;\r\n }\r\n if (this.maxMeasureIndex < measureIndex) {\r\n this.maxMeasureIndex = measureIndex;\r\n }\r\n let brackets = false;\r\n const staff = this.score.staves[smoMeasure.measureNumber.staffId];\r\n const staffId = staff.staffId;\r\n const systemIndex = smoMeasure.measureNumber.systemIndex;\r\n const selection = SmoSelection.measureSelection(this.score, staff.staffId, smoMeasure.measureNumber.measureIndex);\r\n this.smoMeasures.push(smoMeasure);\r\n if (this.staves.length <= staffId) {\r\n this.staves.push(staff);\r\n }\r\n if (selection === null) {\r\n return;\r\n }\r\n let softmax = selection.measure.format.proportionality;\r\n if (softmax === SmoMeasureFormat.defaultProportionality) {\r\n softmax = this.score.layoutManager?.getGlobalLayout().proportionality ?? 0;\r\n }\r\n const vxMeasure: VxMeasure = new VxMeasure(this.context, selection, printing, softmax);\r\n\r\n // create the vex notes, beam groups etc. for the measure\r\n vxMeasure.preFormat();\r\n this.vxMeasures.push(vxMeasure);\r\n\r\n const lastStaff = (staffId === this.score.staves.length - 1);\r\n const smoGroupMap: Record = {};\r\n const adjXMap: Record = {};\r\n const vxMeasures = this.vxMeasures.filter((mm) => !mm.smoMeasure.svg.hideEmptyMeasure);\r\n // If this is the last staff in the column, render the column with justification\r\n if (lastStaff) {\r\n vxMeasures.forEach((mm) => {\r\n if (typeof(adjXMap[mm.smoMeasure.measureNumber.systemIndex]) === 'undefined') {\r\n adjXMap[mm.smoMeasure.measureNumber.systemIndex] = mm.smoMeasure.svg.adjX;\r\n }\r\n adjXMap[mm.smoMeasure.measureNumber.systemIndex] = Math.max(adjXMap[mm.smoMeasure.measureNumber.systemIndex], mm.smoMeasure.svg.adjX);\r\n });\r\n vxMeasures.forEach((vv: VxMeasure) => {\r\n if (!vv.rendered && !vv.smoMeasure.svg.hideEmptyMeasure && vv.stave) {\r\n vv.stave.setNoteStartX(vv.stave.getNoteStartX() + adjXMap[vv.smoMeasure.measureNumber.systemIndex] - vv.smoMeasure.svg.adjX);\r\n const systemGroup = this.score.getSystemGroupForStaff(vv.selection);\r\n const justifyGroup: string = (systemGroup && vv.smoMeasure.format.autoJustify) ? systemGroup.attrs.id : vv.selection.staff.attrs.id;\r\n if (!smoGroupMap[justifyGroup]) {\r\n smoGroupMap[justifyGroup] = { firstMeasure: vv, voices: [] };\r\n }\r\n smoGroupMap[justifyGroup].voices =\r\n smoGroupMap[justifyGroup].voices.concat(vv.voiceAr);\r\n if (vv.tabVoice) {\r\n smoGroupMap[justifyGroup].voices.concat(vv.tabVoice);\r\n }\r\n }\r\n });\r\n }\r\n const keys = Object.keys(smoGroupMap);\r\n keys.forEach((key) => {\r\n smoGroupMap[key].firstMeasure.format(smoGroupMap[key].voices);\r\n });\r\n if (lastStaff) {\r\n vxMeasures.forEach((vv) => {\r\n if (!vv.rendered) {\r\n vv.render();\r\n }\r\n });\r\n }\r\n // Keep track of the y coordinate for the nth staff\r\n const renderedConnection: Record = {};\r\n\r\n if (systemIndex === 0 && lastStaff) {\r\n if (staff.bracketMap[this.lineIndex]) {\r\n staff.bracketMap[this.lineIndex].forEach((element) => {\r\n element.remove();\r\n });\r\n }\r\n staff.bracketMap[this.lineIndex] = [];\r\n const group = this.context.getContext().openGroup();\r\n group.classList.add('lineBracket-' + this.lineIndex);\r\n group.classList.add('lineBracket');\r\n staff.bracketMap[this.lineIndex].push(group);\r\n vxMeasures.forEach((vv) => {\r\n const systemGroup = this.score.getSystemGroupForStaff(vv.selection);\r\n if (systemGroup && !renderedConnection[systemGroup.attrs.id] && \r\n !vv.smoMeasure.svg.hideEmptyMeasure) {\r\n renderedConnection[systemGroup.attrs.id] = 1;\r\n const startSel = this.vxMeasures[systemGroup.startSelector.staff];\r\n const endSel = this.vxMeasures[systemGroup.endSelector.staff];\r\n if (startSel && startSel.rendered && \r\n endSel && endSel.rendered) {\r\n const c1 = new VF.StaveConnector(startSel.stave!, endSel.stave!)\r\n .setType(leftConnectorVx(systemGroup));\r\n c1.setContext(this.context.getContext()).draw();\r\n brackets = true;\r\n }\r\n }\r\n });\r\n if (!brackets && vxMeasures.length > 1) {\r\n const c2 = new VF.StaveConnector(vxMeasures[0].stave!, vxMeasures[vxMeasures.length - 1].stave!);\r\n c2.setType(VF.StaveConnector.type.SINGLE_RIGHT);\r\n c2.setContext(this.context.getContext()).draw();\r\n }\r\n // draw outer brace on parts with multiple staves (e.g. keyboards)\r\n vxMeasures.forEach((vv) => {\r\n if (vv.selection.staff.partInfo.stavesAfter > 0) {\r\n if (this.vxMeasures.length > vv.selection.selector.staff + 1) {\r\n const endSel = this.vxMeasures[vv.selection.selector.staff + 1];\r\n const startSel = vv;\r\n if (startSel && startSel.rendered && \r\n endSel && endSel.rendered) {\r\n const c1 = new VF.StaveConnector(startSel.stave!, endSel.stave!)\r\n .setType(VF.StaveConnector.type.BRACE);\r\n c1.setContext(this.context.getContext()).draw(); \r\n }\r\n }\r\n };\r\n });\r\n this.context.getContext().closeGroup();\r\n } else if (lastStaff && smoMeasure.measureNumber.measureIndex + 1 < staff.measures.length) {\r\n if (staff.measures[smoMeasure.measureNumber.measureIndex + 1].measureNumber.systemIndex === 0) {\r\n const endMeasure = vxMeasure;\r\n const startMeasure = vxMeasures.find((vv) => vv.selection.selector.staff === 0 &&\r\n vv.selection.selector.measure === vxMeasure.selection.selector.measure && \r\n vv.smoMeasure.svg.hideEmptyMeasure === false);\r\n if (endMeasure && endMeasure.stave && startMeasure && startMeasure.stave) {\r\n const group = this.context.getContext().openGroup();\r\n group.classList.add('endBracket-' + this.lineIndex);\r\n group.classList.add('endBracket');\r\n staff.bracketMap[this.lineIndex].push(group);\r\n const c2 = new VF.StaveConnector(startMeasure.stave, endMeasure.stave)\r\n .setType(VF.StaveConnector.type.SINGLE_RIGHT);\r\n c2.setContext(this.context.getContext()).draw();\r\n this.context.getContext().closeGroup();\r\n }\r\n }\r\n }\r\n // keep track of left-hand side for system connectors\r\n if (systemIndex === 0) {\r\n if (staffId === 0) {\r\n this.leftConnector[0] = vxMeasure.stave;\r\n } else if (staffId > this.maxStaffIndex) {\r\n this.maxStaffIndex = staffId;\r\n this.leftConnector[1] = vxMeasure.stave;\r\n }\r\n } else if (smoMeasure.measureNumber.systemIndex > this.maxSystemIndex) {\r\n this.maxSystemIndex = smoMeasure.measureNumber.systemIndex;\r\n }\r\n this.measures.push(vxMeasure);\r\n }\r\n}\r\n","/**\r\n * definitions shared by all SMO types\r\n * @module /smo/data/common\r\n */\r\n/**\r\n * Same as attrs object in Vex objects.\r\n * @param id - unique identifier, can be used in DOM elements\r\n * @param type - a little bit redundate with `ctor` in `SmoObjectParams`\r\n */\r\nexport interface SmoAttrs {\r\n id: string,\r\n type: string\r\n}\r\nexport const smoXmlNs = 'https://aarondavidnewman.github.io/Smoosic';\r\n\r\n// export abstract class SmoXmlSerializable {\r\n// abstract serializeXml(namespace: string, parentElement: Element, tagName: string): Element\r\n// }\r\nexport interface SmoXmlSerializable {\r\n serializeXml: (namespace: string, parentElement: Element, tag: string) => Element;\r\n ctor: string\r\n}\r\nexport function createXmlAttributes(element: Element, obj: any) {\r\n Object.keys(obj).forEach((key) => {\r\n const attr = element.ownerDocument.createAttribute(key);\r\n attr.value = obj[key];\r\n element.setAttributeNode(attr);\r\n });\r\n}\r\nexport function createXmlAttribute(element: Element, name: string, value: any) {\r\n const obj: any = {};\r\n obj[name] = value;\r\n createXmlAttributes(element, obj);\r\n}\r\n\r\nvar nextId = 32768;\r\nexport const getId = () => `smo` + (nextId++).toString();\r\n/**\r\n * All note, measure, staff, and score objects have\r\n * a serialize method and are deserializable with constructor `ctor`\r\n */\r\nexport interface SmoObjectParams {\r\n ctor: string,\r\n attrs?: SmoAttrs\r\n}\r\n\r\n/**\r\n * Note duration. The same abstraction used by vex, except here denominator is\r\n * always 1. remainder is used to reconstruct non-tuplets from tuplets.\r\n * @param numerator - duration, 4096 is 1/4 note\r\n * @param denominator - always 1 for SMO objects\r\n * @param remainder - used for tuplets whose duration doesn't divide evenly\r\n */\r\nexport interface Ticks {\r\n numerator: number,\r\n denominator: number,\r\n remainder: number\r\n}\r\n\r\n/**\r\n * constraint for SmoPitch.letter value, in lower case\r\n */\r\nexport type PitchLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g';\r\n\r\nexport function IsPitchLetter(letter: PitchLetter | string): letter is PitchLetter {\r\n return letter.length === 1 && letter[0] >= 'a' && letter[0] <= 'g';\r\n}\r\n\r\n/**\r\n * PitchKey is a SmoPitch, without the octave\r\n * @param letter - letter note\r\n * @param accidental - an accidental or microtone\r\n */\r\nexport interface PitchKey {\r\n letter: PitchLetter,\r\n accidental: string\r\n}\r\n/**\r\n * Represents a single pitch in Smo object model.\r\n * @param letter - letter note\r\n * @param accidental - an accidental or microtone\r\n * @param octave - standard octave\r\n * @param cautionary? - can be used for courtesy accidental\r\n */\r\nexport interface Pitch {\r\n letter: PitchLetter,\r\n accidental: string,\r\n octave: number,\r\n cautionary?: boolean,\r\n forced?: boolean,\r\n role?: string\r\n}\r\n\r\n/**\r\n * A tuple indicating measure location in the score:\r\n * @param measureIndex - the actual offset from the first measure\r\n * @param localIndex - the index as shown to the user, considers renumbering\r\n * @param sytemIndex - which bar (column) of a system this measure is\r\n * @param staffId - which staff (row) of a system this measure is\r\n */\r\nexport interface MeasureNumber {\r\n measureIndex: number,\r\n localIndex: number,\r\n systemIndex: number,\r\n staffId: number\r\n}\r\n/**\r\n * musical artifacts can contain temporary svg information for\r\n * mapping the UI.\r\n */\r\nexport class SvgPoint {\r\n x: number;\r\n y: number;\r\n static get default() {\r\n return { x: 0, y: 0 };\r\n }\r\n constructor() {\r\n this.x = 0;\r\n this.y = 0;\r\n }\r\n}\r\n/**\r\n * musical artifacts can contain temporary svg information for\r\n * mapping the UI.\r\n */\r\n export class SvgBox {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n static get default(): SvgBox {\r\n return { x: 0, y: 0, width: -1, height: -1 };\r\n }\r\n constructor() {\r\n this.x = 0;\r\n this.y = 0;\r\n this.width = -1;\r\n this.height = -1;\r\n }\r\n}\r\n/**\r\n * kind of a pointless class...\r\n */\r\nexport interface SvgDimensions {\r\n width: number,\r\n height: number\r\n}\r\n/**\r\n * A `Transposable` is an abstraction of a note.\r\n * Can be passed into methods that transform pitches for both\r\n * grace notes and normal notes.\r\n * @param pitches - SMO pitch type\r\n * @param noteType - same convention as VexFlow, 'n' for note, 'r' for rest\r\n * @param renderId - ID for the containing SVG group, used to map UI elements\r\n * @param renderedBox - bounding box in client coordinates\r\n * @param logicalBox - bounding box in SVG coordinates\r\n */\r\nexport interface Transposable {\r\n pitches: Pitch[],\r\n noteType: string,\r\n renderId: string | null,\r\n logicalBox: SvgBox | null\r\n}\r\n\r\n\r\n/**\r\n * All note, measure etc. modifiers have these attributes. The SVG info\r\n * is for the tracker to track the artifacts in the UI (mouse events, etc)\r\n * @param ctor - constructor name for deserialize\r\n * @param logicalBox - bounding box in SVG coordinates\r\n * @param attr - unique ID, simlar to vex object attrs field\r\n */\r\nexport interface SmoModifierBase {\r\n ctor: string,\r\n logicalBox: SvgBox | null,\r\n attrs: SmoAttrs,\r\n serialize: () => any;\r\n}\r\n\r\nexport function serializeXmlModifierArray(object: SmoXmlSerializable[], namespace: string, parentElement: Element, tag: string) {\r\n if (object.length === 0) {\r\n return parentElement;\r\n }\r\n const arEl = parentElement.ownerDocument.createElementNS(namespace, `${tag}-array`);\r\n parentElement.appendChild(arEl);\r\n createXmlAttribute(arEl, 'container', 'array');\r\n createXmlAttribute(arEl, 'name', `${tag}`);\r\n for (var j = 0; j < object.length; ++j) {\r\n const instEl = parentElement.ownerDocument.createElementNS(namespace, `${tag}-instance`);\r\n arEl.appendChild(instEl);\r\n object[j].serializeXml(namespace, instEl, object[j].ctor);\r\n }\r\n return arEl;\r\n}\r\n\r\n/**\r\n * Renderable is just a thing that has a bounding box\r\n */\r\nexport interface Renderable {\r\n logicalBox: SvgBox | null | undefined\r\n}\r\n/**\r\n * Restriction from string to supported clefs\r\n */\r\nexport type Clef = 'treble' | 'bass' | 'tenor' | 'alto' | 'soprano' | 'percussion'\r\n | 'mezzo-soprano' | 'baritone-c' | 'baritone-f' | 'subbass' | 'french';\r\n\r\nexport var Clefs: Clef[] = ['treble' , 'bass' , 'tenor' , 'alto' , 'soprano' , 'percussion'\r\n, 'mezzo-soprano' , 'baritone-c' , 'baritone-f' , 'subbass' , 'french'];\r\n\r\nexport function IsClef(clef: Clef | string): clef is Clef {\r\n return Clefs.findIndex((x) => clef === x) >= 0;\r\n}\r\n\r\n/**\r\n * Most event handling in SMO is an 'any' from jquery, but\r\n * key events are sometimes narrowed to the common browser key event\r\n */\r\nexport interface KeyEvent {\r\n type: string,\r\n shiftKey: boolean,\r\n ctrlKey: boolean,\r\n altKey: boolean,\r\n key: string,\r\n keyCode: string,\r\n code: string,\r\n event: string | null\r\n}\r\n\r\nexport interface TickAccidental {\r\n duration: number,\r\n pitch: Pitch\r\n}\r\n\r\n/**\r\n * Used to create {@link MeasureTickmaps}\r\n */\r\nexport interface AccidentalArray {\r\n duration: string | number,\r\n pitches: Record\r\n}\r\n\r\nexport interface AccidentalDisplay {\r\n symbol: string,\r\n courtesy: boolean,\r\n forced: boolean\r\n}","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\n/**\r\n * Contains definition and supporting classes for {@link SmoMeasure}.\r\n * Most of the engraving is done at the measure level. Measure contains multiple (at least 1)\r\n * voices, which in turn contain notes. Each measure also contains formatting information. This\r\n * is mostly serialized outside of measure (in score), since columns and often an entire region\r\n * share measure formatting. Measures also contain modifiers like barlines. Tuplets and beam groups\r\n * are contained at the measure level.\r\n * @module /smo/data/measure\r\n */\r\nimport { smoSerialize } from '../../common/serializationHelpers';\r\nimport { SmoMusic } from './music';\r\nimport {\r\n SmoBarline, SmoMeasureModifierBase, SmoRepeatSymbol, SmoTempoText, SmoMeasureFormat,\r\n SmoVolta, SmoRehearsalMarkParams, SmoRehearsalMark, SmoTempoTextParams, TimeSignature,\r\n TimeSignatureParametersSer, SmoMeasureFormatParamsSer, SmoTempoTextParamsSer\r\n} from './measureModifiers';\r\nimport { SmoNote, NoteType, SmoNoteParamsSer } from './note';\r\nimport { SmoTuplet, SmoTupletParamsSer, SmoTupletParams } from './tuplet';\r\nimport { layoutDebug } from '../../render/sui/layoutDebug';\r\nimport { SvgHelpers } from '../../render/sui/svgHelpers';\r\nimport { TickMap } from '../xform/tickMap';\r\nimport { MeasureNumber, SvgBox, SmoAttrs, Pitch, PitchLetter, Clef, \r\n TickAccidental, AccidentalArray, getId } from './common';\r\nimport { SmoSelector } from '../xform/selections';\r\nimport { FontInfo } from '../../common/vex';\r\n/**\r\n * Voice is just a container for {@link SmoNote}\r\n */\r\nexport interface SmoVoice {\r\n notes: SmoNote[]\r\n}\r\n\r\nexport interface SmoVoiceSer {\r\n notes: SmoNoteParamsSer[]\r\n}\r\n/**\r\n * TickMappable breaks up a circular dependency on modifiers\r\n * like @SmoDuration\r\n */\r\nexport interface TickMappable {\r\n voices: SmoVoice[],\r\n keySignature: string\r\n}\r\n\r\nexport interface MeasureTick {\r\n voiceIndex: number,\r\n tickIndex: number\r\n}\r\n\r\n/**\r\n * Break up a circlar dependency with {@link SmoBeamGroup}\r\n */\r\nexport interface ISmoBeamGroup {\r\n notes: SmoNote[],\r\n voice: number,\r\n attrs: SmoAttrs\r\n}\r\n/**\r\n * geometry information about the current measure for rendering and\r\n * score layout.\r\n * @internal\r\n */\r\nexport interface MeasureSvg {\r\n staffWidth: number,\r\n unjustifiedWidth: number,\r\n adjX: number, // The start point of the music in the stave (after time sig, etc)\r\n maxColumnStartX: number,\r\n staffX: number, // The left-most x position of the staff\r\n staffY: number,\r\n logicalBox: SvgBox,\r\n yTop: number,\r\n adjRight: number,\r\n history: string[],\r\n lineIndex: number,\r\n pageIndex: number,\r\n rowInSystem: number,\r\n forceClef: boolean,\r\n forceKeySignature: boolean,\r\n forceTimeSignature: boolean,\r\n forceTempo: boolean,\r\n hideEmptyMeasure: boolean,\r\n hideMultimeasure: boolean,\r\n multimeasureLength: number,\r\n multimeasureEndBarline: number,\r\n element: SVGSVGElement | null,\r\n tabStaveBox?: SvgBox,\r\n tabElement?: SVGSVGElement\r\n}\r\n\r\n/**\r\n * Interface for a {@link TickMap} for each voice\r\n * for formatting\r\n */\r\nexport interface MeasureTickmaps {\r\n tickmaps: TickMap[],\r\n accidentalMap: Record>,\r\n accidentalArray: AccidentalArray[]\r\n}\r\n/**\r\n * Column-mapped modifiers, managed by the {@link SmoScore}\r\n */\r\nexport interface ColumnMappedParams {\r\n // ['timeSignature', 'keySignature', 'tempo']\r\n timeSignature: any,\r\n keySignature: string,\r\n tempo: any\r\n}\r\n// @internal\r\nexport type SmoMeasureNumberParam = 'transposeIndex' | 'activeVoice' | 'lines' | 'repeatCount';\r\n// @internal\r\nexport const SmoMeasureNumberParams: SmoMeasureNumberParam[] = ['transposeIndex', 'activeVoice', 'lines', 'repeatCount'];\r\n// @internal\r\nexport type SmoMeasureStringParam = 'keySignature';\r\n// @internal\r\nexport const SmoMeasureStringParams: SmoMeasureStringParam[] = ['keySignature'];\r\n/**\r\n * constructor parameters for a {@link SmoMeasure}. Usually you will call\r\n * {@link SmoMeasure.defaults}, and modify the parameters you need to change.\r\n *\r\n * @param timeSignature\r\n * @param keySignature\r\n * @param tuplets\r\n * @param transposeIndex calculated from {@link SmoPartInfo} for non-concert-key instruments\r\n * @param lines number of lines in the stave\r\n * @param staffY Y coordinate (UL corner) of the measure stave\r\n * @param measureNumber combination configured/calculated measure number\r\n * @param clef\r\n * @param voices\r\n * @param activeVoice the active voice in the editor\r\n * @param tempo\r\n * @param format measure format, is managed by the score\r\n * @param modifiers All measure modifiers that5 aren't format, timeSignature or tempo\r\n * @category SmoParameters\r\n */\r\nexport interface SmoMeasureParams {\r\n timeSignature: TimeSignature,\r\n keySignature: string,\r\n tuplets: SmoTuplet[],\r\n transposeIndex: number,\r\n lines: number,\r\n // bars: [1, 1], // follows enumeration in VF.Barline\r\n measureNumber: MeasureNumber,\r\n clef: Clef,\r\n voices: SmoVoice[],\r\n activeVoice: number,\r\n tempo: SmoTempoText,\r\n format: SmoMeasureFormat | null,\r\n modifiers: SmoMeasureModifierBase[],\r\n repeatSymbol: boolean,\r\n repeatCount: number\r\n}\r\n\r\n/**\r\n * The serializeable bits of SmoMeasure. Some parameters are \r\n * mapped by the stave if the don't change every measure, e.g.\r\n * time signature.\r\n * @category serialization\r\n */\r\nexport interface SmoMeasureParamsSer {\r\n /**\r\n * constructor\r\n */\r\n ctor: string;\r\n /**\r\n * a list of tuplets (serialized)\r\n */\r\n tuplets: SmoTupletParamsSer[],\r\n /**\r\n * transpose the notes up/down. TODO: this should not be serialized\r\n * as its part of the instrument parameters\r\n */\r\n transposeIndex: number,\r\n /**\r\n * lines in the staff (e.g. percussion)\r\n */\r\n lines: number,\r\n /**\r\n * measure number, absolute and relative/remapped\r\n */\r\n measureNumber: MeasureNumber,\r\n /**\r\n * start clef\r\n */\r\n clef: Clef,\r\n /**\r\n * voices contain notes\r\n */\r\n voices: SmoVoiceSer[],\r\n /**\r\n * all other modifiers (barlines, etc)\r\n */\r\n modifiers: SmoMeasureModifierBase[],\r\n // the next 3 are not serialized as part of the measure in most cases, since they are\r\n // mapped to specific measures in the score/system\r\n /**\r\n * key signature\r\n */\r\n keySignature?: string,\r\n /**\r\n * time signature serialization\r\n */\r\n timeSignature?: TimeSignatureParametersSer,\r\n /**\r\n * tempo at this point\r\n */\r\n tempo: SmoTempoTextParamsSer\r\n \r\n}\r\n\r\n/**\r\n * Only arrays and measure numbers are serilialized with default values.\r\n * @param params - result of serialization\r\n * @returns \r\n */\r\nfunction isSmoMeasureParamsSer(params: Partial):params is SmoMeasureParamsSer {\r\n if (!Array.isArray(params.voices) || \r\n !Array.isArray(params.tuplets) || !Array.isArray(params.modifiers) ||\r\n typeof(params?.measureNumber?.measureIndex) !== 'number') {\r\n return false;\r\n }\r\n return true;\r\n}\r\n/**\r\n * Data for a measure of music. Many rules of musical engraving are\r\n * enforced at a measure level: the duration of notes, accidentals, etc.\r\n * \r\n * Measures contain {@link SmoNote}, {@link SmoTuplet}, and {@link SmoBeamGroup}\r\n * Measures are contained in {@link SmoSystemStaff}\r\n * @category SmoObject\r\n */\r\nexport class SmoMeasure implements SmoMeasureParams, TickMappable {\r\n static get timeSignatureDefault(): TimeSignature {\r\n return new TimeSignature(TimeSignature.defaults);\r\n }\r\n static defaultDupleDuration: number = 4096;\r\n static defaultTripleDuration: number = 2048 * 3;\r\n // @internal\r\n static readonly _defaults: SmoMeasureParams = {\r\n timeSignature: SmoMeasure.timeSignatureDefault,\r\n keySignature: 'C',\r\n tuplets: [],\r\n transposeIndex: 0,\r\n modifiers: [],\r\n // bars: [1, 1], // follows enumeration in VF.Barline\r\n measureNumber: {\r\n localIndex: 0,\r\n systemIndex: 0,\r\n measureIndex: 0,\r\n staffId: 0\r\n },\r\n clef: 'treble',\r\n lines: 5,\r\n voices: [],\r\n format: new SmoMeasureFormat(SmoMeasureFormat.defaults),\r\n activeVoice: 0,\r\n tempo: new SmoTempoText(SmoTempoText.defaults),\r\n repeatSymbol: false,\r\n repeatCount: 0 \r\n }\r\n\r\n /**\r\n * Default constructor parameters. Defaults are always copied so the\r\n * caller can modify them to create a new measure.\r\n * @returns constructor params for a new measure\r\n */\r\n static get defaults(): SmoMeasureParams {\r\n const proto: any = JSON.parse(JSON.stringify(SmoMeasure._defaults));\r\n proto.format = new SmoMeasureFormat(SmoMeasureFormat.defaults);\r\n proto.tempo = new SmoTempoText(SmoTempoText.defaults);\r\n proto.modifiers.push(new SmoBarline({\r\n position: SmoBarline.positions.start,\r\n barline: SmoBarline.barlines.singleBar\r\n }));\r\n proto.modifiers.push(new SmoBarline({\r\n position: SmoBarline.positions.end,\r\n barline: SmoBarline.barlines.singleBar\r\n }));\r\n return proto;\r\n }\r\n // @ignore\r\n static convertLegacyTimeSignature(ts: string) {\r\n const rv = new TimeSignature(TimeSignature.defaults);\r\n rv.timeSignature = ts;\r\n return rv;\r\n }\r\n timeSignature: TimeSignature = SmoMeasure.timeSignatureDefault;\r\n /**\r\n * Overrides display of actual time signature, in the case of\r\n * pick-up notes where the actual and displayed durations are different\r\n */\r\n keySignature: string = '';\r\n canceledKeySignature: string = '';\r\n tuplets: SmoTuplet[] = [];\r\n repeatSymbol: boolean = false;\r\n repeatCount: number = 0;\r\n ctor: string='SmoMeasure';\r\n /**\r\n * Adjust for non-concert pitch intstruments\r\n */\r\n transposeIndex: number = 0;\r\n modifiers: SmoMeasureModifierBase[] = [];\r\n /**\r\n * Row, column, and custom numbering information about this measure.\r\n */\r\n measureNumber: MeasureNumber = {\r\n localIndex: 0,\r\n systemIndex: 0,\r\n measureIndex: 0,\r\n staffId: 0\r\n };\r\n clef: Clef = 'treble';\r\n voices: SmoVoice[] = [];\r\n /**\r\n * the active voice in the editor, if there are multiple voices\r\n * */\r\n activeVoice: number = 0;\r\n tempo: SmoTempoText;\r\n beamGroups: ISmoBeamGroup[] = [];\r\n lines: number = 5;\r\n /**\r\n * Runtime information about rendering\r\n */\r\n svg: MeasureSvg;\r\n /**\r\n * Measure-specific formatting parameters.\r\n */\r\n format: SmoMeasureFormat;\r\n /**\r\n * Information for identifying this object\r\n */\r\n id: string;\r\n\r\n /**\r\n * Fill in components. We assume the modifiers are already constructed,\r\n * e.g. by deserialize or the calling function.\r\n * @param params\r\n */\r\n constructor(params: SmoMeasureParams) {\r\n this.tempo = new SmoTempoText(SmoTempoText.defaults);\r\n this.svg = {\r\n staffWidth: 0,\r\n unjustifiedWidth: 0,\r\n staffX: 0,\r\n staffY: 0,\r\n logicalBox: {\r\n x: 0, y: 0, width: 0, height: 0\r\n },\r\n yTop: 0,\r\n adjX: 0,\r\n maxColumnStartX: 0,\r\n adjRight: 0,\r\n history: [],\r\n lineIndex: 0,\r\n pageIndex: 0,\r\n rowInSystem: 0,\r\n forceClef: false,\r\n forceKeySignature: false,\r\n forceTimeSignature: false,\r\n forceTempo: false,\r\n hideEmptyMeasure: false,\r\n hideMultimeasure: false,\r\n multimeasureLength: 0,\r\n multimeasureEndBarline: SmoBarline.barlines['singleBar'],\r\n element: null\r\n };\r\n\r\n const defaults = SmoMeasure.defaults;\r\n SmoMeasureNumberParams.forEach((param) => {\r\n if (typeof (params[param]) !== 'undefined') {\r\n this[param] = params[param];\r\n }\r\n });\r\n SmoMeasureStringParams.forEach((param) => {\r\n this[param] = params[param] ? params[param] : defaults[param];\r\n });\r\n this.clef = params.clef;\r\n this.repeatSymbol = params.repeatSymbol;\r\n this.measureNumber = JSON.parse(JSON.stringify(params.measureNumber));\r\n if (params.tempo) {\r\n this.tempo = new SmoTempoText(params.tempo);\r\n }\r\n // Handle legacy time signature format\r\n if (params.timeSignature) {\r\n const tsAny = params.timeSignature as any;\r\n if (typeof (tsAny) === 'string') {\r\n this.timeSignature = SmoMeasure.convertLegacyTimeSignature(tsAny);\r\n } else {\r\n this.timeSignature = TimeSignature.createFromPartial(tsAny);\r\n }\r\n }\r\n this.voices = params.voices ? params.voices : [];\r\n this.tuplets = params.tuplets ? params.tuplets : [];\r\n this.modifiers = params.modifiers ? params.modifiers : defaults.modifiers;\r\n this.setDefaultBarlines();\r\n this.keySignature = SmoMusic.vexKeySigWithOffset(this.keySignature, this.transposeIndex);\r\n\r\n if (!(params.format)) {\r\n this.format = new SmoMeasureFormat(SmoMeasureFormat.defaults);\r\n this.format.measureIndex = this.measureNumber.measureIndex;\r\n } else {\r\n this.format = new SmoMeasureFormat(params.format);\r\n }\r\n this.id = getId().toString();\r\n this.updateClefChangeNotes();\r\n }\r\n\r\n // @internal\r\n // used for serialization\r\n static get defaultAttributes() {\r\n return [\r\n 'keySignature', \r\n 'measureNumber',\r\n 'activeVoice', 'clef', 'transposeIndex',\r\n 'format', 'rightMargin', 'lines', 'repeatSymbol', 'repeatCount'\r\n ];\r\n }\r\n\r\n // @internal\r\n // used for serialization\r\n static get formattingOptions() {\r\n return ['customStretch', 'customProportion', 'autoJustify', 'systemBreak',\r\n 'pageBreak', 'padLeft'];\r\n }\r\n // @internal\r\n // used for serialization\r\n static get columnMappedAttributes() {\r\n return ['timeSignature', 'keySignature', 'tempo'];\r\n }\r\n static get serializableAttributes() {\r\n const rv: any = [];\r\n SmoMeasure.defaultAttributes.forEach((attr) => {\r\n if (SmoMeasure.columnMappedAttributes.indexOf(attr) < 0 && attr !== 'format') {\r\n rv.push(attr);\r\n }\r\n });\r\n return rv;\r\n }\r\n /**\r\n // Return true if the time signatures are the same, for display purposes (e.g. if a time sig change\r\n // is required)\r\n */\r\n static timeSigEqual(o1: TimeSignature, o2: TimeSignature) {\r\n return o1.timeSignature === o2.timeSignature && o1.useSymbol === o2.useSymbol;\r\n }\r\n /**\r\n * If there is a clef change mid-measure, update the actual clefs of the notes\r\n * so they display correctly.\r\n */\r\n updateClefChangeNotes() {\r\n let changed = false;\r\n let curTick = 0;\r\n let clefChange = this.clef;\r\n for (var i = 0; i < this.voices.length; ++i) {\r\n const voice = this.voices[i];\r\n curTick = 0;\r\n for (var j = 0; j < voice.notes.length; ++j) {\r\n const smoNote = voice.notes[j];\r\n smoNote.clef = this.clef;\r\n if (smoNote.clefNote && smoNote.clefNote.clef !== this.clef) {\r\n clefChange = smoNote.clefNote.clef;\r\n curTick += smoNote.tickCount;\r\n changed = true;\r\n break;\r\n }\r\n curTick += smoNote.tickCount;\r\n }\r\n if (changed) {\r\n break;\r\n }\r\n }\r\n if (!changed) {\r\n return;\r\n }\r\n // clefChangeTick is where the change goes. We only support\r\n // one per measure, others are ignored.\r\n const clefChangeTick = curTick;\r\n\r\n for (var i = 0; i < this.voices.length; ++i) {\r\n const voice = this.voices[i];\r\n curTick = 0;\r\n for (var j = 0; j < voice.notes.length; ++j) {\r\n const smoNote = voice.notes[j];\r\n const noteTicks = smoNote.tickCount;\r\n if (curTick + noteTicks >= clefChangeTick) {\r\n smoNote.clef = clefChange;\r\n }\r\n // Remove any redundant clef changes later in the measure\r\n if (curTick + noteTicks > clefChangeTick) {\r\n if (smoNote.clefNote && smoNote.clefNote.clef === clefChange) {\r\n smoNote.clefNote = null;\r\n }\r\n }\r\n curTick += noteTicks;\r\n }\r\n }\r\n }\r\n /**\r\n * @internal\r\n * @returns column mapped parameters, serialized. caller will\r\n * decide if the parameters need to be persisted\r\n */\r\n serializeColumnMapped(): ColumnMappedParams {\r\n //\r\n return {\r\n timeSignature: this.timeSignature.serialize(),\r\n keySignature: this.keySignature,\r\n tempo: this.tempo.serialize()\r\n };\r\n }\r\n getColumnMapped(): ColumnMappedParams {\r\n return {\r\n timeSignature: this.timeSignature,\r\n keySignature: this.keySignature,\r\n tempo: this.tempo\r\n };\r\n }\r\n\r\n /**\r\n * Convert this measure object to a JSON object, recursively serializing all the notes,\r\n * note modifiers, etc.\r\n */\r\n serialize(): SmoMeasureParamsSer {\r\n const params: Partial = { \"ctor\": \"SmoMeasure\" };\r\n let ser = true;\r\n smoSerialize.serializedMergeNonDefault(SmoMeasure.defaults, SmoMeasure.serializableAttributes, this, params);\r\n // Don't serialize default things\r\n const fmt = this.format.serialize();\r\n // measure number can't be defaulted b/c tempos etc. can map to default measure\r\n params.measureNumber = JSON.parse(JSON.stringify(this.measureNumber));\r\n params.tuplets = [];\r\n params.voices = [];\r\n params.modifiers = [];\r\n\r\n this.tuplets.forEach((tuplet) => {\r\n params.tuplets!.push(tuplet.serialize());\r\n });\r\n\r\n this.voices.forEach((voice) => {\r\n const obj: any = {\r\n\r\n notes: []\r\n };\r\n voice.notes.forEach((note) => {\r\n obj.notes.push(note.serialize());\r\n });\r\n params.voices!.push(obj);\r\n });\r\n\r\n this.modifiers.forEach((modifier) => {\r\n ser = true;\r\n /* don't serialize default modifiers */\r\n if (modifier.ctor === 'SmoBarline' && (modifier as SmoBarline).position === SmoBarline.positions.start &&\r\n (modifier as SmoBarline).barline === SmoBarline.barlines.singleBar) {\r\n ser = false;\r\n } else if (modifier.ctor === 'SmoBarline' && (modifier as SmoBarline).position === SmoBarline.positions.end\r\n && (modifier as SmoBarline).barline === SmoBarline.barlines.singleBar) {\r\n ser = false;\r\n } else if (modifier.ctor === 'SmoTempoText') {\r\n // we don't save tempo text as a modifier anymore\r\n ser = false;\r\n } else if ((modifier as SmoRepeatSymbol).ctor === 'SmoRepeatSymbol' && (modifier as SmoRepeatSymbol).position === SmoRepeatSymbol.positions.start\r\n && (modifier as SmoRepeatSymbol).symbol === SmoRepeatSymbol.symbols.None) {\r\n ser = false;\r\n }\r\n if (ser) {\r\n params.modifiers!.push(modifier.serialize());\r\n }\r\n });\r\n // ['timeSignature', 'keySignature', 'tempo']\r\n if (!isSmoMeasureParamsSer(params)) {\r\n throw 'invalid measure';\r\n }\r\n return params;\r\n }\r\n /**\r\n * restore a serialized measure object. Usually called as part of deserializing a score,\r\n * but can also be used to restore a measure due to an undo operation. Recursively\r\n * deserialize all the notes and modifiers to construct a new measure.\r\n * @param jsonObj the serialized SmoMeasure\r\n * @returns\r\n */\r\n static deserialize(jsonObj: SmoMeasureParamsSer): SmoMeasure {\r\n let j = 0;\r\n let i = 0;\r\n const voices: SmoVoice[] = [];\r\n const noteSum = [];\r\n for (j = 0; j < jsonObj.voices.length; ++j) {\r\n const voice = jsonObj.voices[j];\r\n const notes: SmoNote[] = [];\r\n voices.push({\r\n notes\r\n });\r\n for (i = 0; i < voice.notes.length; ++i) {\r\n const noteParams = voice.notes[i];\r\n const smoNote = SmoNote.deserialize(noteParams);\r\n notes.push(smoNote);\r\n noteSum.push(smoNote);\r\n }\r\n }\r\n\r\n const tuplets = [];\r\n for (j = 0; j < jsonObj.tuplets.length; ++j) {\r\n const tupJson = jsonObj.tuplets[j];\r\n const tupParams = SmoTuplet.defaults;\r\n // Legacy schema had attrs.id, now it is just id\r\n if ((tupJson as any).attrs && (tupJson as any).attrs.id) {\r\n tupParams.id = (tupJson as any).attrs.id;\r\n }\r\n smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj.tuplets[j], tupParams);\r\n const noteAr = noteSum.filter((nn: SmoNote) =>\r\n nn.isTuplet && nn.tupletId === tupParams.id);\r\n\r\n // Bug fix: A tuplet with no notes may be been overwritten\r\n // in a copy/paste operation\r\n if (noteAr.length > 0) {\r\n tupParams.notes = noteAr;\r\n const tuplet = new SmoTuplet(tupParams);\r\n tuplets.push(tuplet);\r\n }\r\n }\r\n\r\n const modifiers: SmoMeasureModifierBase[] = [];\r\n jsonObj.modifiers.forEach((modParams: any) => {\r\n const modifier: SmoMeasureModifierBase = SmoMeasureModifierBase.deserialize(modParams);\r\n modifiers.push(modifier);\r\n });\r\n const params: SmoMeasureParams = SmoMeasure.defaults;\r\n smoSerialize.serializedMerge(SmoMeasure.defaultAttributes, jsonObj, params);\r\n\r\n // explode column-mapped\r\n if (jsonObj.tempo) {\r\n params.tempo = SmoTempoText.deserialize(jsonObj.tempo);\r\n } else {\r\n params.tempo = new SmoTempoText(SmoTempoText.defaults);\r\n }\r\n\r\n // timeSignatureString is now part of timeSignature. upconvert old scores\r\n let timeSignatureString = '';\r\n const jsonLegacy = (jsonObj as any);\r\n if (typeof(jsonLegacy.timeSignatureString) === 'string' && jsonLegacy.timeSignatureString.length > 0) {\r\n timeSignatureString = jsonLegacy.timeSignatureString;\r\n }\r\n if (jsonObj.timeSignature) {\r\n if (timeSignatureString.length) {\r\n jsonObj.timeSignature.displayString = timeSignatureString; \r\n }\r\n params.timeSignature = TimeSignature.deserialize(jsonObj.timeSignature);\r\n } else {\r\n const tparams = TimeSignature.defaults;\r\n if (timeSignatureString.length) {\r\n tparams.displayString = timeSignatureString;\r\n }\r\n params.timeSignature = new TimeSignature(tparams);\r\n }\r\n params.keySignature = jsonObj.keySignature ?? 'C';\r\n params.voices = voices;\r\n params.tuplets = tuplets;\r\n params.modifiers = modifiers;\r\n const rv = new SmoMeasure(params);\r\n // Handle migration for measure-mapped parameters\r\n rv.modifiers.forEach((mod) => {\r\n if (mod.ctor === 'SmoTempoText') {\r\n rv.tempo = (mod as SmoTempoText);\r\n }\r\n });\r\n if (!rv.tempo) {\r\n rv.tempo = new SmoTempoText(SmoTempoText.defaults);\r\n }\r\n return rv;\r\n }\r\n\r\n /**\r\n * When creating a new measure, the 'default' settings can vary depending on\r\n * what comes before/after the measure. This determines the default pitch\r\n * for a clef (appears on 3rd line)\r\n */\r\n static get defaultPitchForClef(): Record {\r\n return {\r\n 'treble': {\r\n letter: 'b',\r\n accidental: 'n',\r\n octave: 4\r\n },\r\n 'bass': {\r\n letter: 'd',\r\n accidental: 'n',\r\n octave: 3\r\n },\r\n 'tenor': {\r\n letter: 'a',\r\n accidental: 'n',\r\n octave: 3\r\n },\r\n 'alto': {\r\n letter: 'c',\r\n accidental: 'n',\r\n octave: 4\r\n },\r\n 'soprano': {\r\n letter: 'b',\r\n accidental: 'n',\r\n octave: 4\r\n },\r\n 'percussion': {\r\n letter: 'b',\r\n accidental: 'n',\r\n octave: 4\r\n },\r\n 'mezzo-soprano': {\r\n letter: 'b',\r\n accidental: 'n',\r\n octave: 4\r\n },\r\n 'baritone-c': {\r\n letter: 'b',\r\n accidental: 'n',\r\n octave: 3\r\n },\r\n 'baritone-f': {\r\n letter: 'e',\r\n accidental: 'n',\r\n octave: 3\r\n },\r\n 'subbass': {\r\n letter: 'd',\r\n accidental: '',\r\n octave: 2\r\n },\r\n 'french': {\r\n letter: 'b',\r\n accidental: '',\r\n octave: 4\r\n } // no idea\r\n };\r\n }\r\n static _emptyMeasureNoteType: NoteType = 'r';\r\n static set emptyMeasureNoteType(tt: NoteType) {\r\n SmoMeasure._emptyMeasureNoteType = tt;\r\n }\r\n static get emptyMeasureNoteType(): NoteType {\r\n return SmoMeasure._emptyMeasureNoteType;\r\n }\r\n static timeSignatureNotes(timeSignature: TimeSignature, clef: Clef) {\r\n const pitch = SmoMeasure.defaultPitchForClef[clef];\r\n const maxTicks = SmoMusic.timeSignatureToTicks(timeSignature.timeSignature);\r\n const noteTick = 8192 / (timeSignature.beatDuration / 2);\r\n let ticks = 0;\r\n const pnotes: SmoNote[] = [];\r\n while (ticks < maxTicks) {\r\n const nextNote = SmoNote.defaults;\r\n nextNote.pitches = [JSON.parse(JSON.stringify(pitch))];\r\n nextNote.noteType = 'r';\r\n nextNote.clef = clef;\r\n nextNote.ticks.numerator = noteTick;\r\n pnotes.push(new SmoNote(nextNote));\r\n ticks += noteTick;\r\n }\r\n if (timeSignature.beatDuration === 8 && (timeSignature.actualBeats % 3 === 0 || timeSignature.actualBeats % 2 !== 0)) {\r\n let ix = 0;\r\n pnotes.forEach((pnote) => {\r\n if ((ix + 1) % 3 === 0) {\r\n pnote.endBeam = true;\r\n }\r\n pnote.beamBeats = 2048 * 3;\r\n ix += 1;\r\n });\r\n }\r\n return pnotes;\r\n }\r\n /**\r\n * Get a measure full of default notes for a given timeSignature/clef.\r\n * returns 8th notes for triple-time meters, etc.\r\n * @param params \r\n * @returns \r\n */\r\n static getDefaultNotes(params: SmoMeasureParams): SmoNote[] {\r\n return SmoMeasure.timeSignatureNotes(new TimeSignature(params.timeSignature), params.clef);\r\n }\r\n\r\n /**\r\n * When creating a new measure, the 'default' settings can vary depending on\r\n * what comes before/after the measure. This determines the defaults from the\r\n * parameters that are passed in, which could be another measure in the score.\r\n * This version returns params with no notes, for callers that want to use their own notes.\r\n * If you want the default notes, see {@link getDefaultMeasureWithNotes}\r\n * \r\n * @param params\r\n * @returns \r\n */\r\n static getDefaultMeasure(params: SmoMeasureParams): SmoMeasure {\r\n const obj: any = {};\r\n smoSerialize.serializedMerge(SmoMeasure.defaultAttributes, SmoMeasure.defaults, obj);\r\n smoSerialize.serializedMerge(SmoMeasure.defaultAttributes, params, obj);\r\n // Don't copy column-formatting options to new measure in new column\r\n smoSerialize.serializedMerge(SmoMeasure.formattingOptions, SmoMeasure.defaults, obj);\r\n obj.timeSignature = new TimeSignature(params.timeSignature);\r\n // The measure expects to get concert KS in constructor and adjust for instrument. So do the\r\n // opposite.\r\n obj.keySignature = SmoMusic.vexKeySigWithOffset(obj.keySignature, -1 * obj.transposeIndex);\r\n // Don't redisplay tempo for a new measure\r\n const rv = new SmoMeasure(obj);\r\n if (rv.tempo && rv.tempo.display) {\r\n rv.tempo.display = false;\r\n }\r\n return rv;\r\n }\r\n\r\n /**\r\n * When creating a new measure, the 'default' settings can vary depending on\r\n * what comes before/after the measure. This determines the defaults from the\r\n * parameters that are passed in, which could be another measure in the score.\r\n * \r\n * @param params \r\n * @returns \r\n */\r\n static getDefaultMeasureWithNotes(params: SmoMeasureParams): SmoMeasure {\r\n var measure = SmoMeasure.getDefaultMeasure(params);\r\n measure.voices.push({\r\n notes: SmoMeasure.getDefaultNotes(params)\r\n });\r\n // fix a bug.\r\n // new measures only have 1 voice, make sure active voice is 0\r\n measure.activeVoice = 0;\r\n return measure;\r\n }\r\n /**\r\n * used by xml export \r\n * @internal\r\n * @param val \r\n */\r\n getForceSystemBreak() {\r\n return this.format.systemBreak;\r\n }\r\n // @internal\r\n setDefaultBarlines() {\r\n if (!this.getStartBarline()) {\r\n this.modifiers.push(new SmoBarline({\r\n position: SmoBarline.positions.start,\r\n barline: SmoBarline.barlines.singleBar\r\n }));\r\n }\r\n if (!this.getEndBarline()) {\r\n this.modifiers.push(new SmoBarline({\r\n position: SmoBarline.positions.end,\r\n barline: SmoBarline.barlines.singleBar\r\n }));\r\n }\r\n }\r\n\r\n get containsSound(): boolean {\r\n let i = 0;\r\n for (i = 0; i < this.voices.length; ++i) {\r\n let j = 0;\r\n const voice = this.voices[i];\r\n for (j = 0; j < this.voices.length; ++j) {\r\n if (voice.notes[j].noteType === 'n') {\r\n return true;\r\n }\r\n }\r\n }\r\n return false;\r\n }\r\n /**\r\n * The rendered width of the measure, or estimate of same\r\n */\r\n get staffWidth() {\r\n return this.svg.staffWidth;\r\n }\r\n\r\n /**\r\n * set the rendered width of the measure, or estimate of same\r\n */\r\n setWidth(width: number, description: string) {\r\n if (layoutDebug.flagSet(layoutDebug.values.measureHistory)) {\r\n this.svg.history.push('setWidth ' + this.staffWidth + '=> ' + width + ' ' + description);\r\n }\r\n if (isNaN(width)) {\r\n throw ('NAN in setWidth');\r\n }\r\n this.svg.staffWidth = width;\r\n }\r\n\r\n /**\r\n * Get rendered or estimated start x\r\n */\r\n get staffX(): number {\r\n return this.svg.staffX;\r\n }\r\n\r\n /**\r\n * Set rendered or estimated start x\r\n */\r\n setX(x: number, description: string) {\r\n if (isNaN(x)) {\r\n throw ('NAN in setX');\r\n }\r\n layoutDebug.measureHistory(this, 'staffX', x, description);\r\n this.svg.staffX = Math.round(x);\r\n }\r\n /**\r\n * A time signature has possibly changed. add/remove notes to\r\n * match the new length\r\n */\r\n alignNotesWithTimeSignature() {\r\n const tsTicks = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature);\r\n if (tsTicks === this.getMaxTicksVoice()) {\r\n return;\r\n }\r\n const replaceNoteWithDuration = (target: number, ar: SmoNote[], note: SmoNote) => {\r\n const fitNote = new SmoNote(SmoNote.defaults);\r\n const duration = SmoMusic.closestDurationTickLtEq(target);\r\n if (duration > 128) {\r\n fitNote.ticks = { numerator: duration, denominator: 1, remainder: 0 };\r\n fitNote.pitches = note.pitches;\r\n fitNote.noteType = note.noteType;\r\n fitNote.clef = note.clef;\r\n ar.push(fitNote);\r\n }\r\n }\r\n const voices: SmoVoice[] = [];\r\n const tuplets: SmoTuplet[] = [];\r\n for (var i = 0; i < this.voices.length; ++i) {\r\n const voice = this.voices[i];\r\n const newNotes: SmoNote[] = [];\r\n let voiceTicks = 0;\r\n for (var j = 0; j < voice.notes.length; ++j) {\r\n const note = voice.notes[j];\r\n // if a tuplet, make sure the whole tuplet fits.\r\n if (note.isTuplet) {\r\n const tuplet = this.getTupletForNote(note);\r\n if (tuplet) {\r\n // remaining notes of an approved tuplet, just add them\r\n if (tuplet.startIndex !== j) {\r\n newNotes.push(note);\r\n continue;\r\n }\r\n else if (tuplet.tickCount + voiceTicks <= tsTicks) {\r\n // first note of the tuplet, it fits, add it\r\n voiceTicks += tuplet.tickCount;\r\n newNotes.push(note);\r\n tuplets.push(tuplet);\r\n } else {\r\n // tuplet will not fit. Make a note as close to remainder as possible and add it\r\n replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note);\r\n voiceTicks = tsTicks;\r\n break;\r\n }\r\n } else { // missing tuplet, now what?\r\n console.warn('missing tuplet info');\r\n replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note);\r\n voiceTicks = tsTicks;\r\n }\r\n } else {\r\n if (note.tickCount + voiceTicks <= tsTicks) {\r\n newNotes.push(note);\r\n voiceTicks += note.tickCount;\r\n } else {\r\n replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note);\r\n voiceTicks = tsTicks;\r\n break;\r\n }\r\n }\r\n }\r\n if (tsTicks - voiceTicks > 128) {\r\n const np = SmoNote.defaults;\r\n np.clef = this.clef;\r\n const nnote = new SmoNote(np);\r\n replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, nnote);\r\n }\r\n voices.push({ notes: newNotes });\r\n }\r\n this.voices = voices;\r\n this.tuplets = tuplets;\r\n }\r\n get measureNumberDbg(): string {\r\n return `${this.measureNumber.measureIndex}/${this.measureNumber.systemIndex}/${this.measureNumber.staffId}`;\r\n }\r\n /**\r\n * Get rendered or estimated start y\r\n */\r\n get staffY(): number {\r\n return this.svg.staffY;\r\n }\r\n\r\n /**\r\n * Set rendered or estimated start y\r\n */\r\n setY(y: number, description: string) {\r\n if (isNaN(y)) {\r\n throw ('NAN in setY');\r\n }\r\n layoutDebug.measureHistory(this, 'staffY', y, description);\r\n this.svg.staffY = Math.round(y);\r\n }\r\n\r\n /**\r\n * Return actual or estimated highest point in score\r\n */\r\n get yTop(): number {\r\n return this.svg.yTop;\r\n }\r\n /**\r\n * return the lowest y (highest value) in this measure svg\r\n *\r\n * @readonly\r\n */\r\n get lowestY(): number {\r\n if (this.svg.tabStaveBox) {\r\n return this.svg.tabStaveBox.y + this.svg.tabStaveBox.height;\r\n } else {\r\n return this.svg.logicalBox.y + this.svg.logicalBox.height;\r\n }\r\n }\r\n /**\r\n * adjust the y for the render boxes to account for the page and margins\r\n */\r\n adjustY(yOffset: number) {\r\n this.svg.logicalBox.y += yOffset;\r\n if (this.svg.tabStaveBox) {\r\n this.svg.tabStaveBox.y += yOffset;\r\n }\r\n }\r\n /**\r\n * WHen setting an instrument, offset the pitches to match the instrument key\r\n * @param offset \r\n * @param newClef \r\n */\r\n transposeToOffset(offset: number, targetKey: string, newClef?: Clef) {\r\n const diff = offset - this.transposeIndex;\r\n this.voices.forEach((voice) => {\r\n voice.notes.forEach((note) => {\r\n const pitches: number[] = [...Array(note.pitches.length).keys()];\r\n // when the note is a rest, preserve the rest but match the new clef.\r\n if (newClef && note.noteType === 'r') {\r\n const defp = JSON.parse(JSON.stringify(SmoMeasure.defaultPitchForClef[newClef]));\r\n note.pitches = [defp];\r\n } else {\r\n note.transpose(pitches, diff, this.keySignature, targetKey);\r\n note.getGraceNotes().forEach((gn) => {\r\n const gpitch: number[] = [...Array(gn.pitches.length).keys()];\r\n const xpose = SmoNote.transpose(gn, gpitch, diff, this.keySignature, targetKey);\r\n gn.pitches = xpose.pitches;\r\n });\r\n }\r\n });\r\n });\r\n }\r\n /**\r\n * Return actual or estimated highest point in score\r\n */\r\n setYTop(y: number, description: string) {\r\n layoutDebug.measureHistory(this, 'yTop', y, description);\r\n this.svg.yTop = y;\r\n }\r\n\r\n /**\r\n * Return actual or estimated bounding box\r\n */\r\n setBox(box: SvgBox, description: string) {\r\n layoutDebug.measureHistory(this, 'logicalBox', box, description);\r\n this.svg.logicalBox = SvgHelpers.smoBox(box);\r\n }\r\n /**\r\n * @returns the DOM identifier for this measure when rendered\r\n */\r\n getClassId() {\r\n return 'mm-' + this.measureNumber.staffId + '-' + this.measureNumber.measureIndex;\r\n }\r\n /**\r\n * \r\n * @param id \r\n * @returns \r\n */\r\n getRenderedNote(id: string) {\r\n let j = 0;\r\n let i = 0;\r\n for (j = 0; j < this.voices.length; ++j) {\r\n const voice = this.voices[j];\r\n for (i = 0; i < voice.notes.length; ++i) {\r\n const note = voice.notes[i];\r\n if (note.renderId === id) {\r\n return {\r\n smoNote: note,\r\n voice: j,\r\n tick: i\r\n };\r\n }\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n getNotes() {\r\n return this.voices[this.activeVoice].notes;\r\n }\r\n\r\n getActiveVoice() {\r\n return this.activeVoice;\r\n }\r\n\r\n setActiveVoice(vix: number) {\r\n if (vix >= 0 && vix < this.voices.length) {\r\n this.activeVoice = vix;\r\n }\r\n }\r\n\r\n tickmapForVoice(voiceIx: number) {\r\n return new TickMap(this, voiceIx);\r\n }\r\n\r\n // ### createMeasureTickmaps\r\n // A tickmap is a map of notes to ticks for the measure. It is speciifc per-voice\r\n // since each voice may have different numbers of ticks. The accidental map is\r\n // overall since accidentals in one voice apply to accidentals in the other\r\n // voices. So we return the tickmaps and the overall accidental map.\r\n createMeasureTickmaps(): MeasureTickmaps {\r\n let i = 0;\r\n const tickmapArray: TickMap[] = [];\r\n const accidentalMap: Record> =\r\n {} as Record>;\r\n for (i = 0; i < this.voices.length; ++i) {\r\n tickmapArray.push(this.tickmapForVoice(i));\r\n }\r\n\r\n for (i = 0; i < this.voices.length; ++i) {\r\n const tickmap: TickMap = tickmapArray[i];\r\n const durationKeys: string[] = Object.keys((tickmap.durationAccidentalMap));\r\n\r\n durationKeys.forEach((durationKey: string) => {\r\n if (!accidentalMap[durationKey]) {\r\n accidentalMap[durationKey] = tickmap.durationAccidentalMap[durationKey];\r\n } else {\r\n const amap = accidentalMap[durationKey];\r\n const tickable: Record = tickmap.durationAccidentalMap[durationKey];\r\n const letterKeys: PitchLetter[] = Object.keys(tickable) as Array;\r\n letterKeys.forEach((pitchKey) => {\r\n if (!amap[pitchKey]) {\r\n amap[pitchKey] = tickmap.durationAccidentalMap[durationKey][pitchKey];\r\n }\r\n });\r\n }\r\n });\r\n }\r\n // duration: duration, pitches: Record\r\n const accidentalArray: AccidentalArray[] = [];\r\n Object.keys(accidentalMap).forEach((durationKey) => {\r\n accidentalArray.push({ duration: durationKey, pitches: accidentalMap[durationKey] });\r\n });\r\n return {\r\n tickmaps: tickmapArray,\r\n accidentalMap,\r\n accidentalArray\r\n };\r\n }\r\n // ### createRestNoteWithDuration\r\n // pad some duration of music with rests.\r\n static createRestNoteWithDuration(duration: number, clef: Clef): SmoNote {\r\n const pitch: Pitch = JSON.parse(JSON.stringify(\r\n SmoMeasure.defaultPitchForClef[clef]));\r\n const note = new SmoNote(SmoNote.defaults);\r\n note.pitches = [pitch];\r\n note.noteType = 'r';\r\n note.hidden = true;\r\n note.ticks = { numerator: duration, denominator: 1, remainder: 0 };\r\n return note;\r\n }\r\n\r\n /**\r\n * Count the number of ticks in each voice and return max\r\n * @returns \r\n */\r\n getMaxTicksVoice() {\r\n let i = 0;\r\n let max = 0;\r\n for (i = 0; i < this.voices.length; ++i) {\r\n const voiceTicks = this.getTicksFromVoice(i);\r\n max = Math.max(voiceTicks, max);\r\n }\r\n return max;\r\n }\r\n\r\n /**\r\n * Count the number of ticks in a specific voice\r\n * @param voiceIndex \r\n * @returns \r\n */\r\n getTicksFromVoice(voiceIndex: number): number {\r\n let ticks = 0;\r\n this.voices[voiceIndex].notes.forEach((note) => {\r\n ticks += note.tickCount;\r\n });\r\n return ticks;\r\n }\r\n\r\n getClosestTickCountIndex(voiceIndex: number, tickCount: number): number {\r\n let i = 0;\r\n let rv = 0;\r\n for (i = 0; i < this.voices[voiceIndex].notes.length; ++i) {\r\n const note = this.voices[voiceIndex].notes[i];\r\n if (note.tickCount + rv > tickCount) {\r\n return rv;\r\n }\r\n rv += note.tickCount;\r\n }\r\n return rv;\r\n }\r\n\r\n isPickup(): boolean {\r\n const ticks = this.getTicksFromVoice(0);\r\n const goal = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature);\r\n return (ticks < goal);\r\n }\r\n\r\n clearBeamGroups() {\r\n this.beamGroups = [];\r\n }\r\n\r\n // ### updateLyricFont\r\n // Update the lyric font, which is the same for all lyrics.\r\n setLyricFont(fontInfo: FontInfo) {\r\n this.voices.forEach((voice) => {\r\n voice.notes.forEach((note) => {\r\n note.setLyricFont(fontInfo);\r\n });\r\n });\r\n }\r\n setLyricAdjustWidth(adjustNoteWidth: boolean) {\r\n this.voices.forEach((voice) => {\r\n voice.notes.forEach((note) => {\r\n note.setLyricAdjustWidth(adjustNoteWidth);\r\n });\r\n });\r\n }\r\n\r\n setChordAdjustWidth(adjustNoteWidth: boolean) {\r\n this.voices.forEach((voice) => {\r\n voice.notes.forEach((note) => {\r\n note.setChordAdjustWidth(adjustNoteWidth);\r\n });\r\n });\r\n }\r\n\r\n // ### updateLyricFont\r\n // Update the lyric font, which is the same for all lyrics.\r\n setChordFont(fontInfo: FontInfo) {\r\n this.voices.forEach((voice) => {\r\n voice.notes.forEach((note) => {\r\n note.setChordFont(fontInfo);\r\n });\r\n });\r\n }\r\n\r\n // ### tuplet methods.\r\n //\r\n // #### tupletNotes\r\n tupletNotes(tuplet: SmoTuplet) {\r\n let j = 0;\r\n let i = 0;\r\n const tnotes = [];\r\n for (j = 0; j < this.voices.length; ++j) {\r\n const vnotes = this.voices[j].notes;\r\n for (i = 0; i < vnotes.length; ++i) {\r\n const note = vnotes[i] as SmoNote;\r\n if (note.tupletId && note.tupletId === tuplet.id) {\r\n tnotes.push(vnotes[i]);\r\n }\r\n }\r\n }\r\n return tnotes;\r\n }\r\n\r\n // #### tupletIndex\r\n // return the index of the given tuplet\r\n tupletIndex(tuplet: SmoTuplet) {\r\n let j = 0;\r\n let i = 0;\r\n for (j = 0; j < this.voices.length; ++j) {\r\n const notes = this.voices[j].notes;\r\n for (i = 0; i < notes.length; ++i) {\r\n const note = notes[i] as SmoNote;\r\n if (note.tupletId && note.tupletId === tuplet.id) {\r\n return i;\r\n }\r\n }\r\n }\r\n return -1;\r\n }\r\n\r\n // #### getTupletForNote\r\n // Finds the tuplet for a given note, or null if there isn't one.\r\n getTupletForNote(note: SmoNote | null): SmoTuplet | null {\r\n let i = 0;\r\n if (!note) {\r\n return null;\r\n }\r\n if (!note.isTuplet) {\r\n return null;\r\n }\r\n for (i = 0; i < this.tuplets.length; ++i) {\r\n const tuplet = this.tuplets[i];\r\n if (typeof(note.tupletId) === 'string' && note.tupletId === tuplet.id) {\r\n return tuplet;\r\n }\r\n }\r\n return null;\r\n }\r\n getNoteById(id: string): SmoNote | null {\r\n for (var i = 0; i < this.voices.length; ++i) {\r\n const voice = this.voices[i];\r\n for (var j = 0; j < voice.notes.length; ++j) {\r\n const note = voice.notes[j];\r\n if (note.attrs.id === id) {\r\n return note;\r\n }\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n removeTupletForNote(note: SmoNote) {\r\n let i = 0;\r\n const tuplets = [];\r\n for (i = 0; i < this.tuplets.length; ++i) {\r\n const tuplet = this.tuplets[i];\r\n if (typeof(note.tupletId) === 'string' && note.tupletId !== tuplet.id) {\r\n tuplets.push(tuplet);\r\n }\r\n }\r\n this.tuplets = tuplets;\r\n }\r\n setClef(clef: Clef) {\r\n const oldClef = this.clef;\r\n this.clef = clef;\r\n this.voices.forEach((voice) => {\r\n voice.notes.forEach((note) => {\r\n note.clef = clef;\r\n });\r\n });\r\n }\r\n /**\r\n * Get the clef that this measure ends with.\r\n * @returns \r\n */\r\n getLastClef() {\r\n for (var i = 0; i < this.voices.length; ++i) {\r\n const voice = this.voices[i];\r\n for (var j = 0; j < voice.notes.length; ++j) {\r\n const note = voice.notes[j];\r\n if (note.clefNote && note.clefNote.clef !== this.clef) {\r\n return note.clefNote.clef;\r\n }\r\n }\r\n }\r\n return this.clef;\r\n }\r\n isRest() {\r\n let i = 0;\r\n for (i = 0; i < this.voices.length; ++i) {\r\n const voice = this.voices[i];\r\n for (var j = 0; j < voice.notes.length; ++j) {\r\n if (!voice.notes[j].isRest()) {\r\n return false;\r\n }\r\n }\r\n }\r\n return true;\r\n }\r\n // ### populateVoice\r\n // Create a new voice in this measure, and populate it with the default note\r\n // for this measure/key/clef\r\n populateVoice(index: number) {\r\n if (index !== this.voices.length) {\r\n return;\r\n }\r\n this.voices.push({ notes: SmoMeasure.getDefaultNotes(this) });\r\n this.activeVoice = index;\r\n }\r\n private _removeSingletonModifier(name: string) {\r\n const ar = this.modifiers.filter(obj => obj.attrs.type !== name);\r\n this.modifiers = ar;\r\n }\r\n\r\n addRehearsalMark(parameters: SmoRehearsalMarkParams) {\r\n this._removeSingletonModifier('SmoRehearsalMark');\r\n this.modifiers.push(new SmoRehearsalMark(parameters));\r\n }\r\n removeRehearsalMark() {\r\n this._removeSingletonModifier('SmoRehearsalMark');\r\n }\r\n getRehearsalMark(): SmoMeasureModifierBase | undefined {\r\n return this.modifiers.find(obj => obj.attrs.type === 'SmoRehearsalMark');\r\n }\r\n getModifiersByType(type: string) {\r\n return this.modifiers.filter((mm) => type === mm.attrs.type);\r\n }\r\n\r\n setTempo(params: SmoTempoTextParams) {\r\n this.tempo = new SmoTempoText(params);\r\n }\r\n /**\r\n * Set measure tempo to the default {@link SmoTempoText}\r\n */\r\n resetTempo() {\r\n this.tempo = new SmoTempoText(SmoTempoText.defaults);\r\n }\r\n getTempo() {\r\n if (typeof (this.tempo) === 'undefined') {\r\n this.tempo = new SmoTempoText(SmoTempoText.defaults);\r\n }\r\n return this.tempo;\r\n }\r\n /**\r\n * Measure text is deprecated, and may not be supported in the future.\r\n * Better to use SmoTextGroup and attach to the measure.\r\n * @param mod \r\n * @returns \r\n */\r\n addMeasureText(mod: SmoMeasureModifierBase) {\r\n var exist = this.modifiers.filter((mm) =>\r\n mm.attrs.id === mod.attrs.id\r\n );\r\n if (exist.length) {\r\n return;\r\n }\r\n this.modifiers.push(mod);\r\n }\r\n\r\n getMeasureText() {\r\n return this.modifiers.filter(obj => obj.ctor === 'SmoMeasureText');\r\n }\r\n\r\n removeMeasureText(id: string) {\r\n var ar = this.modifiers.filter(obj => obj.attrs.id !== id);\r\n this.modifiers = ar;\r\n }\r\n\r\n setRepeatSymbol(rs: SmoRepeatSymbol) {\r\n const ar: SmoMeasureModifierBase[] = [];\r\n let toAdd = true;\r\n const exSymbol = this.getRepeatSymbol();\r\n if (exSymbol && exSymbol.symbol === rs.symbol) {\r\n toAdd = false;\r\n }\r\n this.modifiers.forEach((modifier) => {\r\n if (modifier.ctor !== 'SmoRepeatSymbol') {\r\n ar.push(modifier);\r\n }\r\n });\r\n this.modifiers = ar;\r\n if (toAdd) {\r\n ar.push(rs);\r\n }\r\n }\r\n getRepeatSymbol(): SmoRepeatSymbol | null {\r\n const rv = this.modifiers.filter(obj => obj.ctor === 'SmoRepeatSymbol');\r\n if (rv.length > 0) {\r\n return rv[0] as SmoRepeatSymbol;\r\n }\r\n return null;\r\n }\r\n clearRepeatSymbols() {\r\n const ar: SmoMeasureModifierBase[] = [];\r\n this.modifiers.forEach((modifier) => {\r\n if (modifier.ctor !== 'SmoRepeatSymbol') {\r\n ar.push(modifier);\r\n }\r\n });\r\n this.modifiers = ar;\r\n }\r\n\r\n setBarline(barline: SmoBarline) {\r\n var ar: SmoMeasureModifierBase[] = [];\r\n this.modifiers.forEach((modifier) => {\r\n if (modifier.ctor === 'SmoBarline') {\r\n const o = modifier as SmoBarline;\r\n if (o.position !== barline.position) {\r\n ar.push(o);\r\n }\r\n } else {\r\n ar.push(modifier);\r\n }\r\n });\r\n this.modifiers = ar;\r\n ar.push(barline);\r\n }\r\n\r\n private _getBarline(pos: number): SmoBarline {\r\n let rv = null;\r\n this.modifiers.forEach((modifier) => {\r\n if (modifier.ctor === 'SmoBarline' && (modifier as SmoBarline).position === pos) {\r\n rv = modifier;\r\n }\r\n });\r\n if (rv === null) {\r\n return new SmoBarline(SmoBarline.defaults);\r\n }\r\n return rv;\r\n }\r\n\r\n getEndBarline(): SmoBarline { \r\n return this._getBarline(SmoBarline.positions.end);\r\n }\r\n getStartBarline(): SmoBarline {\r\n return this._getBarline(SmoBarline.positions.start);\r\n }\r\n\r\n addNthEnding(ending: SmoVolta) {\r\n const mods = [];\r\n this.modifiers.forEach((modifier) => {\r\n if (modifier.ctor !== 'SmoVolta' || (modifier as SmoVolta).startBar !== ending.startBar ||\r\n (modifier as SmoVolta).endBar !== ending.endBar) {\r\n mods.push(modifier);\r\n }\r\n });\r\n mods.push(ending);\r\n this.modifiers = mods;\r\n }\r\n\r\n removeNthEnding(ending: SmoVolta) {\r\n const mods: SmoMeasureModifierBase[] = [];\r\n this.modifiers.forEach((modifier) => {\r\n if (modifier.ctor === 'SmoVolta') {\r\n const volta = modifier as SmoVolta;\r\n if (ending.startSelector === null || ending.endSelector === null || volta.startSelector === null || volta.endSelector === null) {\r\n return;\r\n }\r\n if (!SmoSelector.sameMeasure(ending.startSelector, volta.startSelector) || !SmoSelector.sameMeasure(ending.endSelector, volta.endSelector)\r\n && ending.number !== volta.number) {\r\n mods.push(modifier);\r\n }\r\n } else {\r\n mods.push(modifier);\r\n }\r\n });\r\n this.modifiers = mods;\r\n }\r\n\r\n getNthEndings(): SmoVolta[] {\r\n const rv: SmoVolta[] = [];\r\n this.modifiers.forEach((modifier: SmoMeasureModifierBase) => {\r\n if (modifier.ctor === 'SmoVolta') {\r\n rv.push(modifier as SmoVolta);\r\n }\r\n });\r\n return rv;\r\n }\r\n setKeySignature(sig: string) {\r\n this.keySignature = sig;\r\n this.voices.forEach((voice) => {\r\n voice.notes.forEach((note) => {\r\n note.keySignature = sig;\r\n });\r\n });\r\n }\r\n setMeasureNumber(num: MeasureNumber) {\r\n this.measureNumber = num;\r\n }\r\n getBeamGroupForNote(note: SmoNote) {\r\n let i = 0;\r\n let j = 0;\r\n for (i = 0; i < this.beamGroups.length; ++i) {\r\n const bg = this.beamGroups[i];\r\n for (j = 0; j < bg.notes.length; ++j) {\r\n if (bg.notes[j].attrs.id === note.attrs.id) {\r\n return bg;\r\n }\r\n }\r\n }\r\n return null;\r\n }\r\n}\r\n","// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)\r\n// Copyright (c) Aaron David Newman 2021.\r\n/**\r\n * @module /smo/data/measureModifiers\r\n * **/\r\nimport { smoSerialize } from '../../common/serializationHelpers';\r\nimport { SmoMusic } from './music';\r\nimport { SmoAttrs, MeasureNumber, SmoObjectParams, SvgBox, SmoModifierBase, getId } from './common';\r\nimport { SmoSelector } from '../xform/selections';\r\nimport { FontInfo } from '../../common/vex';\r\n\r\n/**\r\n * Measure modifiers are attached to the measure itself. Each instance has a\r\n * `serialize()` method and a `ctor` attribute for deserialization.\r\n * @category SmoModifier\r\n */\r\nexport abstract class SmoMeasureModifierBase implements SmoModifierBase {\r\n attrs: SmoAttrs;\r\n ctor: string;\r\n logicalBox: SvgBox | null = null;\r\n constructor(ctor: string) {\r\n this.ctor = ctor;\r\n this.attrs = {\r\n id: getId().toString(),\r\n type: ctor\r\n };\r\n }\r\n static deserialize(jsonObj: SmoObjectParams) {\r\n const ctor = eval('globalThis.Smo.' + jsonObj.ctor);\r\n const rv = new ctor(jsonObj);\r\n return rv;\r\n }\r\n abstract serialize(): any;\r\n}\r\n\r\nexport type SmoMeasureFormatNumberAttributes = 'customStretch' | 'proportionality' | 'padLeft' | 'measureIndex';\r\nexport const SmoMeasureFormatNumberKeys: SmoMeasureFormatNumberAttributes[] =\r\n ['customStretch', 'proportionality', 'padLeft', 'measureIndex'];\r\nexport type SmoMeasueFormatBooleanAttributes = 'autoJustify' | 'systemBreak' | 'skipMeasureCount' | 'pageBreak' | 'padAllInSystem' | 'restBreak' | 'forceRest';\r\nexport const SmoMeasureFormatBooleanKeys: SmoMeasueFormatBooleanAttributes[] = ['autoJustify','skipMeasureCount', 'systemBreak', 'pageBreak', 'padAllInSystem', 'restBreak', 'forceRest'];\r\n/**\r\n * Constructor parameter for measure formatting object\r\n */\r\nexport interface SmoMeasureFormatParams {\r\n /**\r\n * additional pixels to a measure (plus or minus)\r\n */\r\n customStretch: number | null,\r\n /**\r\n * softmax factor, controls how tightly rhythms are formatted\r\n */\r\n proportionality: number | null,\r\n /**\r\n * break justification for this column\r\n */\r\n autoJustify: boolean | null,\r\n /**\r\n * create a new system before this measure\r\n */\r\n systemBreak: boolean | null,\r\n /**\r\n * create a new system before this page\r\n */\r\n pageBreak: boolean | null,\r\n /**\r\n * force a break in multi-measure rest\r\n */\r\n restBreak: boolean | null,\r\n /**\r\n * treat this measure like a whole rest\r\n */\r\n forceRest: boolean | null,\r\n /**\r\n * if score is grouping measures per system, skip this measure in the count\r\n * (used for short measures, or pickups)\r\n */\r\n skipMeasureCount: boolean | null,\r\n /**\r\n * pad left, e.g. for the first stave in a system\r\n */\r\n padLeft: number | null,\r\n /**\r\n * if padding left, pad all the measures in the column\r\n */\r\n padAllInSystem: boolean | null,\r\n /**\r\n * renumber measures\r\n */\r\n measureIndex: number | null,\r\n}\r\n/**\r\n * Serialization for measure formatting customization, like system break\r\n * @category serialization\r\n */\r\nexport interface SmoMeasureFormatParamsSer extends SmoMeasureFormatParams{\r\n /**\r\n * class name for deserialization\r\n */\r\n ctor: string\r\n }\r\n function isSmoMeasureParamsSer(params: Partial):params is SmoMeasureFormatParamsSer {\r\n return typeof(params.ctor) === 'string';\r\n }\r\n/**\r\n * ISmoMeasureFormatMgr is the DI interface to the\r\n * format manager. Measure formats are often the same to multiple measures\r\n * so we don't serialize each one - instead we map them with this interface\r\n */\r\nexport interface ISmoMeasureFormatMgr {\r\n format: SmoMeasureFormatParams,\r\n measureNumber: MeasureNumber\r\n}\r\n/**\r\n * Measure format holds parameters about the automatic formatting of the measure itself, such as the witch and\r\n * how the durations are proportioned. Note that measure formatting is also controlled by the justification\r\n * between voices and staves. For instance, 2 measures in different staves will have to have the same width\r\n * @category SmoModifier\r\n */\r\nexport class SmoMeasureFormat extends SmoMeasureModifierBase implements SmoMeasureFormatParams {\r\n static get attributes() {\r\n return ['customStretch', 'proportionality', 'autoJustify', 'systemBreak', 'pageBreak', \r\n 'padLeft', 'measureIndex', 'padAllInSystem', 'skipMeasureCount', 'restBreak', 'forceRest'];\r\n }\r\n static get formatAttributes() {\r\n return ['customStretch', 'skipMeasureCount', 'proportionality', 'autoJustify', 'systemBreak', 'pageBreak', 'padLeft'];\r\n }\r\n static get defaultProportionality() {\r\n return 0;\r\n }\r\n static get legacyProportionality() {\r\n return 0;\r\n }\r\n static fromLegacyMeasure(measure: any) {\r\n const o: any = {};\r\n SmoMeasureFormat.formatAttributes.forEach((attr: string | number) => {\r\n if (typeof (measure[attr]) !== 'undefined') {\r\n o[attr] = measure[attr];\r\n } else {\r\n const rhs = (SmoMeasureFormat.defaults as any)[attr];\r\n o[attr] = rhs;\r\n }\r\n o.measureIndex = measure.measureNumber.measureIndex;\r\n });\r\n return new SmoMeasureFormat(o);\r\n }\r\n static get defaults(): SmoMeasureFormatParams {\r\n return JSON.parse(JSON.stringify({\r\n customStretch: 0,\r\n proportionality: SmoMeasureFormat.defaultProportionality,\r\n systemBreak: false,\r\n pageBreak: false,\r\n restBreak: false,\r\n forceRest: false,\r\n padLeft: 0,\r\n padAllInSystem: true,\r\n skipMeasureCount: false,\r\n autoJustify: true,\r\n measureIndex: 0,\r\n }));\r\n }\r\n customStretch: number = SmoMeasureFormat.defaultProportionality;\r\n proportionality: number = 0;\r\n systemBreak: boolean = false;\r\n pageBreak: boolean = false;\r\n restBreak: boolean = false;\r\n skipMeasureCount: boolean = false;\r\n forceRest: boolean = false;\r\n padLeft: number = 0;\r\n padAllInSystem: boolean = true;\r\n autoJustify: boolean = true;\r\n measureIndex: number = 0;\r\n eq(o: SmoMeasureFormatParams) {\r\n let rv = true;\r\n SmoMeasureFormatBooleanKeys.forEach((attr) => {\r\n if (o[attr] !== this[attr]) {\r\n rv = false;\r\n }\r\n });\r\n SmoMeasureFormatNumberKeys.forEach((attr) => {\r\n if (o[attr] !== this[attr] && attr !== 'measureIndex') {\r\n rv = false;\r\n }\r\n });\r\n return rv;\r\n }\r\n get isDefault() {\r\n return this.eq(SmoMeasureFormat.defaults);\r\n }\r\n constructor(parameters: SmoMeasureFormatParams) {\r\n super('SmoMeasureFormat');\r\n const def = SmoMeasureFormat.defaults;\r\n SmoMeasureFormatNumberKeys.forEach((param) => {\r\n this[param] = parameters[param] ? parameters[param] : (def as any)[param];\r\n });\r\n SmoMeasureFormatBooleanKeys.forEach((param) => {\r\n this[param] = parameters[param] ? parameters[param] : (def as any)[param];\r\n });\r\n }\r\n formatMeasure(mm: ISmoMeasureFormatMgr) {\r\n mm.format = new SmoMeasureFormat(this);\r\n mm.format.measureIndex = mm.measureNumber.measureIndex;\r\n }\r\n serialize(): SmoMeasureFormatParamsSer {\r\n const params: Partial = { ctor: 'SmoMeasureFormat' };\r\n smoSerialize.serializedMergeNonDefault(SmoMeasureFormat.defaults, SmoMeasureFormat.attributes, this, params);\r\n if (!isSmoMeasureParamsSer(params)) {\r\n throw('bad type SmoMeasureFormatParamsSer');\r\n }\r\n return params;\r\n }\r\n}\r\n/**\r\n * Used to create a {@link SmoBarline}\r\n */\r\nexport interface SmoBarlineParams {\r\n position: number | null,\r\n barline: number | null\r\n}\r\n\r\nexport interface SmoBarlineParamsSer extends SmoBarlineParams {\r\n ctor: string,\r\n position: number | null,\r\n barline: number | null\r\n}\r\n/**\r\n * Barline is just that, there is a start and end in each measure, which defaults to 'single'.\r\n * @category SmoModifier\r\n */\r\nexport class SmoBarline extends SmoMeasureModifierBase {\r\n static readonly positions: Record = {\r\n start: 0,\r\n end: 1\r\n };\r\n\r\n static readonly barlines: Record = {\r\n singleBar: 0,\r\n doubleBar: 1,\r\n endBar: 2,\r\n startRepeat: 3,\r\n endRepeat: 4,\r\n noBar: 5\r\n }\r\n\r\n static get _barlineToString() {\r\n return ['singleBar', 'doubleBar', 'endBar', 'startRepeat', 'endRepeat', 'noBar'];\r\n }\r\n static barlineString(inst: SmoBarline) {\r\n return SmoBarline._barlineToString[inst.barline];\r\n }\r\n\r\n static get defaults(): SmoBarlineParams {\r\n return JSON.parse(JSON.stringify({\r\n position: SmoBarline.positions.end,\r\n barline: SmoBarline.barlines.singleBar\r\n }));\r\n }\r\n\r\n static get attributes() {\r\n return ['position', 'barline'];\r\n }\r\n serialize(): SmoBarlineParamsSer {\r\n const params: any = {};\r\n smoSerialize.serializedMergeNonDefault(SmoBarline.defaults, SmoBarline.attributes, this, params);\r\n params.ctor = 'SmoBarline';\r\n return params;\r\n }\r\n constructor(parameters: SmoBarlineParams | null) {\r\n super('SmoBarline');\r\n let ops = parameters as any;\r\n if (typeof (parameters) === 'undefined' || parameters === null) {\r\n ops = {};\r\n }\r\n smoSerialize.serializedMerge(SmoBarline.attributes, SmoBarline.defaults, this);\r\n smoSerialize.serializedMerge(SmoBarline.attributes, ops, this);\r\n }\r\n barline: number = SmoBarline.barlines.singleBar;\r\n position: number = SmoBarline.positions.start;\r\n}\r\n\r\n/**\r\n * Constructor for SmoRepeatSymbol\r\n */\r\nexport interface SmoRepeatSymbolParams {\r\n /**\r\n * The symbol enumeration\r\n */\r\n symbol: number,\r\n /**\r\n * x offset for DC, sign etc.\r\n */\r\n xOffset: number,\r\n /**\r\n * y offset for DC, sign etc.\r\n */\r\n yOffset: number,\r\n /**\r\n * position, above or below\r\n */\r\n position: number\r\n}\r\n\r\n/**\r\n * @category serialization\r\n */\r\nexport interface SmoRepeatSymbolParamsSer extends SmoRepeatSymbolParams {\r\n /**\r\n * constructor\r\n */\r\n ctor: string\r\n}\r\nfunction isSmoRepeatSymbolParamsSer(params: Partial