UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

274 lines (273 loc) 11.7 kB
import * as util from '../util'; import { DurationRange } from './durationrange'; export class DefaultCursorFrame { tRangeSources; xRangeSources; yRangeSources; activeElements; describer; constructor(tRangeSources, xRangeSources, yRangeSources, activeElements, describer) { this.tRangeSources = tRangeSources; this.xRangeSources = xRangeSources; this.yRangeSources = yRangeSources; this.activeElements = activeElements; this.describer = describer; } static create(log, score, timeline, span, elementDescriber) { const partCount = score.getPartCount(); if (partCount === 0) { log.warn('No parts found in score, returning empty cursor frames.'); return []; } if (0 > span.fromPartIndex || span.fromPartIndex >= partCount) { throw new Error(`Invalid fromPartIndex: ${span.fromPartIndex}, must be in [0,${partCount - 1}]`); } if (0 > span.toPartIndex || span.toPartIndex >= partCount) { throw new Error(`Invalid toPartIndex: ${span.toPartIndex}, must be in [0,${partCount - 1}]`); } const factory = new CursorFrameFactory(log, score, timeline, span, elementDescriber); return factory.create(); } get tRange() { const t1 = this.tRangeSources[0].moment.time; const t2 = this.tRangeSources[1].moment.time; return new DurationRange(t1, t2); } get xRange() { const x1 = getXRangeBound(this.xRangeSources[0]); const x2 = getXRangeBound(this.xRangeSources[1]); return new util.NumberRange(x1, x2); } get yRange() { const y1 = getYRangeBound(this.yRangeSources[0]); const y2 = getYRangeBound(this.yRangeSources[1]); return new util.NumberRange(y1, y2); } getActiveElements() { return [...this.activeElements]; } toHumanReadable() { const tRangeDescription = this.describer.describeTRange(this.tRangeSources); const xRangeDescription = this.describer.describeXRange(this.xRangeSources); const yRangeDescription = this.describer.describeYRange(this.yRangeSources); return [`t: ${tRangeDescription}`, `x: ${xRangeDescription}`, `y: ${yRangeDescription}`]; } } class CursorFrameFactory { log; score; timeline; span; frames = new Array(); activeElements = new Set(); describer; constructor(log, score, timeline, span, elementDescriber) { this.log = log; this.score = score; this.timeline = timeline; this.span = span; this.describer = new CursorFrameDescriber(elementDescriber); } create() { this.frames = []; this.activeElements = new Set(); for (let index = 0; index < this.timeline.getMomentCount() - 1; index++) { const currentMoment = this.timeline.getMoment(index); const nextMoment = this.timeline.getMoment(index + 1); util.assertNotNull(currentMoment); util.assertNotNull(nextMoment); const tRangeSources = this.getTRangeSources(currentMoment, nextMoment); const xRangeSources = this.getXRangeSources(currentMoment, nextMoment); const yRangeSources = this.getYRangeSources(currentMoment); this.updateActiveElements(currentMoment); this.addFrame(tRangeSources, xRangeSources, yRangeSources); } return this.frames; } getTRangeSources(currentMoment, nextMoment) { return [{ moment: currentMoment }, { moment: nextMoment }]; } getXRangeSources(currentMoment, nextMoment) { const startXRangeSource = this.getStartXRangeSource(currentMoment); const endXRangeSource = this.getEndXRangeSource(startXRangeSource, nextMoment); return [startXRangeSource, endXRangeSource]; } getStartXRangeSource(moment) { const hasStartingTransition = moment.events.some((e) => e.type === 'transition' && e.kind === 'start'); if (hasStartingTransition) { return this.getLeftmostStartingXRangeSource(moment); } this.log.warn('No starting transition found for moment, ' + 'but the moment is trying to be used as a starting anchor. ' + 'How was the moment created?', { momentTimeMs: moment.time.ms }); const event = moment.events.at(0); util.assertDefined(event); switch (event.type) { case 'transition': return { type: 'element', element: event.element, bound: 'left' }; case 'systemend': return { type: 'system', system: event.system, bound: 'left' }; case 'jump': return { type: 'measure', measure: event.measure, bound: 'left' }; } } getEndXRangeSource(startXRangeSource, nextMoment) { let proposedXRangeSource; const shouldUseMeasureEndBoundary = nextMoment.events.some((e) => e.type === 'jump' || e.type === 'systemend'); if (shouldUseMeasureEndBoundary) { const event = nextMoment.events.at(0); util.assertDefined(event); switch (event.type) { case 'transition': proposedXRangeSource = { type: 'measure', measure: event.measure, bound: 'right' }; break; case 'systemend': proposedXRangeSource = { type: 'system', system: event.system, bound: 'right' }; break; case 'jump': proposedXRangeSource = { type: 'measure', measure: event.measure, bound: 'right' }; break; } } else { proposedXRangeSource = this.getStartXRangeSource(nextMoment); } const startBound = getXRangeBound(startXRangeSource); const proposedBound = getXRangeBound(proposedXRangeSource); // Ensure that the proposed X range source is to the right of the start X range source. If it's not, we'll fall back // to the start X range source's right bound (since we know the start X range source is based on the left bound). if (proposedBound >= startBound) { return proposedXRangeSource; } else { this.log.warn('Proposed end X range source is to the left of the start X range source. ' + "Falling back to the start X range source's right bound.", { momentTimeMs: nextMoment.time.ms }); return { ...startXRangeSource, bound: 'right' }; } } getLeftmostStartingXRangeSource(currentMoment) { const elements = currentMoment.events .filter((e) => e.type === 'transition') .filter((e) => e.kind === 'start') .map((e) => e.element); let min = Infinity; let leftmost = undefined; for (const element of elements) { const left = element.rect().left(); if (left < min) { min = left; leftmost = element; } } util.assertDefined(leftmost); return { type: 'element', element: leftmost, bound: 'left' }; } getYRangeSources(currentMoment) { const systemIndex = this.getSystemIndex(currentMoment); const parts = this.score .getSystems() .at(systemIndex) .getMeasures() .flatMap((measure) => measure.getFragments()) .flatMap((fragment) => fragment.getParts()); const topPart = parts.find((part) => part.getIndex() === this.span.fromPartIndex); const bottomPart = parts.find((part) => part.getIndex() === this.span.toPartIndex); util.assertDefined(topPart); util.assertDefined(bottomPart); return [ { part: topPart, bound: 'top' }, { part: bottomPart, bound: 'bottom' }, ]; } getSystemIndex(currentMoment) { const events = currentMoment.events.toSorted((a, b) => { const kindOrder = { start: 0, stop: 1 }; if (a.type === 'transition' && b.type === 'transition') { return kindOrder[a.kind] - kindOrder[b.kind]; } const typeOrder = { transition: 0, systemend: 1, jump: 2 }; return typeOrder[a.type] - typeOrder[b.type]; }); for (const event of events) { switch (event.type) { case 'transition': return event.measure.getSystemIndex(); case 'systemend': return event.system.getIndex(); case 'jump': return event.measure.getSystemIndex(); } } util.assertUnreachable(); } updateActiveElements(moment) { for (const event of moment.events) { if (event.type === 'transition') { if (event.kind === 'start') { this.activeElements.add(event.element); } else if (event.kind === 'stop') { this.activeElements.delete(event.element); } } } } addFrame(tRangeSources, xRangeSources, yRangeSources) { const frame = new DefaultCursorFrame(tRangeSources, xRangeSources, yRangeSources, [...this.activeElements], this.describer); this.frames.push(frame); } } class CursorFrameDescriber { elementDescriber; constructor(elementDescriber) { this.elementDescriber = elementDescriber; } describeTRange(tRangeSources) { return `[${tRangeSources[0].moment.time.ms}ms - ${tRangeSources[1].moment.time.ms}ms]`; } describeXRange(xRangeSources) { return `[${this.describeXRangeSource(xRangeSources[0])} - ${this.describeXRangeSource(xRangeSources[1])}]`; } describeYRange(yRangeSources) { return `[${this.describeYRangeSource(yRangeSources[0])} - ${this.describeYRangeSource(yRangeSources[1])}]`; } describeXRangeSource(source) { switch (source.type) { case 'system': return `${source.bound}(${this.elementDescriber.describe(source.system)})`; case 'measure': return `${source.bound}(${this.elementDescriber.describe(source.measure)})`; case 'element': return `${source.bound}(${this.elementDescriber.describe(source.element)})`; } } describeYRangeSource(source) { return `${source.bound}(system(${source.part.getSystemIndex()}), ${this.elementDescriber.describe(source.part)})`; } } function getXRangeBound(source) { const rect = getXRangeRect(source); switch (source.type) { case 'system': return source.bound === 'left' ? rect.left() : rect.right(); case 'measure': return source.bound === 'left' ? rect.left() : rect.right(); case 'element': return source.bound === 'left' ? rect.left() : rect.right(); } } function getXRangeRect(source) { switch (source.type) { case 'system': return (source.system.getMeasures().at(0)?.getFragments().at(0)?.getParts().at(0)?.getStaves().at(0)?.intrinsicRect() ?? source.system.rect()); case 'measure': return (source.measure.getFragments().at(0)?.getParts().at(0)?.getStaves().at(0)?.intrinsicRect() ?? source.measure.rect()); case 'element': return source.element.rect(); } } function getYRangeBound(source) { return source.bound === 'top' ? source.part.rect().top() : source.part.rect().bottom(); }