UNPKG

smoosic

Version:

<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i

900 lines (857 loc) 31.8 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. import { SuiMapper, SuiRendererBase } from './mapper'; import { SvgHelpers, StrokeInfo, OutlineInfo } from './svgHelpers'; import { SmoSelection, SmoSelector, ModifierTab } from '../../smo/xform/selections'; import { smoSerialize } from '../../common/serializationHelpers'; import { SuiOscillator } from '../audio/oscillator'; import { SmoScore } from '../../smo/data/score'; import { SvgBox, KeyEvent, defaultKeyEvent, keyHandler } from '../../smo/data/common'; import { SuiScroller } from './scroller'; import { PasteBuffer } from '../../smo/xform/copypaste'; import { SmoNote } from '../../smo/data/note'; import { SmoMeasure } from '../../smo/data/measure'; import { layoutDebug } from './layoutDebug'; declare var $: any; export interface TrackerKeyHandler { moveHome : keyHandler, moveEnd : keyHandler, moveSelectionRight : keyHandler, moveSelectionLeft : keyHandler, moveSelectionUp : keyHandler, moveSelectionDown : keyHandler, moveSelectionRightMeasure : keyHandler, moveSelectionLeftMeasure : keyHandler, advanceModifierSelection : keyHandler, growSelectionRight : keyHandler, growSelectionLeft : keyHandler, moveSelectionPitchUp : keyHandler, moveSelectionPitchDown: keyHandler } /** * SuiTracker * A tracker maps the UI elements to the logical elements ,and allows the user to * move through the score and make selections, for navigation and editing. * @category SuiRender */ export class SuiTracker extends SuiMapper implements TrackerKeyHandler { idleTimer: number = Date.now(); musicCursorGlyph: SVGSVGElement | null = null; deferPlayAdvance: boolean = false; static get strokes(): Record<string, StrokeInfo> { return { suggestion: { strokeName: 'suggestion', stroke: '#fc9', strokeWidth: 3, strokeDasharray: '4,1', fill: 'none', opacity: 1.0 }, selection: { strokeName: 'selection', stroke: '#99d', strokeWidth: 3, strokeDasharray: 2, fill: 'none', opacity: 1.0 }, staffModifier: { strokeName: 'staffModifier', stroke: '#933', strokeWidth: 3, fill: 'none', strokeDasharray: 0, opacity: 1.0 }, pitchSelection: { strokeName: 'pitchSelection', stroke: '#933', strokeWidth: 3, fill: 'none', strokeDasharray: 0, opacity: 1.0 } }; } constructor(renderer: SuiRendererBase, scroller: SuiScroller) { super(renderer, scroller); } // ### renderElement // the element the score is rendered on get renderElement(): Element { return this.renderer.renderElement; } get score(): SmoScore | null { return this.renderer.score; } getIdleTime(): number { return this.idleTimer; } playSelection(artifact: SmoSelection) { if (!this.deferPlayAdvance && this.score) { SuiOscillator.playSelectionNow(artifact, this.score, 1); } else { this.deferPlayAdvance = false; } } deferNextAutoPlay() { if (this.score) { // don't play on advance if we've just added a note and played it because they overlap if (this.score.preferences.autoAdvance && this.score.preferences.autoPlay) { this.deferPlayAdvance = true; } } } getSelectedModifier() { if (this.modifierSelections.length) { return this.modifierSelections[0]; } return null; } getSelectedModifiers() { return this.modifierSelections; } static serializeEvent(evKey: KeyEvent | null): any { if (!evKey) { return []; } const rv = {}; smoSerialize.serializedMerge(['type', 'shiftKey', 'ctrlKey', 'altKey', 'key', 'keyCode'], evKey, rv); return rv; } advanceModifierSelection(keyEv?: KeyEvent) { if (!keyEv) { return; } this.idleTimer = Date.now(); const offset = keyEv.key === 'ArrowLeft' ? -1 : 1; this.modifierIndex = this.modifierIndex + offset; this.modifierIndex = (this.modifierIndex === -2 && this.localModifiers.length) ? this.localModifiers.length - 1 : this.modifierIndex; if (this.modifierIndex >= this.localModifiers.length || this.modifierIndex < 0) { this.modifierIndex = -1; this.modifierSelections = []; $('.vf-staffModifier').remove(); return; } const local: ModifierTab = this.localModifiers[this.modifierIndex]; const box: SvgBox = SvgHelpers.smoBox(local.box) as SvgBox; this.modifierSelections = [{ index: 0, box, modifier: local.modifier, selection: local.selection }]; this._highlightModifier(); } static stringifyBox(box: SvgBox): string { return '{x:' + box.x + ',y:' + box.y + ',width:' + box.width + ',height:' + box.height + '}'; } // ### _getOffsetSelection // Get the selector that is the offset of the first existing selection _getOffsetSelection(offset: number): SmoSelector { if (!this.score) { return SmoSelector.default; } let testSelection = this.getExtremeSelection(Math.sign(offset)); const scopyTick = JSON.parse(JSON.stringify(testSelection.selector)); const scopyMeasure = JSON.parse(JSON.stringify(testSelection.selector)); scopyTick.tick += offset; scopyMeasure.measure += offset; const targetMeasure = SmoSelection.measureSelection(this.score, testSelection.selector.staff, scopyMeasure.measure); if (targetMeasure && targetMeasure.measure && targetMeasure.measure.voices.length <= scopyMeasure.voice) { scopyMeasure.voice = 0; } if (targetMeasure && targetMeasure.measure) { scopyMeasure.tick = (offset < 0) ? targetMeasure.measure.voices[scopyMeasure.voice].notes.length - 1 : 0; } if (testSelection.measure.voices.length > scopyTick.voice && testSelection.measure.voices[scopyTick.voice].notes.length > scopyTick.tick && scopyTick.tick >= 0) { if (testSelection.selector.voice !== testSelection.measure.getActiveVoice()) { scopyTick.voice = testSelection.measure.getActiveVoice(); testSelection = this._getClosestTick(scopyTick); return testSelection.selector; } return scopyTick; } else if (targetMeasure && scopyMeasure.measure < testSelection.staff.measures.length && scopyMeasure.measure >= 0) { return scopyMeasure; } return testSelection.selector; } getSelectedGraceNotes(): ModifierTab[] { if (!this.modifierSelections.length) { return []; } const ff = this.modifierSelections.filter((mm) => mm.modifier?.attrs?.type === 'SmoGraceNote' ); return ff; } isGraceNoteSelected(): boolean { if (this.modifierSelections.length) { const ff = this.modifierSelections.findIndex((mm) => mm.modifier.attrs.type === 'SmoGraceNote'); return ff >= 0; } return false; } _growGraceNoteSelections(offset: number) { this.idleTimer = Date.now(); const far = this.modifierSelections.filter((mm) => mm.modifier.attrs.type === 'SmoGraceNote'); if (!far.length) { return; } const ix = (offset < 0) ? 0 : far.length - 1; const sel: ModifierTab = far[ix] as ModifierTab; const left = this.localModifiers.filter((mt) => mt.modifier?.attrs?.type === 'SmoGraceNote' && sel.selection && mt.selection && SmoSelector.sameNote(mt.selection.selector, sel.selection.selector) ); if (ix + offset < 0 || ix + offset >= left.length) { return; } const leftSel = left[ix + offset]; if (!leftSel) { console.warn('bad selector in _growGraceNoteSelections'); } leftSel.box = leftSel.box ?? SvgBox.default; this.modifierSelections.push(leftSel); this._highlightModifier(); } get autoPlay(): boolean { return this.renderer.score ? this.renderer.score.preferences.autoPlay : false; } growSelectionRight() { this._growSelectionRight(false); } _growSelectionRight(skipPlay: boolean): number { this.idleTimer = Date.now(); if (this.isGraceNoteSelected()) { this._growGraceNoteSelections(1); return 0; } const nselect = this._getOffsetSelection(1); // already selected const artifact = this._getClosestTick(nselect); if (!artifact) { return 0; } if (this.selections.find((sel) => SmoSelector.sameNote(sel.selector, artifact.selector))) { return 0; } if (!this.mapping && this.autoPlay && skipPlay === false && this.score) { this.playSelection(artifact); } this.selections.push(artifact); this.deferHighlight(); this._createLocalModifiersList(); return (artifact.note as SmoNote).tickCount; } moveHome(keyEvent?: KeyEvent) { const evKey = keyEvent ?? defaultKeyEvent(); this.idleTimer = Date.now(); const ls = this.selections[0].staff; if (evKey.ctrlKey) { const mm = ls.measures[0]; const homeSel = this._getClosestTick({ staff: ls.staffId, measure: 0, voice: mm.getActiveVoice(), tick: 0, pitches: [] }); if (evKey.shiftKey) { this._selectBetweenSelections(this.selections[0], homeSel); } else { this.selections = [homeSel]; this.deferHighlight(); this._createLocalModifiersList(); if (homeSel.measure.svg.logicalBox) { this.scroller.scrollVisibleBox(homeSel.measure.svg.logicalBox); } } } else { const system = this.selections[0].measure.svg.lineIndex; const lm = ls.measures.find((mm) => mm.svg.lineIndex === system && mm.measureNumber.systemIndex === 0); const mm = lm as SmoMeasure; const homeSel = this._getClosestTick({ staff: ls.staffId, measure: mm.measureNumber.measureIndex, voice: mm.getActiveVoice(), tick: 0, pitches: [] }); if (evKey.shiftKey) { this._selectBetweenSelections(this.selections[0], homeSel); } else if (homeSel?.measure?.svg?.logicalBox) { this.selections = [homeSel]; this.scroller.scrollVisibleBox(homeSel.measure.svg.logicalBox); this.deferHighlight(); this._createLocalModifiersList(); } } } moveEnd(keyEvent?: KeyEvent) { this.idleTimer = Date.now(); const ls = this.selections[0].staff; const evKey = keyEvent ?? defaultKeyEvent(); if (evKey.ctrlKey) { const lm = ls.measures[ls.measures.length - 1]; const voiceIx = lm.getActiveVoice(); const voice = lm.voices[voiceIx]; const endSel = this._getClosestTick({ staff: ls.staffId, measure: ls.measures.length - 1, voice: voiceIx, tick: voice.notes.length - 1, pitches: [] }); if (evKey.shiftKey) { this._selectBetweenSelections(this.selections[0], endSel); } else { this.selections = [endSel]; this.deferHighlight(); this._createLocalModifiersList(); if (endSel.measure.svg.logicalBox) { this.scroller.scrollVisibleBox(endSel.measure.svg.logicalBox); } } } else { const system = this.selections[0].measure.svg.lineIndex; // find the largest measure index on this staff in this system const measures = ls.measures.filter((mm) => mm.svg.lineIndex === system); const lm = measures.reduce((a, b) => b.measureNumber.measureIndex > a.measureNumber.measureIndex ? b : a); const ticks = lm.voices[lm.getActiveVoice()].notes.length; const endSel = this._getClosestTick({ staff: ls.staffId, measure: lm.measureNumber.measureIndex, voice: lm.getActiveVoice(), tick: ticks - 1, pitches: [] }); if (evKey.shiftKey) { this._selectBetweenSelections(this.selections[0], endSel); } else { this.selections = [endSel]; this.deferHighlight(); this._createLocalModifiersList(); if (endSel.measure.svg.logicalBox) { this.scroller.scrollVisibleBox(endSel.measure.svg.logicalBox); } } } } growSelectionRightMeasure() { let toSelect = 0; const rightmost = this.getExtremeSelection(1); const ticksLeft = rightmost.measure.voices[rightmost.measure.activeVoice] .notes.length - rightmost.selector.tick; if (ticksLeft === 0) { if (rightmost.selector.measure < rightmost.staff.measures.length) { const mix = rightmost.selector.measure + 1; rightmost.staff.measures[mix].setActiveVoice(rightmost.selector.voice); toSelect = rightmost.staff.measures[mix] .voices[rightmost.staff.measures[mix].activeVoice].notes.length; } } else { toSelect = ticksLeft; } while (toSelect > 0) { this._growSelectionRight(true); toSelect -= 1; } } growSelectionLeft(): number { if (this.isGraceNoteSelected()) { this._growGraceNoteSelections(-1); return 0; } this.idleTimer = Date.now(); const nselect = this._getOffsetSelection(-1); // already selected const artifact = this._getClosestTick(nselect); if (!artifact) { return 0; } if (this.selections.find((sel) => SmoSelector.sameNote(sel.selector, artifact.selector))) { return 0; } artifact.measure.setActiveVoice(nselect.voice); this.selections.push(artifact); if (this.autoPlay && this.score) { this.playSelection(artifact); } this.deferHighlight(); this._createLocalModifiersList(); return (artifact.note as SmoNote).tickCount; } // if we are being moved right programmatically, avoid playing the selected note. moveSelectionRight() { if (this.selections.length === 0 || this.score === null) { return; } const skipPlay = !this.score.preferences.autoPlay; // const original = JSON.parse(JSON.stringify(this.getExtremeSelection(-1).selector)); const nselect = this._getOffsetSelection(1); // skip any measures that are not displayed due to rest or repetition const mselect = SmoSelection.measureSelection(this.score, nselect.staff, nselect.measure); if (mselect?.measure.svg.multimeasureLength) { nselect.measure += mselect?.measure.svg.multimeasureLength; } if (mselect) { mselect.measure.setActiveVoice(nselect.voice); } this._replaceSelection(nselect, skipPlay); } moveSelectionLeft() { if (this.selections.length === 0 || this.score === null) { return; } const nselect = this._getOffsetSelection(-1); // Skip multimeasure rests in parts const mselect = SmoSelection.measureSelection(this.score, nselect.staff, nselect.measure); while (nselect.measure > 0 && mselect && (mselect.measure.svg.hideMultimeasure || mselect.measure.svg.multimeasureLength > 0)) { nselect.measure -= 1; } if (mselect) { mselect.measure.setActiveVoice(nselect.voice); } this._replaceSelection(nselect, false); } moveSelectionLeftMeasure() { this._moveSelectionMeasure(-1); } moveSelectionRightMeasure() { this._moveSelectionMeasure(1); } _moveSelectionMeasure(offset: number) { const selection = this.getExtremeSelection(Math.sign(offset)); this.idleTimer = Date.now(); const selector = JSON.parse(JSON.stringify(selection.selector)); selector.measure += offset; selector.tick = 0; const selObj = this._getClosestTick(selector); if (selObj) { this.selections = [selObj]; } this.deferHighlight(); this._createLocalModifiersList(); } _moveStaffOffset(offset: number) { if (this.selections.length === 0 || this.score === null) { return; } this.idleTimer = Date.now(); const nselector = JSON.parse(JSON.stringify(this.selections[0].selector)); nselector.staff = this.score.incrementActiveStaff(offset); this.selections = [this._getClosestTick(nselector)]; this.deferHighlight(); this._createLocalModifiersList(); } removePitchSelection() { if (this.outlines['pitchSelection']) { if (this.outlines['pitchSelection'].element) { this.outlines['pitchSelection'].element.remove(); } delete this.outlines['pitchSelection']; } } // ### _moveSelectionPitch // Suggest a specific pitch in a chord, so we can transpose just the one note vs. the whole chord. _moveSelectionPitch(index: number) { this.idleTimer = Date.now(); if (!this.selections.length) { return; } const sel = this.selections[0]; const note = sel.note as SmoNote; if (note.pitches.length < 2) { this.pitchIndex = -1; this.removePitchSelection(); return; } this.pitchIndex = (this.pitchIndex + index) % note.pitches.length; sel.selector.pitches = []; sel.selector.pitches.push(this.pitchIndex); this._highlightPitchSelection(note, this.pitchIndex); } moveSelectionPitchUp() { this._moveSelectionPitch(1); } moveSelectionPitchDown() { if (!this.selections.length) { return; } this._moveSelectionPitch((this.selections[0].note as SmoNote).pitches.length - 1); } moveSelectionUp() { this._moveStaffOffset(-1); } moveSelectionDown() { this._moveStaffOffset(1); } containsArtifact(): boolean { return this.selections.length > 0; } _replaceSelection(nselector: SmoSelector, skipPlay: boolean) { if (this.score === null) { return; } var artifact = SmoSelection.noteSelection(this.score, nselector.staff, nselector.measure, nselector.voice, nselector.tick); if (!artifact) { artifact = SmoSelection.noteSelection(this.score, nselector.staff, nselector.measure, 0, nselector.tick); } if (!artifact) { artifact = SmoSelection.noteSelection(this.score, nselector.staff, nselector.measure, 0, 0); } if (!artifact) { // disappeared - default to start artifact = SmoSelection.noteSelection(this.score, 0, 0, 0, 0); } if (!skipPlay && this.autoPlay && artifact) { this.playSelection(artifact); } if (!artifact) { return; } artifact.measure.setActiveVoice(nselector.voice); // clear modifier selections this.clearModifierSelections(); this.score.setActiveStaff(nselector.staff); const mapKey = Object.keys(this.measureNoteMap).find((k) => artifact && SmoSelector.sameNote(this.measureNoteMap[k].selector, artifact.selector) ); if (!mapKey) { return; } const mapped = this.measureNoteMap[mapKey]; // If this is a new selection, remove pitch-specific and replace with note-specific if (!nselector.pitches || nselector.pitches.length === 0) { this.pitchIndex = -1; } this.selections = [mapped]; this.deferHighlight(); this._createLocalModifiersList(); } getFirstMeasureOfSelection() { if (this.selections.length) { return this.selections[0].measure; } return null; } // ## measureIterator // Description: iterate over the any measures that are part of the selection getSelectedMeasures(): SmoSelection[] { const set: number[] = []; const rv: SmoSelection[] = []; if (!this.score) { return []; } this.selections.forEach((sel) => { const measure = SmoSelection.measureSelection(this.score!, sel.selector.staff, sel.selector.measure); if (measure) { const ix = measure.selector.measure; if (set.indexOf(ix) === -1) { set.push(ix); rv.push(measure); } } }); return rv; } _addSelection(selection: SmoSelection) { const ar: SmoSelection[] = this.selections.filter((sel) => SmoSelector.neq(sel.selector, selection.selector) ); if (this.autoPlay && this.score) { this.playSelection(selection); } ar.push(selection); this.selections = ar; } _selectFromToInStaff(score: SmoScore, sel1: SmoSelection, sel2: SmoSelection) { const selections = SmoSelection.innerSelections(score, sel1.selector, sel2.selector); /* .filter((ff) => ff.selector.voice === sel1.measure.activeVoice ); */ this.selections = []; // Get the actual selections from our map, since the client bounding boxes are already computed selections.forEach((sel) => { const key = SmoSelector.getNoteKey(sel.selector); sel.measure.setActiveVoice(sel.selector.voice); // Skip measures that are not rendered because they are part of a multi-rest if (this.measureNoteMap && this.measureNoteMap[key]) { this.selections.push(this.measureNoteMap[key]); } }); if (this.selections.length === 0) { this.selections = [sel1]; } this.idleTimer = Date.now(); } _selectBetweenSelections(s1: SmoSelection, s2: SmoSelection) { const score = this.renderer.score ?? null; if (!score) { return; } const min = SmoSelector.gt(s1.selector, s2.selector) ? s2 : s1; const max = SmoSelector.lt(min.selector, s2.selector) ? s2 : s1; this._selectFromToInStaff(score, min, max); this._createLocalModifiersList(); this.highlightQueue.selectionCount = this.selections.length; this.deferHighlight(); } selectSuggestion(ev: KeyEvent) { if (!this.suggestion || !this.suggestion.measure || this.score === null) { return; } this.idleTimer = Date.now(); if (this.modifierSuggestion) { this.modifierIndex = -1; this.modifierSelections = [this.modifierSuggestion]; this.modifierSuggestion = null; this.createLocalModifiersFromModifierTabs(this.modifierSelections); // If we selected due to a mouse click, move the selection to the // selected modifier this._highlightModifier(); return; } else if (ev.type === 'click') { this.clearModifierSelections(); // if we click on a non-modifier, clear the // modifier selections } if (ev.shiftKey) { const sel1 = this.getExtremeSelection(-1); if (sel1.selector.staff === this.suggestion.selector.staff) { this._selectBetweenSelections(sel1, this.suggestion); return; } } if (ev.ctrlKey) { this._addSelection(this.suggestion); this._createLocalModifiersList(); this.deferHighlight(); return; } if (this.autoPlay) { this.playSelection(this.suggestion); } const preselected = this.selections[0] ? SmoSelector.sameNote(this.suggestion.selector, this.selections[0].selector) && this.selections.length === 1 : false; if (this.selections.length === 0) { this.selections.push(this.suggestion); } const note = this.selections[0].note as SmoNote; if (preselected && note.pitches.length > 1) { this.pitchIndex = (this.pitchIndex + 1) % note.pitches.length; this.selections[0].selector.pitches = [this.pitchIndex]; } else { const selection = SmoSelection.noteFromSelector(this.score, this.suggestion.selector); if (selection) { selection.box = JSON.parse(JSON.stringify(this.suggestion.box)); selection.scrollBox = JSON.parse(JSON.stringify(this.suggestion.scrollBox)); this.selections = [selection]; // There is a single selection, make sure the active voice is set to it. selection.measure.setActiveVoice(selection.selector.voice); this.selectActiveVoice(); } } if (preselected && this.modifierSelections.length) { const mods = this.modifierSelections.filter((mm) => mm.selection && SmoSelector.sameNote(mm.selection.selector, this.selections[0].selector)); if (mods.length) { const modToAdd = mods[0]; if (!modToAdd) { console.warn('bad modifier selection in selectSuggestion 2'); } this.modifierSelections[0] = modToAdd; this.modifierIndex = mods[0].index; this._highlightModifier(); return; } } this.score.setActiveStaff(this.selections[0].selector.staff); this.deferHighlight(); this._createLocalModifiersList(); } _setModifierAsSuggestion(artifact: ModifierTab): void { if (!artifact.box) { return; } this.modifierSuggestion = artifact; this._drawRect(artifact.box, 'suggestion'); } _setArtifactAsSuggestion(artifact: SmoSelection) { let sameSel: SmoSelection | null = null; let i = 0; for (i = 0; i < this.selections.length; ++i) { const ss = this.selections[i]; if (ss && SmoSelector.sameNote(ss.selector, artifact.selector)) { sameSel = ss; break; } } if (sameSel || !artifact.box) { return; } this.modifierSuggestion = null; this.suggestion = artifact; this._drawRect(artifact.box, 'suggestion'); } _highlightModifier() { let box: SvgBox | null = null; if (!this.modifierSelections.length) { return; } this.removeModifierSelectionBox(); this.modifierSelections.forEach((artifact) => { if (box === null) { box = artifact.modifier.logicalBox ?? null; } else { box = SvgHelpers.unionRect(box, SvgHelpers.smoBox(artifact.modifier.logicalBox)); } }); if (box === null) { return; } this._drawRect(box, 'staffModifier'); } _highlightPitchSelection(note: SmoNote, index: number) { const noteDiv = $(this.renderElement).find('#' + note.renderId); const heads = noteDiv.find('.vf-notehead'); if (!heads.length) { return; } const headEl = heads[index]; const pageContext = this.renderer.pageMap.getRendererFromModifier(note); $(pageContext.svg).find('.vf-pitchSelection').remove(); const box = pageContext.offsetBbox(headEl); this._drawRect(box, 'pitchSelection'); } _highlightActiveVoice(selection: SmoSelection) { let i = 0; const selector = selection.selector; for (i = 1; i <= 4; ++i) { const cl = 'v' + i.toString() + '-active'; $('body').removeClass(cl); } const c2 = 'v' + (selector.voice + 1).toString() + '-active'; $('body').addClass(c2); } // The user has just switched voices, select the active voice selectActiveVoice() { const selection = this.selections[0]; const selector = JSON.parse(JSON.stringify(selection.selector)); selector.voice = selection.measure.activeVoice; this.selections = [this._getClosestTick(selector)]; this.deferHighlight(); } highlightSelection() { let i = 0; let prevSel: SmoSelection | null = null; let curBox: SvgBox = SvgBox.default; this.idleTimer = Date.now(); const grace = this.getSelectedGraceNotes(); // If this is not a note with grace notes, logically unselect the grace notes if (grace && grace.length && grace[0].selection && this.selections.length) { if (!SmoSelector.sameNote(grace[0].selection.selector, this.selections[0].selector)) { this.clearModifierSelections(); } else { this._highlightModifier(); return; } } // If there is a race condition with a change, avoid referencing null note if (!this.selections[0].note) { return; } const note = this.selections[0].note as SmoNote; if (this.pitchIndex >= 0 && this.selections.length === 1 && this.pitchIndex < note.pitches.length) { this._highlightPitchSelection(note, this.pitchIndex); this._highlightActiveVoice(this.selections[0]); return; } this.removePitchSelection(); this.pitchIndex = -1; if (this.selections.length === 1 && note.logicalBox) { this._drawRect(note.logicalBox, 'selection'); this._highlightActiveVoice(this.selections[0]); return; } const sorted = this.selections.sort((a, b) => SmoSelector.gt(a.selector, b.selector) ? 1 : -1); prevSel = sorted[0]; // rendered yet? if (!prevSel || !prevSel.box) { return; } curBox = SvgHelpers.smoBox(prevSel.box); const boxes: SvgBox[] = []; for (i = 1; i < sorted.length; ++i) { const sel = sorted[i]; if (!sel.box || !prevSel.box) { continue; } // const ydiff = Math.abs(prevSel.box.y - sel.box.y); if (sel.selector.staff === prevSel.selector.staff && sel.measure.svg.lineIndex === prevSel.measure.svg.lineIndex) { curBox = SvgHelpers.unionRect(curBox, sel.box); } else if (curBox) { boxes.push(curBox); curBox = SvgHelpers.smoBox(sel.box); } this._highlightActiveVoice(sel); prevSel = sel; } boxes.push(curBox); if (this.modifierSelections.length) { boxes.push(this.modifierSelections[0].box); } this._drawRect(boxes, 'selection'); } /** * Boxes are divided up into lines/systems already. But we need * to put the correct box on the correct page. * @param boxes */ drawSelectionRects(boxes: SvgBox[]) { const keys = Object.keys(this.selectionRects); // erase any old selections keys.forEach((key) => { const oon = this.selectionRects[parseInt(key)]; oon.forEach((outline) => { if (outline.element) { outline.element.remove(); outline.element = undefined; } }) }); this.selectionRects = {}; // Create an OutlineInfo for each page const pages: number[] = []; const stroke: StrokeInfo = (SuiTracker.strokes as any)['selection']; boxes.forEach((box) => { let testBox: SvgBox = SvgHelpers.smoBox(box); let context = this.renderer.pageMap.getRenderer(testBox); testBox.y -= context.box.y; if (!this.selectionRects[context.pageNumber]) { this.selectionRects[context.pageNumber] = []; pages.push(context.pageNumber); } this.selectionRects[context.pageNumber].push({ context: context, box: testBox, classes: '', stroke, scroll: this.scroller.scrollState, timeOff: 0 }); }); pages.forEach((pageNo) => { const outlineInfos = this.selectionRects[pageNo]; outlineInfos.forEach((info) => { SvgHelpers.outlineRect(info); }); }); } _drawRect(pBox: SvgBox | SvgBox[], strokeName: string) { const stroke: StrokeInfo = (SuiTracker.strokes as any)[strokeName]; const boxes = Array.isArray(pBox) ? pBox : [pBox]; if (strokeName === 'selection') { this.drawSelectionRects(boxes); return; } boxes.forEach((box) => { let testBox: SvgBox = SvgHelpers.smoBox(box); let context = this.renderer.pageMap.getRenderer(testBox); const timeOff = strokeName === 'suggestion' ? 1000 : 0; if (context) { testBox.y -= context.box.y; if (!this.outlines[strokeName]) { this.outlines[strokeName] = { context: context, box: testBox, classes: '', stroke, scroll: this.scroller.scrollState, timeOff }; } this.outlines[strokeName].box = testBox; this.outlines[strokeName].context = context; SvgHelpers.outlineRect(this.outlines[strokeName]); } }); } }