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

752 lines (735 loc) 32 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. import { VxMeasure } from './vxMeasure'; import { SmoSelection, SmoSelector } from '../../smo/xform/selections'; import { SvgHelpers } from '../sui/svgHelpers'; import { SmoLyric } from '../../smo/data/noteModifiers'; import { SmoStaffHairpin, SmoSlur, StaffModifierBase, SmoTie, SmoStaffTextBracket, SmoPedalMarking } from '../../smo/data/staffModifiers'; import { SmoScore } from '../../smo/data/score'; import { SmoMusic } from '../../smo/data/music'; import { leftConnectorVx, rightConnectorVx } from './smoAdapter'; import { SmoMeasure, SmoVoice } from '../../smo/data/measure'; import { SvgBox, ElementLike, RemoveElementLike } from '../../smo/data/common'; import { SmoNote } from '../../smo/data/note'; import { SmoSystemStaff } from '../../smo/data/systemStaff'; import { SmoVolta } from '../../smo/data/measureModifiers'; import { SmoMeasureFormat } from '../../smo/data/measureModifiers'; import { SmoScoreText } from '../../smo/data/scoreText' import { SvgPage } from '../sui/svgPageMap'; import { SuiScroller } from '../sui/scroller'; import { VexFlow, Voice, Note, createHairpin, createSlur, createTie, PedalMarking, StaveNote, Beam, Stem } from '../../common/vex'; import { toVexVolta, vexOptions } from './smoAdapter'; const VF = VexFlow; /** * @category SuiRender */ export interface VoltaInfo { smoMeasure: SmoMeasure, ending: SmoVolta } /** * @category SuiRender */ export interface SuiSystemGroup { firstMeasure: VxMeasure, voices: Voice[] } /** * Create a system of staves and draw music on it. This calls the Vex measure * rendering methods, and also draws all the score and system level stuff like slurs, * text, aligns the lyrics. * @category SuiRender * */ export class VxSystem { context: SvgPage; leftConnector: any[] = [null, null]; score: SmoScore; vxMeasures: VxMeasure[] = []; smoMeasures: SmoMeasure[] = []; lineIndex: number; maxStaffIndex: number; maxSystemIndex: number; minMeasureIndex: number = -1; maxMeasureIndex: number = 0; width: number; staves: SmoSystemStaff[] = []; box: SvgBox = SvgBox.default; currentY: number; topY: number; clefWidth: number; ys: number[] = []; measures: VxMeasure[] = []; modifiers: any[] = []; constructor(context: SvgPage, topY: number, lineIndex: number, score: SmoScore) { this.context = context; this.lineIndex = lineIndex; this.score = score; this.maxStaffIndex = -1; this.maxSystemIndex = -1; this.width = -1; this.staves = []; this.currentY = 0; this.topY = topY; this.clefWidth = 70; this.ys = []; } getVxMeasure(smoMeasure: SmoMeasure) { let i = 0; for (i = 0; i < this.vxMeasures.length; ++i) { const vm = this.vxMeasures[i]; if (vm.smoMeasure.id === smoMeasure.id) { return vm; } } return null; } getVxNote(smoNote: SmoNote): Note | null { let i = 0; if (!smoNote) { return null; } for (i = 0; i < this.measures.length; ++i) { const mm = this.measures[i]; if (mm.noteToVexMap[smoNote.attrs.id]) { return mm.noteToVexMap[smoNote.attrs.id]; } } return null; } _updateChordOffsets(note: SmoNote) { var i = 0; for (i = 0; i < 3; ++i) { const chords = note.getLyricForVerse(i, SmoLyric.parsers.chord); chords.forEach((bchord) => { const chord = bchord as SmoLyric; const dom = this.context.svg.getElementById('vf-' + chord.attrs.id); if (dom) { dom.setAttributeNS('', 'transform', 'translate(' + chord.translateX + ' ' + (-1 * chord.translateY) + ')'); } }); } } _lowestYLowestVerse(lyrics: SmoLyric[], vxMeasures: VxMeasure[]) { // Move each verse down, according to the lowest lyric on that line/verse, // and the accumulation of the verses above it let lowestY = 0; for (var lowVerse = 0; lowVerse < 4; ++lowVerse) { let maxVerseHeight = 0; const verseLyrics = lyrics.filter((ll) => ll.verse === lowVerse); if (lowVerse === 0) { // first verse, go through list twice. first find lowest points verseLyrics.forEach((lyric: SmoLyric) => { if (lyric.logicalBox) { // 'lowest' Y on screen is Y with largest value... const ly = lyric.logicalBox.y - this.context.box.y; lowestY = Math.max(ly + lyric.musicYOffset, lowestY); } }); // second offset all to that point verseLyrics.forEach((lyric: SmoLyric) => { if (lyric.logicalBox) { const ly = lyric.logicalBox.y - this.context.box.y; const offset = Math.max(0, lowestY - ly); lyric.adjY = offset + lyric.translateY; } }); } else { // subsequent verses, first find the tallest lyric verseLyrics.forEach((lyric: SmoLyric)=> { if (lyric.logicalBox) { maxVerseHeight = Math.max(lyric.logicalBox.height, maxVerseHeight); } }); // adjust lowestY to be the verse height below the previous verse lowestY = lowestY + maxVerseHeight * 1.1; // 1.1 magic number? // and offset these lyrics verseLyrics.forEach((lyric: SmoLyric)=> { if (lyric.logicalBox) { const ly = lyric.logicalBox.y - this.context.box.y; const offset = Math.max(0, lowestY - ly); lyric.adjY = offset + lyric.translateY; } }); } } } // ### updateLyricOffsets // Adjust the y position for all lyrics in the line so they are even. // Also replace '-' with a longer dash do indicate 'until the next measure' updateLyricOffsets() { let i = 0; for (i = 0; i < this.score.staves.length; ++i) { const tmpI = i; const lyricsDash: SmoLyric[] = []; const lyricHyphens: SmoLyric[] = []; const lyricVerseMap: Record<number, SmoLyric[]> = {}; const lyrics: SmoLyric[] = []; // is this necessary? They should all be from the current line const vxMeasures = this.vxMeasures.filter((vx) => vx.smoMeasure.measureNumber.staffId === tmpI ); // All the lyrics on this line // The vertical bounds on each line vxMeasures.forEach((mm) => { var smoMeasure = mm.smoMeasure; // Get lyrics from any voice. smoMeasure.voices.forEach((voice) => { voice.notes.forEach((note) => { this._updateChordOffsets(note); note.getTrueLyrics().forEach((ll: SmoLyric) => { const hasLyric = ll.getText().length > 0 || ll.isHyphenated(); if (hasLyric && ll.logicalBox && !lyricVerseMap[ll.verse]) { lyricVerseMap[ll.verse] = []; }else if (hasLyric && !ll.logicalBox) { console.warn( `unrendered lyric for note ${note.attrs.id} measure ${smoMeasure.measureNumber.staffId}-${smoMeasure.measureNumber.measureIndex}`); } if (hasLyric && ll.logicalBox) { lyricVerseMap[ll.verse].push(ll); lyrics.push(ll); } }); }); }); }); // calculate y offset so the lyrics all line up this._lowestYLowestVerse(lyrics, vxMeasures); const vkey: string[] = Object.keys(lyricVerseMap).sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); vkey.forEach((sverse) => { const verse = parseInt(sverse, 10); let hyphenLyric: SmoLyric | null = null; const lastVerse = lyricVerseMap[verse][lyricVerseMap[verse].length - 1].attrs.id; lyricVerseMap[verse].forEach((ll: SmoLyric) => { if (hyphenLyric !== null && hyphenLyric.logicalBox !== null && ll.logicalBox !== null) { const x = ll.logicalBox.x - (ll.logicalBox.x - (hyphenLyric.logicalBox.x + hyphenLyric.logicalBox.width)) / 2; ll.hyphenX = x; lyricHyphens.push(ll); } if (ll.isHyphenated() && ll.logicalBox !== null) { if (ll.attrs.id === lastVerse) { // Last word on the system, place the hyphen after the word const fontSize = SmoScoreText.fontPointSize(ll.fontInfo.size); ll.hyphenX = ll.logicalBox.x + ll.logicalBox.width + fontSize / 2; lyricHyphens.push(ll); } else if (ll.getText().length) { // place the hyphen 1/2 between next word and this one. hyphenLyric = ll; } } else { hyphenLyric = null; } }); }); lyrics.forEach((lyric) => { const dom = this.context.svg.getElementById('vf-' + lyric.attrs.id) as SVGSVGElement; if (dom) { dom.setAttributeNS('', 'transform', 'translate(' + lyric.adjX + ' ' + lyric.adjY + ')'); // Keep track of lyrics that are 'dash' if (lyric.isDash()) { lyricsDash.push(lyric); } } }); lyricHyphens.forEach((lyric) => { const parent = this.context.svg.getElementById('vf-' + lyric.attrs.id); if (parent && lyric.logicalBox !== null) { const ly = lyric.logicalBox.y - this.context.box.y; const text = document.createElementNS(SvgHelpers.namespace, 'text'); text.textContent = '-'; const fontSize = SmoScoreText.fontPointSize(lyric.fontInfo.size); text.setAttributeNS('', 'x', (lyric.hyphenX - fontSize / 3).toString()); text.setAttributeNS('', 'y', (ly + (lyric.logicalBox.height * 2) / 3).toString()); text.setAttributeNS('', 'font-size', '' + fontSize + 'pt'); parent.appendChild(text); } }); lyricsDash.forEach((lyric) => { const parent = this.context.svg.getElementById('vf-' + lyric.attrs.id); if (parent && lyric.logicalBox !== null) { const ly = lyric.logicalBox.y - this.context.box.y; const line = document.createElementNS(SvgHelpers.namespace, 'line'); const ymax = Math.round(ly + lyric.logicalBox.height / 2); const offset = Math.round(lyric.logicalBox.width / 2); line.setAttributeNS('', 'x1', (lyric.logicalBox.x - offset).toString()); line.setAttributeNS('', 'y1', ymax.toString()); line.setAttributeNS('', 'x2', (lyric.logicalBox.x + lyric.logicalBox.width + offset).toString()); line.setAttributeNS('', 'y2', ymax.toString()); line.setAttributeNS('', 'stroke-width', '1'); line.setAttributeNS('', 'fill', 'none'); line.setAttributeNS('', 'stroke', '#999999'); parent.appendChild(line); const texts = parent.getElementsByTagName('text'); // hide hyphen and replace with dash if (texts && texts.length) { const text = texts[0]; text.setAttributeNS('', 'fill', '#fff'); } } }); } } // ### renderModifier // render a line-type modifier that is associated with a staff (e.g. slur) renderModifier(scroller: SuiScroller, modifier: StaffModifierBase, vxStart: Note | null, vxEnd: Note | null, smoStart: SmoSelection, smoEnd: SmoSelection) { const setSameIfNull = (a: any, b: any) => { if (typeof (a) === 'undefined' || a === null) { return b; } return a; }; if (smoStart && smoStart.note && smoStart.note.noteType === '/') { return; } if (smoEnd && smoEnd.note && smoEnd.note.noteType === '/') { return; } // if (modifier.ctor === 'SmoPedalMarking' && (vxStart === null || vxEnd === null)) { // return; // } let slurOffset = 0; // if it is split between lines, render one artifact for each line, with a common class for // both if it is removed. if (vxStart) { const toRemove = this.context.svg.getElementById('vf-' + modifier.attrs.id); if (toRemove) { toRemove.remove(); } } const artifactId = modifier.attrs.id + '-' + this.lineIndex; const group = this.context.getContext().openGroup('slur', artifactId); group.classList.add(modifier.attrs.id); const measureMod = 'mod-' + smoStart.selector.staff + '-' + smoStart.selector.measure; const staffMod = 'mod-' + smoStart.selector.staff; group.classList.add(measureMod); group.classList.add(staffMod); if (modifier.ctor === 'SmoStaffHairpin') { const hp = modifier as SmoStaffHairpin; if (!vxStart && !vxEnd) { this.context.getContext().closeGroup(); } vxStart = setSameIfNull(vxStart, vxEnd); vxEnd = setSameIfNull(vxEnd, vxStart); const smoVexHairpinParams = { vxStart, vxEnd, hairpinType: hp.hairpinType, height: hp.height, yOffset: hp.yOffset, leftShiftPx: hp.xOffsetLeft, rightShiftPx: hp.xOffsetRight }; const hairpin = createHairpin(smoVexHairpinParams); hairpin.setContext(this.context.getContext()).setPosition(hp.position).draw(); } else if (modifier.ctor === 'SmoSlur') { const startNote: SmoNote = smoStart!.note as SmoNote; const slur = modifier as SmoSlur; let startPosition = slur.position; let endPosition = slur.position_end; let openingDirection = 'up'; let yOffset = slur.yOffset; let slurX = slur.xOffset; const svgPoint: SVGPoint[] = JSON.parse(JSON.stringify(slur.controlPoints)); const lyric = startNote.longestLyric() as SmoLyric; // Find direction for slur based on beam/stem direction // Note: vex slur orientation follows beam direction, not slur direction. Smo // orientation follows slur direction. if (vxStart !== null && vxEnd !== null) { if (slur.position === SmoSlur.positions.AUTO || slur.position_end === SmoSlur.positions.AUTO || slur.orientation === SmoSlur.orientations.AUTO) { startPosition = SmoSlur.positions.HEAD; endPosition = SmoSlur.positions.HEAD; if (vxStart.hasStem()) { if (vxStart.getStemDirection() === VF.Stem.UP) { openingDirection = 'up'; } else { openingDirection = 'down'; } if (vxEnd.hasStem() && vxEnd.getStemDirection() !== vxStart.getStemDirection()) { endPosition = SmoSlur.positions.TOP; } } else { openingDirection = slur.orientation === SmoSlur.orientations.UP ? 'down' : 'up'; startPosition = slur.position; endPosition = slur.position_end; } } else { openingDirection = slur.orientation === SmoSlur.orientations.UP ? 'down' : 'up'; startPosition = slur.position; endPosition = slur.position_end; } } else if (vxStart !== null && vxEnd === null) { slurX = 10; slurOffset = -5; if (slur.orientation === SmoSlur.orientations.AUTO && vxStart.hasStem()) { openingDirection = vxStart.getStemDirection() === VF.Stem.UP ? 'up' : 'down'; } else { openingDirection = slur.orientation === SmoSlur.orientations.UP ? 'down' : 'up'; } startPosition = SmoSlur.positions.HEAD; endPosition = SmoSlur.positions.HEAD; } else if (vxEnd !== null && vxStart === null) { slurX = 10; slurOffset = 5; if (slur.orientation === SmoSlur.orientations.AUTO && vxEnd.hasStem()) { openingDirection = vxEnd.getStemDirection() === VF.Stem.UP ? 'up' : 'down'; } else { openingDirection = slur.orientation === SmoSlur.orientations.UP ? 'down' : 'up'; } startPosition = SmoSlur.positions.HEAD; endPosition = SmoSlur.positions.HEAD; } // yoffset is always in the direction of the curve, not SVG. Make sure the curve clears the yoffset // TODO: I think we should adjust this line vs. space if (openingDirection === 'up') { yOffset += 15; } else { yOffset += 10; } if (lyric && lyric.getText()) { // If there is a lyric, the bounding box of the start note is stretched to the right. // slide the slur left, and also make it a bit wider. const xtranslate = (-1 * lyric.getText().length * 6); slurX += (xtranslate / 2) - SmoSlur.defaults.xOffset; } if (SmoSelector.lt(smoEnd.selector, slur.endSelector)) { slurX += 15; } const smoVexSlurParams = { vxStart, vxEnd, thickness: slur.thickness, xShift: slurX, yShift: yOffset, openingDirection, cps: svgPoint, position: startPosition, positionEnd: endPosition }; const curve = createSlur(smoVexSlurParams); curve.setContext(this.context.getContext()).draw(); } else if (modifier.ctor === 'SmoPedalMarking') { const pedalMarking = modifier as SmoPedalMarking; const pedalAr: StaveNote[] = []; if (vxStart !== null) { pedalAr.push(vxStart as StaveNote); } if (SmoSelector.gt(smoEnd.selector, smoStart.selector) && vxEnd !== null) { // Add releases for the pedal marking pedalMarking.releases.forEach((selector: SmoSelector) => { if (SmoSelector.gt(selector, smoStart.selector) && SmoSelector.lt(selector, smoEnd.selector) && vxStart !== null) { const note = SmoSelection.noteSelection(this.score, selector.staff, selector.measure, selector.voice, selector.tick); if (note !== null && note.note !== null) { const vexNote = this.getVxNote(note.note); if (vexNote) { // incidate release and depress pedalAr.push(vexNote as StaveNote); pedalAr.push(vexNote as StaveNote); } } } }); pedalAr.push(vxEnd as StaveNote); if (vxStart === null) { pedalAr.push(vxEnd as StaveNote); } } const vexPedal = new VF.PedalMarking(pedalAr); if (pedalMarking.releaseText.length > 0 || pedalMarking.depressText.length > 0) { vexPedal.setCustomText(pedalMarking.depressText, pedalMarking.releaseText); } if (!pedalMarking.startMark && pedalMarking.depressText.length < 1) { vexPedal.setCustomText(' ', pedalMarking.releaseText); } if (pedalMarking.bracket) { if (pedalMarking.startMark || pedalMarking.releaseMark) { vexPedal.setType(VF.PedalMarking.type.MIXED); } else { vexPedal.setType(VF.PedalMarking.type.BRACKET); } } else { vexPedal.setType(VF.PedalMarking.type.TEXT); } if (SmoSelector.gt(smoStart.selector, modifier.startSelector) && (pedalMarking.startMark)) { // If this is the completion of a pedal marking from a previous staff, don't print the depress // pedal again vexPedal.setType(VF.PedalMarking.type.MIXED); vexPedal.setCustomText(' ', pedalMarking.depressText); } if (SmoSelector.lt(smoEnd.selector, modifier.endSelector) && pedalMarking.releaseMark) { vexPedal.setType(VF.PedalMarking.type.MIXED); vexPedal.setCustomText(pedalMarking.depressText, ' '); } vexPedal.setContext(this.context.getContext()); vexPedal.draw(); } else if (modifier.ctor === 'SmoTie') { const ctie = modifier as SmoTie; const startNote: SmoNote = smoStart!.note as SmoNote; const endNote: SmoNote = smoEnd!.note as SmoNote; ctie.checkLines(startNote, endNote); if (ctie.lines.length > 0) { const fromLines = ctie.lines.map((ll) => ll.from); const toLines = ctie.lines.map((ll) => ll.to); const smoVexTieParams = { fromLines, toLines, firstNote: vxStart, lastNote: vxEnd, vexOptions: vexOptions(ctie) } const tie = createTie(smoVexTieParams); tie.setContext(this.context.getContext()).draw(); } } else if (modifier.ctor === 'SmoStaffTextBracket') { if (vxStart && !vxEnd) { vxEnd = vxStart; } else if (vxEnd && !vxStart) { vxStart = vxEnd; } if (vxStart && vxEnd) { const smoBracket = (modifier as SmoStaffTextBracket); const bracket = new VF.TextBracket({ start: vxStart, stop: vxEnd, text: smoBracket.text, superscript: smoBracket.superscript, position: smoBracket.position }); bracket.setLine(smoBracket.line).setContext(this.context.getContext()).draw(); } } this.context.getContext().closeGroup(); if (slurOffset) { const slurBox = this.context.svg.getElementById('vf-' + artifactId) as SVGSVGElement; if (slurBox) { SvgHelpers.translateElement(slurBox, slurOffset, 0); } } modifier.element = group; } renderEndings(scroller: SuiScroller) { let j = 0; let i = 0; if (this.staves.length < 1) { return; } const voltas = this.staves[0].getVoltaMap(this.minMeasureIndex, this.maxMeasureIndex); voltas.forEach((ending) => { ending.elements.forEach((element: ElementLike) => { RemoveElementLike(element); }); ending.elements = []; }); for (j = 0; j < this.smoMeasures.length; ++j) { let pushed = false; const smoMeasure = this.smoMeasures[j]; // Only draw volta on top staff of system if (smoMeasure.svg.rowInSystem > 0) { continue; } const vxMeasure = this.getVxMeasure(smoMeasure); const voAr: VoltaInfo[] = []; for (i = 0; i < voltas.length && vxMeasure !== null; ++i) { const ending = voltas[i]; const mix = smoMeasure.measureNumber.measureIndex; if ((ending.startBar <= mix) && (ending.endBar >= mix) && vxMeasure.stave !== null) { const group = this.context.getContext().openGroup(undefined, ending.attrs.id); group.classList.add(ending.attrs.id); group.classList.add(ending.endingId ?? ''); ending.elements.push(group); const vtype = toVexVolta(ending, smoMeasure.measureNumber.measureIndex); const vxVolta = new VF.Volta(vtype, ending.number.toString(), smoMeasure.staffX + ending.xOffsetStart, ending.yOffset); vxVolta.setContext(this.context.getContext()).draw(vxMeasure.stave, -1 * ending.xOffsetEnd); this.context.getContext().closeGroup(); const height = parseInt(vxVolta.getFontSize(), 10) * 2; const width = smoMeasure.staffWidth; const y = smoMeasure.svg.logicalBox.y - (height + ending.yOffset); ending.logicalBox = { x: smoMeasure.svg.staffX, y, width, height }; if (!pushed) { voAr.push({ smoMeasure, ending }); pushed = true; } vxMeasure.stave.getModifiers().push(vxVolta); } } // Adjust real height of measure to match volta height for (i = 0; i < voAr.length; ++i) { const mm = voAr[i].smoMeasure; const ending = voAr[i].ending; if (ending.logicalBox !== null) { const delta = mm.svg.logicalBox.y - ending.logicalBox.y; if (delta > 0) { mm.setBox(SvgHelpers.boxPoints( mm.svg.logicalBox.x, mm.svg.logicalBox.y - delta, mm.svg.logicalBox.width, mm.svg.logicalBox.height + delta), 'vxSystem adjust for volta'); } } } } } getMeasureByIndex(measureIndex: number, staffId: number) { let i = 0; for (i = 0; i < this.smoMeasures.length; ++i) { const mm = this.smoMeasures[i]; if (measureIndex === mm.measureNumber.measureIndex && staffId === mm.measureNumber.staffId) { return mm; } } return null; } // ## renderMeasure // ## Description: // Create the graphical (VX) notes and render them on svg. Also render the tuplets and beam // groups renderMeasure(smoMeasure: SmoMeasure, printing: boolean) { if (smoMeasure.svg.hideMultimeasure) { return; } const measureIndex = smoMeasure.measureNumber.measureIndex; if (this.minMeasureIndex < 0 || this.minMeasureIndex > measureIndex) { this.minMeasureIndex = measureIndex; } if (this.maxMeasureIndex < measureIndex) { this.maxMeasureIndex = measureIndex; } let brackets = false; const staff = this.score.staves[smoMeasure.measureNumber.staffId]; const staffId = staff.staffId; const systemIndex = smoMeasure.measureNumber.systemIndex; const selection = SmoSelection.measureSelection(this.score, staff.staffId, smoMeasure.measureNumber.measureIndex); this.smoMeasures.push(smoMeasure); if (this.staves.length <= staffId) { this.staves.push(staff); } if (selection === null) { return; } let softmax = selection.measure.format.proportionality; if (softmax === SmoMeasureFormat.defaultProportionality) { softmax = this.score.layoutManager?.getGlobalLayout().proportionality ?? 0; } const vxMeasure: VxMeasure = new VxMeasure(this.context, selection, printing, softmax); // create the vex notes, beam groups etc. for the measure vxMeasure.preFormat(); this.vxMeasures.push(vxMeasure); const lastStaff = (staffId === this.score.staves.length - 1); const smoGroupMap: Record<string, SuiSystemGroup> = {}; const adjXMap: Record<number, number> = {}; const vxMeasures = this.vxMeasures.filter((mm) => !mm.smoMeasure.svg.hideEmptyMeasure); // If this is the last staff in the column, render the column with justification if (lastStaff) { vxMeasures.forEach((mm) => { if (typeof(adjXMap[mm.smoMeasure.measureNumber.systemIndex]) === 'undefined') { adjXMap[mm.smoMeasure.measureNumber.systemIndex] = mm.smoMeasure.svg.adjX; } adjXMap[mm.smoMeasure.measureNumber.systemIndex] = Math.max(adjXMap[mm.smoMeasure.measureNumber.systemIndex], mm.smoMeasure.svg.adjX); }); vxMeasures.forEach((vv: VxMeasure) => { if (!vv.rendered && !vv.smoMeasure.svg.hideEmptyMeasure && vv.stave) { vv.stave.setNoteStartX(vv.stave.getNoteStartX() + adjXMap[vv.smoMeasure.measureNumber.systemIndex] - vv.smoMeasure.svg.adjX); const systemGroup = this.score.getSystemGroupForStaff(vv.selection); const justifyGroup: string = (systemGroup && vv.smoMeasure.format.autoJustify) ? systemGroup.attrs.id : vv.selection.staff.attrs.id; if (!smoGroupMap[justifyGroup]) { smoGroupMap[justifyGroup] = { firstMeasure: vv, voices: [] }; } smoGroupMap[justifyGroup].voices = smoGroupMap[justifyGroup].voices.concat(vv.voiceAr); if (vv.tabVoice) { smoGroupMap[justifyGroup].voices.concat(vv.tabVoice); } } }); } const keys = Object.keys(smoGroupMap); keys.forEach((key) => { smoGroupMap[key].firstMeasure.format(smoGroupMap[key].voices); }); if (lastStaff) { vxMeasures.forEach((vv) => { if (!vv.rendered) { vv.render(); } }); } // Keep track of the y coordinate for the nth staff const renderedConnection: Record<string, number> = {}; if (systemIndex === 0 && lastStaff) { if (staff.bracketMap[this.lineIndex]) { staff.bracketMap[this.lineIndex].forEach((element) => { RemoveElementLike(element); }); } staff.bracketMap[this.lineIndex] = []; const group = this.context.getContext().openGroup(); group.classList.add('lineBracket-' + this.lineIndex); group.classList.add('lineBracket'); staff.bracketMap[this.lineIndex].push(group); vxMeasures.forEach((vv) => { const systemGroup = this.score.getSystemGroupForStaff(vv.selection); if (systemGroup && !renderedConnection[systemGroup.attrs.id] && !vv.smoMeasure.svg.hideEmptyMeasure) { renderedConnection[systemGroup.attrs.id] = 1; const startSel = this.vxMeasures[systemGroup.startSelector.staff]; const endSel = this.vxMeasures[systemGroup.endSelector.staff]; if (startSel && startSel.rendered && endSel && endSel.rendered) { const c1 = new VF.StaveConnector(startSel.stave!, endSel.stave!) .setType(leftConnectorVx(systemGroup)); c1.setContext(this.context.getContext()).draw(); brackets = true; } } }); if (!brackets && vxMeasures.length > 1) { const c2 = new VF.StaveConnector(vxMeasures[0].stave!, vxMeasures[vxMeasures.length - 1].stave!); c2.setType(VF.StaveConnector.type.SINGLE_RIGHT); c2.setContext(this.context.getContext()).draw(); } // draw outer brace on parts with multiple staves (e.g. keyboards) vxMeasures.forEach((vv) => { if (vv.selection.staff.partInfo.stavesAfter > 0) { if (this.vxMeasures.length > vv.selection.selector.staff + 1) { const endSel = this.vxMeasures[vv.selection.selector.staff + 1]; const startSel = vv; if (startSel && startSel.rendered && endSel && endSel.rendered) { const c1 = new VF.StaveConnector(startSel.stave!, endSel.stave!) .setType(VF.StaveConnector.type.BRACE); c1.setContext(this.context.getContext()).draw(); } } }; }); this.context.getContext().closeGroup(); } else if (lastStaff && smoMeasure.measureNumber.measureIndex + 1 < staff.measures.length) { if (staff.measures[smoMeasure.measureNumber.measureIndex + 1].measureNumber.systemIndex === 0) { const endMeasure = vxMeasure; const startMeasure = vxMeasures.find((vv) => vv.selection.selector.staff === 0 && vv.selection.selector.measure === vxMeasure.selection.selector.measure && vv.smoMeasure.svg.hideEmptyMeasure === false); if (endMeasure && endMeasure.stave && startMeasure && startMeasure.stave) { const group: ElementLike = this.context.getContext().openGroup(); group.classList.add('endBracket-' + this.lineIndex); group.classList.add('endBracket'); staff.bracketMap[this.lineIndex].push(group); const c2 = new VF.StaveConnector(startMeasure.stave, endMeasure.stave) .setType(VF.StaveConnector.type.SINGLE_RIGHT); c2.setContext(this.context.getContext()).draw(); this.context.getContext().closeGroup(); } } } // keep track of left-hand side for system connectors if (systemIndex === 0) { if (staffId === 0) { this.leftConnector[0] = vxMeasure.stave; } else if (staffId > this.maxStaffIndex) { this.maxStaffIndex = staffId; this.leftConnector[1] = vxMeasure.stave; } } else if (smoMeasure.measureNumber.systemIndex > this.maxSystemIndex) { this.maxSystemIndex = smoMeasure.measureNumber.systemIndex; } this.measures.push(vxMeasure); } }