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
661 lines (641 loc) • 26.9 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
import { RemoveElementLike, SvgBox, SvgPoint } from '../../smo/data/common';
import { SmoMeasure, SmoVoice } from '../../smo/data/measure';
import { SmoScore } from '../../smo/data/score';
import { SmoTextGroup } from '../../smo/data/scoreText';
import { SmoSelection } from '../../smo/xform/selections';
import { SmoSystemStaff } from '../../smo/data/systemStaff';
import { StaffModifierBase } from '../../smo/data/staffModifiers';
import { VxMeasure } from '../vex/vxMeasure';
import { SuiMapper } from './mapper';
import { VxSystem } from '../vex/vxSystem';
import { SvgHelpers, StrokeInfo } from './svgHelpers';
import { SuiPiano } from './piano';
import { SuiLayoutFormatter, RenderedPage } from './formatter';
import { SmoBeamer } from '../../smo/xform/beamers';
import { SuiTextBlock } from './textRender';
import { layoutDebug } from './layoutDebug';
import { SourceSansProFont } from '../../styles/font_metrics/ssp-sans-metrics';
import { SmoRenderConfiguration } from './configuration';
import { createTopDomContainer } from '../../common/htmlHelpers';
import { UndoBuffer } from '../../smo/xform/undo';
import { SvgPageMap, SvgPage } from './svgPageMap';
import { VexFlow } from '../../common/vex';
import { Note } from '../../common/vex';
declare var $: any;
const VF = VexFlow;
/**
* a renderer creates the SVG render context for vexflow from the given element. Then it
* renders the initial score.
* @category SuiRender
*/
export interface ScoreRenderParams {
elementId: any,
score: SmoScore,
config: SmoRenderConfiguration,
undoBuffer: UndoBuffer
}
/**
* @category SuiRender
*/
export interface MapParameters {
vxSystem: VxSystem, measuresToBox: SmoMeasure[], modifiersToBox: StaffModifierBase[], printing: boolean
}
/**
* This module renders the entire score. It calculates the layout first based on the
* computed dimensions.
* @category SuiRender
**/
export class SuiScoreRender {
constructor(params: ScoreRenderParams) {
this.elementId = params.elementId;
this.score = params.score;
this.vexContainers = new SvgPageMap(this.score.layoutManager!.globalLayout, this.elementId, this.score.layoutManager!.pageLayouts);
this.setViewport();
}
elementId: any;
startRenderTime: number = 0;
formatter: SuiLayoutFormatter | null = null;
vexContainers: SvgPageMap;
// vexRenderer: any = null;
score: SmoScore | null = null;
measureMapper: SuiMapper | null = null;
measuresToMap: MapParameters[] = [];
viewportChanged: boolean = false;
renderTime: number = 0;
backgroundRender: boolean = false;
static debugMask: number = 0;
renderedPages: Record<number, RenderedPage | null> = {};
_autoAdjustRenderTime: boolean = true;
lyricsToOffset: Map<number, VxSystem> = new Map();
renderingPage: number = -1;
get autoAdjustRenderTime() {
return this._autoAdjustRenderTime;
}
set autoAdjustRenderTime(value: boolean) {
this._autoAdjustRenderTime = value;
}
getRenderer(box: SvgBox | SvgPoint): SvgPage | null {
return this.vexContainers.getRenderer(box);
}
renderTextGroup(gg: SmoTextGroup) {
let ix = 0;
let jj = 0;
if (gg.skipRender || this.score === null || this.measureMapper === null) {
return;
}
gg.elements.forEach((element) => {
RemoveElementLike(element);
});
gg.elements = [];
const layoutManager = this.score!.layoutManager!;
const scaledScoreLayout = layoutManager.getScaledPageLayout(0);
// If this text hasn't been rendered before, estimate the logical box.
const dummyContainer = this.vexContainers.getRendererFromModifier(gg);
if (dummyContainer && !gg.logicalBox) {
const dummyBlock = SuiTextBlock.fromTextGroup(gg, dummyContainer, this.vexContainers, this.measureMapper!.scroller);
gg.logicalBox = dummyBlock.getLogicalBox();
}
// If this is a per-page score text, get a text group copy for each page.
// else the array contains the original.
const groupAr = SmoTextGroup.getPagedTextGroups(gg, this.score!.layoutManager!.pageLayouts.length, scaledScoreLayout.pageHeight);
groupAr.forEach((newGroup) => {
let container: SvgPage = this.vexContainers.getRendererFromModifier(newGroup);
// If this text is attached to the measure, base the block location on the rendered measure location.
if (newGroup.attachToSelector) {
// If this text is attached to a staff that is not visible, don't draw it.
let mappedStaff = this.score!.staves.find((staff) => staff.staffId === newGroup.selector!.staff);
if (this.score?.isPartExposed() && this.score.staves[0].partInfo.preserveTextGroups) {
mappedStaff = this.score!.staves.find((staff) =>
staff.getMappedStaffId() === newGroup.selector!.staff);
}
if (!mappedStaff) {
return;
}
// Indicate the new map;
// newGroup.selector.staff = mappedStaff.staffId;
const mmSel: SmoSelection | null = SmoSelection.measureSelection(this.score!, mappedStaff.staffId, newGroup.selector!.measure);
if (mmSel) {
const mm = mmSel.measure;
if (mm.svg.logicalBox.width > 0) {
const xoff = mm.svg.logicalBox.x + newGroup.musicXOffset;
const yoff = mm.svg.logicalBox.y + newGroup.musicYOffset;
newGroup.textBlocks[0].text.x = xoff;
newGroup.textBlocks[0].text.y = yoff;
}
}
}
if (container) {
const block = SuiTextBlock.fromTextGroup(newGroup, container, this.vexContainers, this.measureMapper!.scroller);
block.render();
if (block.currentBlock?.text.element) {
gg.elements.push(block.currentBlock?.text.element);
}
// For the first one we render, use that as the bounding box for all the text, for
// purposes of mapper/tracker
if (ix === 0) {
gg.logicalBox = JSON.parse(JSON.stringify(block.logicalBox));
// map all the child scoreText objects, too.
for (jj = 0; jj < gg.textBlocks.length; ++jj) {
gg.textBlocks[jj].text.logicalBox = JSON.parse(JSON.stringify(block.inlineBlocks[jj].text.logicalBox));
}
}
ix += 1;
}
});
}
// ### unrenderAll
// ### Description:
// Delete all the svg elements associated with the score.
unrenderAll() {
if (!this.score) {
return;
}
this.score.staves.forEach((staff) => {
this.unrenderStaff(staff);
});
// $(this.context.svg).find('g.lineBracket').remove();
}
// ### unrenderStaff
// ### Description:
// See unrenderMeasure. Like that, but with a staff.
unrenderStaff(staff: SmoSystemStaff) {
staff.measures.forEach((measure) => {
this.unrenderMeasure(measure);
});
staff.renderableModifiers.forEach((modifier) => {
if (modifier.element) {
modifier.element.remove();
modifier.element = null;
}
});
}
clearRenderedPage(pg: number) {
if (this.renderedPages[pg]) {
this.renderedPages[pg] = null;
}
}
// ### _setViewport
// Create (or recrate) the svg viewport, considering the dimensions of the score.
setViewport() {
if (this.score === null) {
return;
}
const layoutManager = this.score!.layoutManager!;
// All pages have same width/height, so use that
const layout = layoutManager.getGlobalLayout();
this.vexContainers.updateLayout(layout, layoutManager.pageLayouts);
this.renderedPages = {};
this.viewportChanged = true;
if (this.measureMapper) {
this.measureMapper.scroller.scrollAbsolute(0, 0);
}
if (this.measureMapper) {
this.measureMapper.scroller.updateViewport();
}
// this.context.setFont(this.font.typeface, this.font.pointSize, "").setBackgroundFillStyle(this.font.fillStyle);
if (SuiScoreRender.debugMask) {
console.log('layout setViewport: pstate initial');
}
}
async unrenderTextGroups(): Promise<void> {
return new Promise((resolve) => {
// remove existing modifiers, and also remove parent group for 'extra'
// groups associated with pagination (once per page etc)
for (var i = 0; i < this.score!.textGroups.length; ++i) {
const tg = this.score!.textGroups[i];
tg.elements.forEach((element) => {
RemoveElementLike(element);
});
tg.elements = [];
}
resolve();
});
}
async renderTextGroups(): Promise<void> {
return new Promise((resolve) => {
let tgs = this.score!.textGroups;
if (this.score?.isPartExposed() && this.score.staves[0].partInfo.preserveTextGroups) {
tgs = this.score.staves[0].partInfo.textGroups;
}
// group.classList.add('all-score-text');
for (var i = 0; i < tgs.length; ++i) {
const tg = tgs[i];
this.renderTextGroup(tg);
}
resolve();
});
}
async rerenderTextGroups(): Promise<void> {
await this.unrenderTextGroups();
await this.renderTextGroups();
}
/**
* for music we've just rendered, get the bounding boxes. We defer this step so we don't force
* a reflow, which can slow rendering.
* @param vxSystem
* @param measures
* @param modifiers
* @param printing
*/
measureRenderedElements(vxSystem: VxSystem, measures: SmoMeasure[], modifiers: StaffModifierBase[], printing: boolean) {
const pageContext = vxSystem.context;
measures.forEach((smoMeasure) => {
const element = smoMeasure.svg.element;
if (element) {
smoMeasure.setBox(pageContext.offsetBbox(element), 'vxMeasure bounding box');
}
const vxMeasure = vxSystem.getVxMeasure(smoMeasure);
if (vxMeasure) {
vxMeasure.modifiersToBox.forEach((modifier) => {
if (modifier.element) {
modifier.logicalBox = pageContext.offsetBbox(modifier.element);
}
});
}
// unit test codes don't have tracker.
if (this.measureMapper) {
const tmpStaff: SmoSystemStaff | undefined = this.score!.staves.find((ss) => ss.staffId === smoMeasure.measureNumber.staffId);
if (tmpStaff) {
this.measureMapper.mapMeasure(tmpStaff, smoMeasure, printing);
}
}
});
modifiers.forEach((modifier) => {
if (modifier.element) {
modifier.logicalBox = pageContext.offsetBbox(modifier.element);
}
});
}
_renderSystem(lineIx: number, printing: boolean) {
if (this.score === null || this.formatter === null) {
return;
}
const measuresToBox: SmoMeasure[] = [];
const modifiersToBox: StaffModifierBase[] = [];
const columns: Record<number, SmoMeasure[]> = this.formatter.systems[lineIx].systems;
// If this page hasn't changed since rendered
const pageIndex = columns[0][0].svg.pageIndex;
if (this.renderingPage !== pageIndex && this.renderedPages[pageIndex] && !printing) {
if (SuiScoreRender.debugMask) {
console.log(`skipping render on page ${pageIndex}`);
}
return;
}
const context = this.vexContainers.getRendererForPage(pageIndex);
if (this.renderingPage !== pageIndex) {
context.clearMap();
this.renderingPage = pageIndex;
}
const vxSystem: VxSystem = new VxSystem(context, 0, lineIx, this.score);
const colKeys = Object.keys(columns);
colKeys.forEach((colKey) => {
columns[parseInt(colKey, 10)].forEach((measure: SmoMeasure) => {
if (this.measureMapper !== null) {
const modId = 'mod-' + measure.measureNumber.staffId + '-' + measure.measureNumber.measureIndex;
SvgHelpers.removeElementsByClass(context.svg, modId);
vxSystem.renderMeasure(measure, printing);
const pageIndex = measure.svg.pageIndex;
const renderMeasures = this.renderedPages[pageIndex];
if (!renderMeasures) {
this.renderedPages[pageIndex] = {
startMeasure: measure.measureNumber.measureIndex,
endMeasure: measure.measureNumber.measureIndex
}
} else {
renderMeasures.endMeasure = measure.measureNumber.measureIndex;
}
measuresToBox.push(measure);
if (!printing && !measure.format.isDefault) {
const at: any[] = [];
at.push({ y: measure.svg.logicalBox.y - 5 });
at.push({ x: measure.svg.logicalBox.x + 25 });
at.push({ 'font-family': SourceSansProFont.fontFamily });
at.push({ 'font-size': '12pt' });
SvgHelpers.placeSvgText(context.svg, at, 'measure-format', '*');
}
}
});
});
this.score.staves.forEach((stf) => {
this.renderModifiers(stf, vxSystem).forEach((modifier) => {
modifiersToBox.push(modifier);
});
});
if (this.measureMapper !== null) {
vxSystem.renderEndings(this.measureMapper.scroller);
}
this.measuresToMap.push({vxSystem, measuresToBox, modifiersToBox, printing });
// this.measureRenderedElements(vxSystem, measuresToBox, modifiersToBox, printing);
const timestamp = new Date().valueOf();
if (!this.lyricsToOffset.has(vxSystem.lineIndex)) {
this.lyricsToOffset.set(vxSystem.lineIndex, vxSystem);
}
// vxSystem.updateLyricOffsets();
layoutDebug.setTimestamp(layoutDebug.codeRegions.POST_RENDER, new Date().valueOf() - timestamp);
}
_renderNextSystemPromise(systemIx: number, keys: number[], printing: boolean) {
return new Promise((resolve: any) => {
// const sleepDate = new Date().valueOf();
this._renderSystem(keys[systemIx], printing);
requestAnimationFrame(() => resolve());
});
}
async _renderNextSystem(lineIx: number, keys: number[], printing: boolean) {
createTopDomContainer('#renderProgress', 'progress');
if (lineIx < keys.length) {
const progress = Math.round((100 * lineIx) / keys.length);
$('#renderProgress').attr('max', 100);
$('#renderProgress').val(progress);
await this._renderNextSystemPromise(lineIx,keys, printing);
lineIx++;
await this._renderNextSystem(lineIx, keys, printing);
} else {
await this.rerenderTextGroups();
this.numberMeasures();
this.measuresToMap.forEach((mm) => {
this.measureRenderedElements(mm.vxSystem, mm.measuresToBox, mm.modifiersToBox, mm.printing);
});
this.lyricsToOffset.forEach((vv) => {
vv.updateLyricOffsets();
});
this.measuresToMap = [];
this.lyricsToOffset = new Map();
// We pro-rate the background render timer on how long it takes
// to actually render the score, so we are not thrashing on a large
// score.
if (this._autoAdjustRenderTime) {
this.renderTime = new Date().valueOf() - this.startRenderTime;
}
$('body').removeClass('show-render-progress');
// indicate the display is 'clean' and up-to-date with the score
$('body').removeClass('refresh-1');
if (this.measureMapper !== null) {
this.measureMapper.updateMap();
if (layoutDebug.mask & layoutDebug.values['artifactMap']) {
this.score?.staves.forEach((staff) => {
staff.measures.forEach((mm) => {
mm.voices.forEach((voice: SmoVoice) => {
voice.notes.forEach((note) => {
if (note.logicalBox) {
const page = this.vexContainers.getRendererFromPoint(note.logicalBox);
if (page) {
const noteBox = SvgHelpers.smoBox(note.logicalBox);
noteBox.y -= page.box.y;
SvgHelpers.debugBox(page.svg, noteBox, 'measure-place-dbg', 0);
}
}
});
});
});
});
}
}
this.backgroundRender = false;
}
}
// ### unrenderMeasure
// All SVG elements are associated with a logical SMO element. We need to erase any SVG element before we change a SMO
// element in such a way that some of the logical elements go away (e.g. when deleting a measure).
unrenderMeasure(measure: SmoMeasure) {
if (!measure) {
return;
}
const modId = 'mod-' + measure.measureNumber.staffId + '-' + measure.measureNumber.measureIndex;
const context = this.vexContainers.getRenderer(measure.svg.logicalBox);
if (!context) {
return;
}
SvgHelpers.removeElementsByClass(context.svg, modId);
if (measure.svg.element) {
measure.svg.element.remove();
measure.svg.element = null;
if (measure.svg.tabElement) {
measure.svg.tabElement.remove();
measure.svg.tabElement = undefined;
}
}
const renderPage = this.renderedPages[measure.svg.pageIndex];
if (renderPage) {
this.renderedPages[measure.svg.pageIndex] = null;
}
measure.setYTop(0, 'unrender');
}
// ### _renderModifiers
// ### Description:
// Render staff modifiers (modifiers straddle more than one measure, like a slur). Handle cases where the destination
// is on a different system due to wrapping.
renderModifiers(staff: SmoSystemStaff, system: VxSystem): StaffModifierBase[] {
let nextNote: SmoSelection | null = null;
let lastNote: SmoSelection | null = null;
let testNote: Note | null = null;
let vxStart: Note | null = null;
let vxEnd: Note | null = null;
const modifiersToBox: StaffModifierBase[] = [];
const removedModifiers: StaffModifierBase[] = [];
if (this.score === null || this.measureMapper === null) {
return [];
}
const renderedId: Record<string, boolean> = {};
staff.renderableModifiers.forEach((modifier) => {
let startNote = SmoSelection.noteSelection(this.score!,
modifier.startSelector.staff, modifier.startSelector.measure, modifier.startSelector.voice, modifier.startSelector.tick);
let endNote = SmoSelection.noteSelection(this.score!,
modifier.endSelector.staff, modifier.endSelector.measure, modifier.endSelector.voice, modifier.endSelector.tick);
if (!startNote || !endNote) {
// If the modifier doesn't have score endpoints, delete it from the score
removedModifiers.push(modifier);
return;
}
if (startNote.note !== null) {
vxStart = system.getVxNote(startNote.note);
}
if (endNote.note !== null) {
vxEnd = system.getVxNote(endNote.note);
}
// If the modifier goes to the next staff, draw what part of it we can on this staff.
if (vxStart && !vxEnd) {
nextNote = SmoSelection.nextNoteSelection(this.score!,
modifier.startSelector.staff, modifier.startSelector.measure, modifier.startSelector.voice, modifier.startSelector.tick);
if (nextNote === null) {
console.warn('bad selector ' + JSON.stringify(modifier.startSelector, null, ' '));
} else {
if (nextNote.note !== null) {
testNote = system.getVxNote(nextNote.note);
}
while (testNote) {
vxEnd = testNote;
endNote = nextNote;
nextNote = SmoSelection.nextNoteSelection(this.score!,
nextNote.selector.staff, nextNote.selector.measure, nextNote.selector.voice, nextNote.selector.tick);
if (!nextNote) {
break;
}
if (nextNote.note !== null) {
testNote = system.getVxNote(nextNote.note);
} else {
testNote = null;
}
}
}
}
if (vxEnd && !vxStart) {
lastNote = SmoSelection.lastNoteSelection(this.score!,
modifier.endSelector.staff, modifier.endSelector.measure, modifier.endSelector.voice, modifier.endSelector.tick);
if (lastNote !== null && lastNote.note !== null) {
testNote = system.getVxNote(lastNote.note);
while (testNote !== null) {
vxStart = testNote;
startNote = lastNote;
lastNote = SmoSelection.lastNoteSelection(this.score!,
lastNote.selector.staff, lastNote.selector.measure, lastNote.selector.voice, lastNote.selector.tick);
if (!lastNote) {
break;
}
if (lastNote.note !== null) {
testNote = system.getVxNote(lastNote.note);
} else {
testNote = null;
}
}
}
}
if (!vxStart && !vxEnd || renderedId[modifier.attrs.id]) {
return;
}
renderedId[modifier.attrs.id] = true;
system.renderModifier(this.measureMapper!.scroller, modifier, vxStart, vxEnd, startNote, endNote);
modifiersToBox.push(modifier);
});
// Silently remove modifiers from the score if the endpoints no longer exist
removedModifiers.forEach((mod) => {
staff.removeStaffModifier(mod);
});
return modifiersToBox;
}
drawPageLines() {
let i = 0;
const printing = $('body').hasClass('print-render');
const layoutMgr = this.score!.layoutManager;
if (printing || !layoutMgr) {
return;
}
for (i = 1; i < layoutMgr.pageLayouts.length; ++i) {
const context = this.vexContainers.getRendererForPage(i - 1);
if (context) {
$(context.svg).find('.pageLine').remove();
const scaledPage = layoutMgr.getScaledPageLayout(i);
const y = scaledPage.pageHeight * i - context.box.y;
SvgHelpers.line(context.svg, 0, y, scaledPage.pageWidth, y,
{ strokeName: 'line', stroke: '#321', strokeWidth: '2', strokeDasharray: '4,1', fill: 'none', opacity: 1.0 }, 'pageLine');
}
}
}
replaceSelection(staffMap: Record<number | string, { system: VxSystem, staff: SmoSystemStaff }>, change: SmoSelection) {
let system: VxSystem | null = null;
if (this.renderedPages[change.measure.svg.pageIndex]) {
this.renderedPages[change.measure.svg.pageIndex] = null;
}
SmoBeamer.applyBeams(change.measure);
const lineIndex = change.measure.svg.lineIndex;
// Defer modifier update until all selected measures are drawn.
if (!staffMap[lineIndex]) {
const context = this.vexContainers.getRenderer(change.measure.svg.logicalBox);
if (context) {
system = new VxSystem(context, change.measure.staffY, lineIndex, this.score!);
staffMap[lineIndex] = { system, staff: change.staff };
}
} else {
system = staffMap[lineIndex].system;
}
const selections = SmoSelection.measuresInColumn(this.score!, change.measure.measureNumber.measureIndex);
const measuresToMeasure: SmoMeasure[] = [];
selections.forEach((selection) => {
if (system !== null && this.measureMapper !== null) {
this.unrenderMeasure(selection.measure);
system.renderMeasure(selection.measure, false);
measuresToMeasure.push(selection.measure);
}
});
if (system) {
this.measureRenderedElements(system, measuresToMeasure, [], false);
}
}
async renderAllMeasures(lines: number[]) {
if (!this.score) {
return;
}
const printing = $('body').hasClass('print-render');
$('.measure-format').remove();
if (!printing) {
$('body').addClass('show-render-progress');
const isShowing = SuiPiano.isShowing;
if (this.score.preferences.showPiano && !isShowing) {
SuiPiano.showPiano();
this.measureMapper!.scroller.updateViewport();
} else if (isShowing && !this.score.preferences.showPiano) {
SuiPiano.hidePiano();
this.measureMapper!.scroller.updateViewport();
}
}
this.backgroundRender = true;
this.startRenderTime = new Date().valueOf();
this.renderingPage = -1;
this.vexContainers.updateContainerOffset(this.measureMapper!.scroller.scrollState);
await this._renderNextSystem(0, lines, printing);
}
// Number the measures at the first measure in each system.
numberMeasures() {
const printing: boolean = $('body').hasClass('print-render');
const staff = this.score!.staves[0];
const measures = staff.measures.filter((measure) => measure.measureNumber.systemIndex === 0);
$('.measure-number').remove();
measures.forEach((measure) => {
const context = this.vexContainers.getRenderer(measure.svg.logicalBox);
if (measure.measureNumber.localIndex > 0 && measure.measureNumber.systemIndex === 0 && measure.svg.logicalBox && context) {
const numAr: any[] = [];
const modBox = context.offsetSvgPoint(measure.svg.logicalBox);
numAr.push({ y: modBox.y - 10 });
numAr.push({ x: modBox.x });
numAr.push({ 'font-family': SourceSansProFont.fontFamily });
numAr.push({ 'font-size': '10pt' });
SvgHelpers.placeSvgText(context.svg, numAr, 'measure-number', (measure.measureNumber.localIndex + 1).toString());
// Show line-feed symbol
if (measure.format.systemBreak && !printing) {
const starAr: any[] = [];
const symbol = '\u21b0';
starAr.push({ y: modBox.y - 5 });
starAr.push({ x: modBox.x + 25 });
starAr.push({ 'font-family': SourceSansProFont.fontFamily });
starAr.push({ 'font-size': '12pt' });
SvgHelpers.placeSvgText(context.svg, starAr, 'measure-format', symbol);
}
}
});
}
/**
* This calculates the position of all the elements in the score, then renders the score
* @returns
*/
async layout() {
if (!this.score) {
return;
}
const score = this.score;
$('head title').text(this.score.scoreInfo.name);
const formatter = new SuiLayoutFormatter(score, this.vexContainers, this.renderedPages);
Object.keys(this.renderedPages).forEach((key) => {
this.vexContainers.clearModifiersForPage(parseInt(key));
});
const startPageCount = this.score.layoutManager!.pageLayouts.length;
this.formatter = formatter;
formatter.layout();
if (this.formatter.trimPages(startPageCount)) {
this.setViewport();
}
this.measuresToMap = [];
this.lyricsToOffset = new Map();
await this.renderAllMeasures(formatter.lines);
}
}