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
687 lines (655 loc) • 27.3 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
// ## Description:
// This file calls the vexflow routines that actually render a
// measure of music. If multiple measures are justified in a
// column, the rendering is deferred until all the measures have been
// preformatted.
import { SmoNote } from '../../smo/data/note';
import { SmoMusic } from '../../smo/data/music';
import { layoutDebug } from '../sui/layoutDebug';
import { SmoRepeatSymbol, SmoMeasureText, SmoBarline, SmoMeasureModifierBase, SmoRehearsalMark } from '../../smo/data/measureModifiers';
import { SourceSerifProFont } from '../../styles/font_metrics/ssp-serif-metrics';
import { SmoOrnament, SmoDynamicText,
SmoNoteModifierBase, SmoTabNote } from '../../smo/data/noteModifiers';
import { SmoSelection } from '../../smo/xform/selections';
import { SmoMeasure, MeasureTickmaps } from '../../smo/data/measure';
import { SvgHelpers } from '../sui/svgHelpers';
import { Clef, IsClef, ElementLike } from '../../smo/data/common';
import { SvgPage } from '../sui/svgPageMap';
import { SmoTabStave } from '../../smo/data/staffModifiers';
import { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, toVexSymbol,
toVexTextJustification, toVexTextPosition, getVexChordBlocks, toVexStemDirection,
VexTabNotePositions } from './smoAdapter';
import { VexFlow, Stave,StemmableNote, Note, Beam, Tuplet, Voice,
Formatter, Accidental, Annotation, StaveNoteStruct, StaveText, StaveModifier,
createStaveText, renderDynamics, applyStemDirection,
getVexNoteParameters, defaultNoteScale, defaultCueScale, getVexTuplets,
createStave, createVoice, getOrnamentGlyph, getSlashGlyph, getRepeatBar, getMultimeasureRest,
createTextNote, TabStave, createTabStave, TabNotePosition, TabNoteStruct,
CreateVexNoteParams, TabNote, PedalMarking
} from '../../common/vex';
import { VxMeasureIf, VexNoteModifierIf, VxNote } from './vxNote';
import { SmoTuplet } from '../../smo/data/tuplet';
import { vexGlyph } from './glyphDimensions';
const VF = VexFlow;
declare var $: any;
// const VF = eval('Vex.Flow');
/**
* This is the interface for VexFlow library that actually does the engraving.
* @category SuiRender
*/
export class VxMeasure implements VxMeasureIf {
context: SvgPage;
printing: boolean;
selection: SmoSelection;
softmax: number;
smoMeasure: SmoMeasure;
smoTabStave?: SmoTabStave;
tabStave?: TabStave;
rendered: boolean = false;
noteToVexMap: Record<string, Note> = {};
beamToVexMap: Record<string, Beam> = {};
tupletToVexMap: Record<string, Tuplet> = {};
multimeasureRest: any | null = null;
vexNotes: Note[] = [];
vexBeamGroups: Beam[] = [];
vexTuplets: Tuplet[] = [];
tickmapObject: MeasureTickmaps | null = null;
stave: Stave | null = null; // vex stave
voiceNotes: Note[] = []; // notes for current voice, as rendering
tabNotes: Note[] = [];
voiceAr: Voice[] = [];
tabVoice: Voice | null = null;
formatter: Formatter | null = null;
allCues: boolean = false;
modifiersToBox: SmoNoteModifierBase[] = [];
collisionMap: Record<number, SmoNote[]> = {};
dbgLeftX: number = 0;
dbgWidth: number = 0;
constructor(context: SvgPage, selection: SmoSelection, printing: boolean, softmax: number) {
this.context = context;
this.rendered = false;
this.selection = selection;
this.smoMeasure = this.selection.measure;
this.printing = printing;
this.allCues = selection.staff.partInfo.displayCues;
this.tupletToVexMap = {};
this.vexNotes = [];
this.vexBeamGroups = [];
this.vexBeamGroups = [];
this.beamToVexMap = {};
this.softmax = softmax;
this.smoTabStave = selection.staff.getTabStaveForMeasure(selection.selector);
}
static get fillStyle() {
return '#000';
}
// Treat a rest like a whole rest if there is only a single rest in the measure
// and the measure length is not a pickup
isWholeRest() {
return (this.smoMeasure.voices.length === 1 &&
this.smoMeasure.voices[0].notes.length === 1 &&
this.smoMeasure.voices[0].notes[0].isRest() &&
this.smoMeasure.getMaxTicksVoice() > 4096
);
}
createCollisionTickmap() {
let i = 0;
let j = 0;
if (!this.tickmapObject) {
return;
}
for (i = 0; i < this.smoMeasure.voices.length; ++i) {
const tm = this.tickmapObject.tickmaps[i];
for (j = 0; j < tm.durationMap.length; ++j) {
if (typeof(this.collisionMap[tm.durationMap[j]]) === 'undefined') {
this.collisionMap[tm.durationMap[j]] = [];
}
this.collisionMap[tm.durationMap[j]].push(this.smoMeasure.voices[i].notes[j]);
}
}
}
isCollision(voiceIx: number, tickIx: number): boolean {
let i = 0;
let j = 0;
let k = 0;
let staffLines: number[] = [];
if (!this.tickmapObject) {
return false;
}
const tick = this.tickmapObject.tickmaps[voiceIx].durationMap[tickIx];
// Just one note, no collision
if (this.collisionMap[tick].length < 2) {
return false;
}
for (i = 0; i < this.collisionMap[tick].length; ++i) {
const note = this.collisionMap[tick][i];
for (j = 0; j < note.pitches.length; ++j) {
const clef: Clef = IsClef(note.clef) ? note.clef : 'treble';
const pitch = note.pitches[j];
const curLine = SmoMusic.pitchToStaffLine(clef, pitch);
for (k = 0;k < staffLines.length; ++k) {
if (Math.abs(curLine - staffLines[k]) < 1) {
return true;
}
}
staffLines.push(curLine);
}
}
return false;
}
/**
* convert a smoNote into a vxNote so it can be rasterized
* @param smoNote
* @param tickIndex - used to calculate accidental
* @param voiceIx
* @returns
*/
createVexNote(smoNote: SmoNote, tickIndex: number, voiceIx: number) {
let vexNote: Note | null = null;
let smoTabNote: SmoTabNote | null = null;
let timestamp = new Date().valueOf();
const stemTicks = SmoMusic.ticksToDuration[smoNote.stemTicks];
let tabNote: StemmableNote | null = null;
const noteHead = smoNote.isRest() ? 'r' : smoNote.noteHead;
const keys = SmoMusic.smoPitchesToVexKeys(smoNote.pitches, 0, noteHead);
const smoNoteParams = {
isTuplet: smoNote.isTuplet,
measureIndex: this.smoMeasure.measureNumber.measureIndex,
clef: smoNote.clef,
stemTicks,
keys,
noteType: smoNote.noteType,
isCue: smoNote.isCue
};
const { noteParams, duration } = getVexNoteParameters(smoNoteParams);
if (this.tabStave && this.smoTabStave) {
smoTabNote = this.smoTabStave.getTabNoteFromNote(smoNote, this.smoMeasure.transposeIndex);
if (smoTabNote) {
const positions: TabNotePosition[] = VexTabNotePositions(this.smoTabStave, smoTabNote, smoNote);
if (positions.length) {
if (!smoNote.isRest()) {
tabNote = new VF.TabNote({ positions, duration: duration });
if (this.smoTabStave.showStems) {
tabNote.render_options.draw_stem = true;
tabNote.render_options.draw_dots = true;
tabNote.render_options.draw_stem_through_stave = smoTabNote.flagThrough;
}
} else {
tabNote = new VF.StaveNote(noteParams);
}
}
}
}
if (smoNote.noteType === '/') {
// vexNote = new VF.GlyphNote('\uE504', { duration });
vexNote = getSlashGlyph();
smoNote.renderId = 'vf-' + vexNote.getAttribute('id'); // where does 'vf' come from?
} else {
const smoVexStemParams = {
voiceCount: this.smoMeasure.voices.length,
voiceIx,
isAuto: smoNote.flagState === SmoNote.flagStates.auto,
isUp: smoNote.flagState === SmoNote.flagStates.up
}
applyStemDirection(smoVexStemParams, noteParams);
if (smoTabNote && tabNote) {
tabNote.setStemDirection(noteParams.stem_direction);
}
layoutDebug.setTimestamp(layoutDebug.codeRegions.PREFORMATA, new Date().valueOf() - timestamp);
timestamp = new Date().valueOf();
vexNote = new VF.StaveNote(noteParams);
if (voiceIx > 0 && this.isCollision(voiceIx, tickIndex)) {
vexNote.setXShift(-10);
}
if (this.isWholeRest()) {
noteParams.duration = 'wr';
vexNote = new VF.StaveNote(noteParams);
vexNote.setCenterAlignment(true);
}
layoutDebug.setTimestamp(layoutDebug.codeRegions.PREFORMATB, new Date().valueOf() - timestamp);
timestamp = new Date().valueOf();
if (smoNote.fillStyle && !this.printing) {
vexNote.setStyle({ fillStyle: smoNote.fillStyle });
} else if (voiceIx > 0 && !this.printing) {
const voiceFill = ['#115511','#555511','#883344']
vexNote.setStyle({ fillStyle: voiceFill[voiceIx - 1] });
} else if (smoNote.isHidden() && this.printing) {
vexNote.setStyle({ fillStyle: "#ffffff00" });
}
smoNote.renderId = 'vf-' + vexNote.getAttribute('id'); // where does 'vf' come from?
if (this.stave) {
vexNote.setStave(this.stave);
}
}
const noteData: VexNoteModifierIf = {
smoMeasure: this.smoMeasure,
vxMeasure: this,
smoNote: smoNote,
staveNote: vexNote,
voiceIndex: voiceIx,
tickIndex: tickIndex
}
if (tabNote) {
noteData.tabNote = tabNote;
this.tabNotes.push(tabNote);
}
const modObj = new VxNote(noteData);
modObj.addModifiers();
layoutDebug.setTimestamp(layoutDebug.codeRegions.PREFORMATC, new Date().valueOf() - timestamp);
return modObj;
}
renderNoteGlyph(smoNote: SmoNote, textObj: SmoDynamicText) {
var x = this.noteToVexMap[smoNote.attrs.id].getAbsoluteX() + textObj.xOffset;
// the -3 is copied from vexflow textDynamics
var y = this.stave!.getYForLine(textObj.yOffsetLine - 3) + textObj.yOffsetPixels;
let maxh = 0;
const minx = x;
var group: ElementLike = this.context.getContext().openGroup();
group.classList.add(textObj.attrs.id + '-' + smoNote.attrs.id);
group.classList.add(textObj.attrs.id);
// const duration = SmoMusic.closestVexDuration(smoNote.tickCount);
for (var i = 0; i < textObj.text.length; i += 1 ) {
const { width , height } = renderDynamics(this.context.getContext(), VF.TextDynamics.GLYPHS[textObj.text[i]].code,
textObj.fontSize, x, y);
/* const { width , height } = renderDynamics(this.context.getContext(), VF.TextDynamics.GLYPHS[textObj.text[i]],
textObj.fontSize, x, y); */
x += width;
maxh = Math.max(height, maxh);
}
textObj.logicalBox = SvgHelpers.boxPoints(minx, y + this.context.box.y, x - minx, maxh);
textObj.element = group;
this.modifiersToBox.push(textObj);
this.context.getContext().closeGroup();
}
renderDynamics() {
this.smoMeasure.voices.forEach((voice) => {
voice.notes.forEach((smoNote) => {
const mods = smoNote.textModifiers.filter((mod) =>
mod.attrs.type === 'SmoDynamicText'
);
mods.forEach((btm) => {
const tm = btm as SmoDynamicText;
this.renderNoteGlyph(smoNote, tm);
});
});
});
}
createRepeatSymbol() {
this.voiceNotes = [];
// const vexNote = new VF.GlyphNote('\uE500', { duration: 'w' }, { line: 2 });
const vexNote = getRepeatBar();
vexNote.setCenterAlignment(true);
this.vexNotes.push(vexNote);
this.voiceNotes.push(vexNote);
}
/**
* create an a array of VF.StaveNote objects to render the active voice.
* @param voiceIx
*/
createVexNotes(voiceIx: number) {
let i = 0;
this.voiceNotes = [];
const voice = this.smoMeasure.voices[voiceIx];
let clefNoteAdded = false;
for (i = 0;
i < voice.notes.length; ++i) {
const smoNote = voice.notes[i];
const textNotes = smoNote.getTextOrnaments();
const vexNote = this.createVexNote(smoNote, i, voiceIx);
this.noteToVexMap[smoNote.attrs.id] = vexNote.noteData.staveNote;
this.vexNotes.push(vexNote.noteData.staveNote);
if (vexNote.noteData.smoNote.clefNote && !clefNoteAdded) {
const cf = new VF.ClefNote(vexNote.noteData.smoNote.clefNote.clef, 'small');
this.voiceNotes.push(cf);
clefNoteAdded = true; // ignore 2nd in a measure
}
this.voiceNotes.push(vexNote.noteData.staveNote);
textNotes.forEach((tn) => {
this.voiceNotes.push(createTextNote(SmoOrnament.textNoteOrnaments[tn.ornament]));
});
if (isNaN(smoNote.ticks.numerator) || isNaN(smoNote.ticks.denominator)
|| isNaN(smoNote.ticks.remainder)) {
throw ('vxMeasure: NaN in ticks');
}
}
}
/**
* Group the notes for beaming and create Vex beam objects
* @param vix - voice index
* @returns
*/
createVexBeamGroups(vix: number) {
let keyNoteIx = -1;
let i = 0;
let j = 0;
let stemDirection = VF.Stem.DOWN;
for (i = 0; i < this.smoMeasure.beamGroups.length; ++i) {
const bg = this.smoMeasure.beamGroups[i];
if (bg.voice !== vix) {
continue;
}
const vexNotes: StemmableNote[] = [];
keyNoteIx = bg.notes.findIndex((nn) => nn.noteType === 'n');
// Fix stem bug: key off first non-rest note.
keyNoteIx = (keyNoteIx >= 0) ? keyNoteIx : 0;
for (j = 0; j < bg.notes.length; ++j) {
const note = bg.notes[j];
if (note.noteType === '/') {
continue;
}
const vexNote = this.noteToVexMap[note.attrs.id];
// some type of redraw condition?
if (!(vexNote instanceof VF.StaveNote || vexNote instanceof VF.GraceNote)) {
return;
}
if (note.tickCount >= 4096 || vexNote.getIntrinsicTicks() >= 4096) {
console.warn('bad length in beam group');
return;
}
if (keyNoteIx === j) {
stemDirection = note.flagState === SmoNote.flagStates.auto ?
vexNote.getStemDirection() : toVexStemDirection(note);
}
vexNote.setStemDirection(stemDirection);
vexNotes.push(vexNote);
}
const vexBeam = new VF.Beam(vexNotes);
vexBeam.breakSecondaryAt(bg.secondaryBeamBreaks);
this.beamToVexMap[bg.attrs.id] = vexBeam;
this.vexBeamGroups.push(vexBeam);
}
}
createVexTuplets(vix: number) {
this.vexTuplets = [];
this.tupletToVexMap = {};
for (let i = 0; i < this.smoMeasure.tupletTrees.length; ++i) {
const tupletTree = this.smoMeasure.tupletTrees[i];
if (tupletTree.voice !== vix) {
continue;
}
const traverseTupletTree = ( parentTuplet: SmoTuplet): void => {
const vexNotes: Note[] = [];
for (let smoNote of this.smoMeasure.tupletNotes(parentTuplet)) {
vexNotes.push(this.noteToVexMap[smoNote.attrs.id]);
}
// If the tuplet has been deleted, the measure might not be updated.
if (vexNotes.length < 2) {
return;
}
const location = this.smoMeasure.getStemDirectionForTuplet(parentTuplet) === SmoNote.flagStates.up ?
VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM;
const smoTupletParams = {
vexNotes,
numNotes: parentTuplet.numNotes,
notesOccupied: parentTuplet.notesOccupied,
ratioed: parentTuplet.ratioed,
bracketed: parentTuplet.bracketed,
location
}
const vexTuplet = getVexTuplets(smoTupletParams);
this.tupletToVexMap[parentTuplet.attrs.id] = vexTuplet;
this.vexTuplets.push(vexTuplet);
for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) {
const tuplet = parentTuplet.childrenTuplets[i];
traverseTupletTree(tuplet);
}
}
traverseTupletTree(tupletTree.tuplet);
}
}
/**
* create the modifiers for the stave itself, bar lines etc.
*/
createMeasureModifiers() {
const sb = this.smoMeasure.getStartBarline();
const eb = this.smoMeasure.getEndBarline();
const sym = this.smoMeasure.getRepeatSymbol();
if (!this.stave) {
return;
}
// don't create a begin bar for any but the 1st measure.
if (this.smoMeasure.measureNumber.systemIndex !== 0 && sb.barline === SmoBarline.barlines.singleBar
&& this.smoMeasure.format.padLeft === 0) {
this.stave.setBegBarType(VF.Barline.type.NONE);
} else {
this.stave.setBegBarType(toVexBarlineType(sb));
}
if (this.smoMeasure.svg.multimeasureLength > 0 && !this.smoMeasure.svg.hideMultimeasure) {
this.stave.setEndBarType(vexBarlineType[this.smoMeasure.svg.multimeasureEndBarline]);
} else if (eb.barline !== SmoBarline.barlines.singleBar) {
this.stave.setEndBarType(toVexBarlineType(eb));
}
if (sym && sym.symbol !== SmoRepeatSymbol.symbols.None) {
const rep = new VF.Repetition(toVexSymbol(sym), sym.xOffset + this.smoMeasure.staffX, sym.yOffset);
this.stave.getModifiers().push(rep);
}
const tms = this.smoMeasure.getMeasureText();
// TODO: set font
tms.forEach((tmb: SmoMeasureModifierBase) => {
const tm = tmb as SmoMeasureText;
const offset = tm.position === SmoMeasureText.positions.left ? this.smoMeasure.format.padLeft : 0;
const staveText = createStaveText(tm.text, toVexTextPosition(tm),
{
shiftX: tm.adjustX + offset, shiftY: tm.adjustY, justification: toVexTextJustification(tm)
}
);
this.stave?.addModifier(staveText);
// hack - we can't create staveText directly so this is the only way I could set the font
const ar = this.stave!.getModifiers();
const vm = ar[ar.length - 1];
vm.setFont(tm.fontInfo);
});
if (this.smoMeasure.svg.rowInSystem === 0) {
const rmb = this.smoMeasure.getRehearsalMark();
const rm = rmb as SmoRehearsalMark;
if (rm) {
this.stave.setSection(rm.symbol, 0);
}
}
const tempo = this.smoMeasure.getTempo();
if (tempo && this.smoMeasure.svg.forceTempo) {
this.stave.setTempo(tempo.toVexTempo(), -1 * tempo.yOffset);
const vexTempo = this.stave.getModifiers().find((mod: StaveModifier) => mod.getAttribute('type') === 'StaveTempo');
if (vexTempo) {
vexTempo.setFont({ family: SourceSerifProFont.fontFamily, size: 13, weight: 'bold' });
}
}
}
/**
* Create all Vex notes and modifiers. We defer the format and rendering so
* we can align across multiple staves
*/
preFormat() {
var j = 0;
if (this.smoMeasure.svg.element !== null) {
this.smoMeasure.svg.element.remove();
this.smoMeasure.svg.element = null;
if (this.smoMeasure.svg.tabElement) {
this.smoMeasure.svg.tabElement.remove();
this.smoMeasure.svg.tabElement = undefined;
}
}
if (this.smoMeasure.svg.hideEmptyMeasure) {
return;
}
// Note: need to do this to get it into VEX KS format
const staffX = this.smoMeasure.staffX + this.smoMeasure.format.padLeft;
const staffY = this.smoMeasure.staffY - this.context.box.y;
const key = SmoMusic.vexKeySignatureTranspose(this.smoMeasure.keySignature, 0);
const canceledKey = SmoMusic.vexKeySignatureTranspose(this.smoMeasure.canceledKeySignature, 0);
const smoVexStaveParams = {
x: staffX,
y: staffY,
padLeft: this.smoMeasure.format.padLeft,
id: this.smoMeasure.id,
staffX: this.smoMeasure.staffX,
staffY: this.smoMeasure.staffY,
staffWidth: this.smoMeasure.staffWidth,
forceClef: this.smoMeasure.svg.forceClef,
clef: this.smoMeasure.clef,
forceKey: this.smoMeasure.svg.forceKeySignature,
key,
canceledKey,
startX: this.smoMeasure.svg.maxColumnStartX,
adjX: this.smoMeasure.svg.adjX,
lines: this.smoMeasure.lines,
context: this.context.getContext()
}
this.stave = createStave(smoVexStaveParams);
if (this.smoMeasure.svg.forceTimeSignature) {
const ts = this.smoMeasure.timeSignature;
let tsString = ts.timeSignature;
if (this.smoMeasure.timeSignature.useSymbol && ts.actualBeats === 4 && ts.beatDuration === 4) {
tsString = 'C';
} else if (this.smoMeasure.timeSignature.useSymbol && ts.actualBeats === 2 && ts.beatDuration === 4) {
tsString = 'C|';
} else if (this.smoMeasure.timeSignature.displayString.length) {
tsString = this.smoMeasure.timeSignature.displayString;
}
this.stave.addTimeSignature(tsString);
}
// Connect it to the rendering context and draw!
this.stave.setContext(this.context.getContext());
if (this.smoTabStave && this.smoMeasure.svg.tabStaveBox?.width) {
const box = this.smoMeasure.svg.tabStaveBox;
let tabWidth = 0;
box.y -= this.context.box.y;
box.x = staffX - this.context.box.x;
box.width = this.smoMeasure.staffWidth;
this.tabStave = createTabStave(box, this.smoTabStave.spacing, this.smoTabStave.numLines);
if (this.smoMeasure.svg.forceClef) {
this.tabStave.addTabGlyph();
tabWidth = vexGlyph.dimensions['tab'].width;
}
this.tabStave.setNoteStartX(this.tabStave.getNoteStartX() + this.smoMeasure.svg.adjX - tabWidth);
this.tabStave.setContext(this.context.getContext());
}
this.createMeasureModifiers();
this.tickmapObject = this.smoMeasure.createMeasureTickmaps();
this.createCollisionTickmap();
this.voiceAr = [];
this.vexNotes = [];
this.noteToVexMap = {};
// If there are multiple voices, add them all to the formatter at the same time so they don't collide
for (j = 0; j < this.smoMeasure.voices.length; ++j) {
const smoVexVoiceParams = {
actualBeats: this.smoMeasure.timeSignature.actualBeats,
beatDuration: this.smoMeasure.timeSignature.beatDuration,
notes: this.vexNotes
}
if (!this.smoMeasure.svg.multimeasureLength && !this.smoMeasure.repeatSymbol) {
this.createVexNotes(j);
smoVexVoiceParams.notes = this.voiceNotes;
this.createVexTuplets(j);
this.createVexBeamGroups(j);
// Create a voice in 4/4 and add above notes
const voice = createVoice(smoVexVoiceParams);
this.voiceAr.push(voice);
}
if (this.smoMeasure.repeatSymbol) {
this.createRepeatSymbol();
// Create a voice in 4/4 and add above notes
const voice = createVoice(smoVexVoiceParams);
this.voiceAr.push(voice);
}
}
// Need to format for x position, then set y position before drawing dynamics.
this.formatter = new VF.Formatter({ softmaxFactor: this.softmax, globalSoftmax: false });
this.formatter.joinVoices(this.voiceAr);
if (this.tabStave) {
this.tabVoice = createVoice({
actualBeats: this.smoMeasure.timeSignature.actualBeats,
beatDuration: this.smoMeasure.timeSignature.beatDuration,
notes: this.tabNotes
});
this.formatter.joinVoices([this.tabVoice]);
}
}
/**
* Create the Vex formatter that calculates the X and Y positions of the notes. A formatter
* may actually span multiple staves for justified staves. The notes are drawn in their
* individual vxMeasure objects but formatting is done once for all justified staves
* @param voices Voice objects from VexFlow
* @returns
*/
format(voices: Voice[]) {
if (this.smoMeasure.svg.hideEmptyMeasure) {
return;
}
if (this.smoMeasure.svg.multimeasureLength > 0) {
this.multimeasureRest = getMultimeasureRest(this.smoMeasure.svg.multimeasureLength);
this.multimeasureRest.setContext(this.context.getContext());
this.multimeasureRest.setStave(this.stave);
return;
}
if (!this.formatter) {
return;
}
const timestamp = new Date().valueOf();
const staffWidth = this.smoMeasure.staffWidth -
(this.smoMeasure.svg.maxColumnStartX + this.smoMeasure.svg.adjRight + this.smoMeasure.format.padLeft) - 10;
this.dbgLeftX = this.smoMeasure.staffX + this.smoMeasure.format.padLeft + this.smoMeasure.svg.adjX;
this.dbgWidth = staffWidth;
this.formatter.format(voices, staffWidth);
if (this.tabVoice && this.tabNotes.length) {
this.formatter.format([this.tabVoice], staffWidth);
}
layoutDebug.setTimestamp(layoutDebug.codeRegions.FORMAT, new Date().valueOf() - timestamp);
}
/**
* render is called after format. Actually draw the things.
*/
render() {
if (this.smoMeasure.svg.hideEmptyMeasure) {
return;
}
var group = this.context.getContext().openGroup() as SVGSVGElement;
var mmClass = this.smoMeasure.getClassId();
var j = 0;
try {
// bound each measure in its own SVG group for easy deletion and mapping to screen coordinate
group.classList.add(this.smoMeasure.id);
group.classList.add(mmClass);
group.id = this.smoMeasure.id;
this.stave!.draw();
this.smoMeasure.svg.element = group;
for (j = 0; j < this.voiceAr.length; ++j) {
this.voiceAr[j].draw(this.context.getContext(), this.stave!);
}
this.vexBeamGroups.forEach((b) => {
b.setContext(this.context.getContext()).draw();
});
this.vexTuplets.forEach((tuplet) => {
tuplet.setContext(this.context.getContext()).draw();
});
if (this.multimeasureRest) {
this.multimeasureRest.draw();
}
// this._updateLyricDomSelectors();
this.renderDynamics();
// this.smoMeasure.adjX = this.stave.start_x - (this.smoMeasure.staffX);
this.context.getContext().closeGroup();
if (this.tabStave) {
const tabStaveId = `${this.smoMeasure.id}-tab`;
const tabGroup = this.context.getContext().openGroup() as SVGSVGElement;
tabGroup.classList.add(tabStaveId);
this.tabStave.draw();
this.tabVoice?.draw(this.context.getContext(), this.tabStave);
this.context.getContext().closeGroup();
this.smoMeasure.svg.tabElement = tabGroup;
}
// layoutDebug.setTimestamp(layoutDebug.codeRegions.RENDER, new Date().valueOf() - timestamp);
this.rendered = true;
if (layoutDebug.mask & layoutDebug.values['adjust']) {
SvgHelpers.debugBoxNoText(this.context.getContext().svg,
SvgHelpers.boxPoints(this.dbgLeftX,
this.smoMeasure.svg.staffY, this.dbgWidth, 40), 'render-x-dbg', 0);
}
} catch (exc) {
console.warn('unable to render measure ' + this.smoMeasure.measureNumber.measureIndex);
this.context.getContext().closeGroup();
}
}
}