Spaces:
Sleeping
Sleeping
| import {CM_TO_PX} from "../constants"; | |
| import {roundNumber} from "./utils"; | |
| // eslint-disable-next-line | |
| import StaffToken from "./staffToken"; | |
| // eslint-disable-next-line | |
| import * as LilyNotation from "../lilyNotation"; | |
| import pick from "../pick"; | |
| interface SheetMarkingData { | |
| id: string; | |
| text: string; | |
| x: number; | |
| y: number; | |
| cls: string; | |
| } | |
| export interface SheetMeasure { | |
| index: number; | |
| tokens: StaffToken[]; | |
| headX: number; | |
| lineX?: number; | |
| matchedTokens?: StaffToken[]; // for baking mode | |
| noteRange: { | |
| begin: number, | |
| end: number, | |
| }; | |
| class?: {[key: string]: boolean}; | |
| }; | |
| export interface SheetStaff { | |
| measures: SheetMeasure[]; | |
| tokens: StaffToken[]; | |
| markings?: Partial<SheetMarkingData>[]; | |
| // the third staff line Y coordinate value | |
| // The third staff line Y supposed to be zero, but regarding to the line stroke width, | |
| // there is some error for original values in SVG document (which erased by coordinate rounding). | |
| yRoundOffset?: number; // 0.0657 for default | |
| x: number; | |
| y: number; | |
| top?: number; | |
| headWidth?: number; | |
| }; | |
| export interface SheetSystem { | |
| index?: number; | |
| pageIndex?: number; | |
| measureIndices?: [number, number][]; // [end_x, index] | |
| staves: SheetStaff[]; | |
| tokens: StaffToken[]; | |
| x: number; | |
| y: number; | |
| width?: number; | |
| top: number; | |
| bottom: number; | |
| }; | |
| export interface SheetPage { | |
| width: string; | |
| height: string; | |
| viewBox: { | |
| x: number, | |
| y: number, | |
| width: number, | |
| height: number, | |
| }; | |
| systems: SheetSystem[]; | |
| tokens: StaffToken[]; | |
| hidden?: boolean; | |
| // DEPRECATED | |
| rows?: SheetSystem[]; | |
| }; | |
| /*const ALTER_PREFIXES = { | |
| [-2]: "\u266D\u266D", | |
| [-1]: "\u266D", | |
| [0]: "\u266E", | |
| [1]: "\u266F", | |
| [2]: "\uD834\uDD2A", | |
| };*/ | |
| // char codes defined in music font | |
| const ALTER_PREFIXES = { | |
| [-2]: "\ue02a", | |
| [-1]: "\ue021", | |
| [0]: "\ue01d", | |
| [1]: "\ue013", | |
| [2]: "\ue01c", | |
| }; | |
| let sheetMarkingIndex = 0; | |
| class SheetMarking { | |
| alter?: number; | |
| index: number; // as v-for key | |
| id?: string; | |
| text?: string; | |
| x?: number; | |
| y?: number; | |
| cls?: string; | |
| constructor (fields: Partial<SheetMarkingData>) { | |
| this.index = sheetMarkingIndex++; | |
| Object.assign(this, fields); | |
| } | |
| get alterText (): string { | |
| return Number.isInteger(this.alter) ? ALTER_PREFIXES[this.alter] : null; | |
| } | |
| }; | |
| const parseUnitExp = exp => { | |
| if (/[\d.]+mm/.test(exp)) { | |
| const [value] = exp.match(/[\d.]+/); | |
| return Number(value) * 0.1 * CM_TO_PX; | |
| } | |
| return Number(exp); | |
| }; | |
| type MeasureLocationTable = {[key: number]: {[key: number]: number}}; | |
| const cc = <T>(arrays: T[][]): T[] => [].concat(...arrays); | |
| class SheetDocument { | |
| pages: SheetPage[]; | |
| constructor (fields: Partial<SheetDocument>, {initialize = true} = {}) { | |
| Object.assign(this, fields); | |
| if (initialize) | |
| this.updateTokenIndex(); | |
| } | |
| get systems (): SheetSystem[] { | |
| return [].concat(...this.pages.map(page => page.systems)); | |
| } | |
| // DEPRECATED | |
| get rows (): SheetSystem[] { | |
| return this.systems; | |
| } | |
| get trackCount (): number{ | |
| return Math.max(...this.systems.map(system => system.staves.length), 0); | |
| } | |
| get pageSize (): {width: number, height: number} { | |
| const page = this.pages && this.pages[0]; | |
| if (!page) | |
| return null; | |
| return { | |
| width: parseUnitExp(page.width), | |
| height: parseUnitExp(page.height), | |
| }; | |
| } | |
| updateTokenIndex () { | |
| // remove null pages for broken document | |
| this.pages = this.pages.filter(page => page); | |
| this.pages.forEach((page, index) => page.systems.forEach(system => system.pageIndex = index)); | |
| let rowMeasureIndex = 1; | |
| this.systems.forEach((system, index) => { | |
| system.index = index; | |
| system.width = system.tokens.concat(...system.staves.map(staff => staff.tokens)) | |
| .reduce((max, token) => Math.max(max, token.x), 0); | |
| system.measureIndices = []; | |
| system.staves = system.staves.filter(s => s); | |
| system.staves.forEach((staff, t) => { | |
| staff.measures.forEach((measure, i) => { | |
| measure.index = rowMeasureIndex + i; | |
| measure.class = {}; | |
| measure.tokens.forEach(token => { | |
| token.system = index; | |
| token.measure = measure.index; | |
| token.endX = measure.noteRange.end; | |
| }); | |
| measure.lineX = measure.lineX || 0; | |
| if (i < staff.measures.length - 1) | |
| staff.measures[i + 1].lineX = measure.noteRange.end; | |
| if (t === 0) | |
| system.measureIndices.push([measure.noteRange.end, measure.index]); | |
| }); | |
| staff.markings = []; | |
| staff.yRoundOffset = 0; | |
| const line = staff.tokens.find(token => token.is("STAFF_LINE")); | |
| if (line) | |
| staff.yRoundOffset = line.y - line.ry; | |
| }); | |
| rowMeasureIndex += Math.max(...system.staves.map(staff => staff.measures.length)); | |
| }); | |
| } | |
| updateMatchedTokens (matchedIds: Set<string>) { | |
| this.systems.forEach(system => { | |
| system.staves.forEach(staff => | |
| staff.measures.forEach(measure => { | |
| measure.matchedTokens = measure.tokens.filter(token => token.href && matchedIds.has(token.href)); | |
| if (!staff.yRoundOffset) { | |
| const token = measure.matchedTokens[0]; | |
| if (token) | |
| staff.yRoundOffset = token.y - token.ry; | |
| } | |
| })); | |
| }); | |
| } | |
| addMarking (systemIndex: number, staffIndex: number, data: Partial<SheetMarkingData>): SheetMarking { | |
| const system = this.systems[systemIndex]; | |
| if (!system) { | |
| console.warn("system index out of range:", systemIndex, this.systems.length); | |
| return; | |
| } | |
| const staff = system.staves[staffIndex]; | |
| if (!staff) { | |
| console.warn("staff index out of range:", staffIndex, system.staves.length); | |
| return; | |
| } | |
| const marking = new SheetMarking(data); | |
| staff.markings.push(marking); | |
| return marking; | |
| } | |
| removeMarking (id: string) { | |
| this.systems.forEach(system => system.staves.forEach(staff => | |
| staff.markings = staff.markings.filter(marking => marking.id !== id))); | |
| } | |
| clearMarkings () { | |
| this.systems.forEach(system => system.staves.forEach(staff => staff.markings = [])); | |
| } | |
| toJSON (): object { | |
| return { | |
| __prototype: "SheetDocument", | |
| pages: this.pages, | |
| }; | |
| } | |
| getLocationTable (): MeasureLocationTable { | |
| const table = {}; | |
| this.systems.forEach(system => system.staves.forEach(staff => staff.measures.forEach(measure => { | |
| measure.tokens.forEach(token => { | |
| if (token.href) { | |
| const location = token.href.match(/\d+/g); | |
| if (location) { | |
| const [line, column] = location.map(Number); | |
| table[line] = table[line] || {}; | |
| table[line][column] = Number.isFinite(table[line][column]) ? Math.min(table[line][column], measure.index) : measure.index; | |
| } | |
| else | |
| console.warn("invalid href:", token.href); | |
| } | |
| }); | |
| }))); | |
| return table; | |
| } | |
| lookupMeasureIndex (systemIndex: number, x: number): number { | |
| const system = this.systems[systemIndex]; | |
| if (!system || !system.measureIndices) | |
| return null; | |
| const [_, index] = system.measureIndices.find(([end]) => x < end) || [null, null]; | |
| return index; | |
| } | |
| tokensInSystem (systemIndex: number): StaffToken[] { | |
| const system = this.systems[systemIndex]; | |
| if (!system) | |
| return null; | |
| return system.staves.reduce((tokens, staff) => { | |
| const translate = token => token.translate({x: staff.x, y: staff.y}); | |
| tokens.push(...staff.tokens.map(translate)); | |
| staff.measures.forEach(measure => tokens.push(...measure.tokens.map(translate))); | |
| return tokens; | |
| }, [...system.tokens]); | |
| } | |
| tokensInPage (pageIndex: number, {withPageTokens = false} = {}): StaffToken[] { | |
| const page = this.pages[pageIndex]; | |
| if (!page) | |
| return null; | |
| return page.systems.reduce((tokens, system) => { | |
| tokens.push(...this.tokensInSystem(system.index).map(token => token.translate({x: system.x, y: system.y}))); | |
| return tokens; | |
| }, withPageTokens ? [...page.tokens] : []); | |
| } | |
| fitPageViewbox ({margin = 5, verticalCropOnly = false, pageTokens = false} = {}) { | |
| if (!this.pages || !this.pages.length) | |
| return; | |
| const svgScale = this.pageSize.width / this.pages[0].viewBox.width; | |
| this.pages.forEach((page, i) => { | |
| const rects = page.systems.filter(system => Number.isFinite(system.x + system.width + system.y + system.top + system.bottom)) | |
| .map(system => [system.x, system.x + system.width, system.y + system.top, system.y + system.bottom ]); | |
| const tokens = this.tokensInPage(i, {withPageTokens: pageTokens}) || []; | |
| const tokenXs = tokens.map(token => token.x).filter(Number.isFinite); | |
| const tokenYs = tokens.map(token => token.y).filter(Number.isFinite); | |
| //console.debug("tokens:", i, tokens, tokenXs, tokenYs); | |
| if (!rects.length) | |
| return; | |
| const left = Math.min(...rects.map(rect => rect[0]), ...tokenXs); | |
| const right = Math.max(...rects.map(rect => rect[1]), ...tokenXs); | |
| const top = Math.min(...rects.map(rect => rect[2]), ...tokenYs); | |
| const bottom = Math.max(...rects.map(rect => rect[3]), ...tokenYs); | |
| const x = verticalCropOnly ? page.viewBox.x : left - margin; | |
| const y = (verticalCropOnly && i === 0) ? page.viewBox.y : top - margin; | |
| const width = verticalCropOnly ? page.viewBox.width : right - left + margin * 2; | |
| const height = (verticalCropOnly && i === 0) ? bottom + margin - y : bottom - top + margin * 2; | |
| page.viewBox = {x, y, width, height}; | |
| page.width = (page.viewBox.width * svgScale).toString(); | |
| page.height = (page.viewBox.height * svgScale).toString(); | |
| }); | |
| } | |
| getTokensOf (symbol: string): StaffToken[] { | |
| return this.systems.reduce((tokens, system) => { | |
| system.staves.forEach(staff => staff.measures.forEach(measure => | |
| tokens.push(...measure.tokens.filter(token => token.is(symbol))))); | |
| return tokens; | |
| }, []); | |
| } | |
| getNoteHeads (): StaffToken[] { | |
| return this.getTokensOf("NOTEHEAD"); | |
| } | |
| getNotes (): StaffToken[] { | |
| return this.getTokensOf("NOTE"); | |
| } | |
| getTokenMap (): Map<string, StaffToken> { | |
| return this.systems.reduce((tokenMap, system) => { | |
| system.staves.forEach(staff => staff.measures.forEach(measure => measure.tokens | |
| .filter(token => token.href) | |
| .forEach(token => tokenMap.set(token.href, token)))); | |
| return tokenMap; | |
| }, new Map<string, StaffToken>()); | |
| } | |
| findTokensAround (token: StaffToken, indices: number[]): StaffToken[] { | |
| const system = this.systems[token.system]; | |
| if (system) { | |
| const tokens = [ | |
| ...system.tokens, | |
| ...cc(system.staves.map(staff => [ | |
| ...staff.tokens, | |
| ...cc(staff.measures.map(measure => measure.tokens)), | |
| ])), | |
| ]; | |
| return tokens.filter(token => indices.includes(token.index)); | |
| } | |
| return null; | |
| } | |
| findTokenAround (token: StaffToken, index: number): StaffToken { | |
| const results = this.findTokensAround(token, [index]); | |
| return results && results[0]; | |
| } | |
| alignTokensWithNotation (notation: LilyNotation.Notation, {partial = false, assignFlags = false} = {}) { | |
| const shortId = (href: string): string => href.split(":").slice(0, 2).join(":"); | |
| const noteTokens = this.getNotes(); | |
| const tokenMap = noteTokens.reduce((map, token) => { | |
| const sid = token.href && shortId(token.href); | |
| const tokens = map.get(sid) || []; | |
| // shift column for command chord element | |
| if (/^\\/.test(token.source)) { | |
| const spaceCapture = token.source.match(/(?<=\s+)(\S|$)/); | |
| if (spaceCapture) { | |
| const [line, column] = token.href.match(/\d+/g).map(Number); | |
| map.set(`${line}:${column + spaceCapture.index}`, [token]); | |
| return map; | |
| } | |
| else | |
| console.warn("unresolved command chord element:", token.source, token); | |
| } | |
| tokens.push(token); | |
| token.href && map.set(sid, tokens); | |
| return map; | |
| }, new Map<string, StaffToken[]>()); | |
| //console.assert(tokenMap.size === noteTokens.length, "tokens noteTokens count dismatch:", tokenMap.size, noteTokens.length); | |
| const tokenTickMap = new Map<StaffToken, {measureTick: number, tick: number}>(); | |
| // assign tick & track | |
| notation.measures.forEach((measure, mi) => { | |
| const pendingStems = new Map<StaffToken, StaffToken>(); // stem -> beam | |
| measure.notes.forEach(note => { | |
| const tokens = tokenMap.get(shortId(note.id)); | |
| if (tokens) { | |
| tokens.forEach(token => { | |
| token.href = note.id; | |
| if (!Number.isFinite(token.tick)) { | |
| tokenTickMap.set(token, {measureTick: measure.tick, tick: measure.tick + note.tick}); | |
| token.pitch = note.pitch; | |
| token.track = note.track; | |
| if (token.stems) { | |
| const stems = this.findTokensAround(token, token.stems); | |
| if (stems) { | |
| const stem = stems.find(stem => stem.division === note.division && !Number.isFinite(stem.track)); | |
| if (stem) { | |
| stem.track = note.track; | |
| if (stem.beam >= 0) { | |
| const beam = this.findTokenAround(stem, stem.beam); | |
| if (stems.length < 2 || stems[0].division !== stems[1].division) | |
| beam.track = stem.track; | |
| } | |
| } | |
| else if (!stems.find(stem => stem.division === note.division)) | |
| console.warn("missed stem:", mi, token.href, note.division, token.stems, stems.map(stem => stem.division)); | |
| stems.forEach(stem => { | |
| tokenTickMap.set(stem, {measureTick: measure.tick, tick: measure.tick + note.tick}); | |
| if (stems.length > 1 && stem.beam >= 0) { | |
| const beam = this.findTokenAround(stem, stem.beam); | |
| if (beam) | |
| pendingStems.set(stem, beam); | |
| } | |
| }); | |
| } | |
| else | |
| console.warn("stems token missing:", token.system, token.stems, mi, token.href); | |
| } | |
| } | |
| }); | |
| } | |
| else if (!partial) | |
| note.overlapped = true; | |
| }); | |
| //if (pendingStems.size) | |
| // console.log("pendingStems:", mi, [...pendingStems].map(s => s.index)); | |
| for (const [stem, beam] of pendingStems) { | |
| if (Number.isFinite(beam.track)) | |
| stem.track = beam.track; | |
| } | |
| }); | |
| const tokenTickMapKeys = Array.from(tokenTickMap.keys()); | |
| this.systems.forEach(system => { | |
| system.staves.forEach(staff => staff.measures.forEach(measure => { | |
| const tokens = measure.tokens.filter(token => tokenTickMapKeys.includes(token)); | |
| const meastureTick = tokens.reduce((tick, token) => Math.min(tokenTickMap.get(token).measureTick, tick), Infinity); | |
| tokens.forEach(token => token.tick = tokenTickMap.get(token).tick - meastureTick); | |
| })); | |
| }); | |
| if (assignFlags) | |
| this.assignFlagsTrack(); | |
| } | |
| assignFlagsTrack () { | |
| const flags = this.getTokensOf("FLAG"); | |
| flags.forEach(flag => { | |
| if (Number.isFinite(flag.stem)) { | |
| const stem = this.findTokenAround(flag, flag.stem); | |
| if (stem && Number.isFinite(stem.track)) | |
| flag.track = stem.track; | |
| } | |
| }); | |
| } | |
| pruneForBakingMode () { | |
| const round = x => roundNumber(x, 1e-4); | |
| this.pages.forEach(page => { | |
| page.tokens = []; | |
| page.systems.forEach(system => { | |
| system.tokens = []; | |
| system.measureIndices = system.measureIndices && system.measureIndices.map(([x, i]) => [round(x), i]); | |
| system.staves.forEach(staff => { | |
| staff.tokens = []; | |
| staff.yRoundOffset = round(staff.yRoundOffset); | |
| delete staff.top; | |
| delete staff.headWidth; | |
| staff.measures.forEach(measure => { | |
| measure.headX = round(measure.headX); | |
| measure.lineX = round(measure.lineX); | |
| measure.noteRange = { | |
| begin: round(measure.noteRange.begin), | |
| end: round(measure.noteRange.end), | |
| }; | |
| measure.tokens = measure.matchedTokens.map(token => new StaffToken(pick(token, [ | |
| "x", "y", "symbol", "href", "scale", "tied", | |
| ]))); | |
| delete measure.matchedTokens; | |
| }); | |
| }); | |
| }); | |
| }); | |
| } | |
| appendLinkedTokensForStaves (): void { | |
| const doneTokens = new Set(); | |
| const appendLink = (staff: SheetStaff, oldStaff: SheetStaff, token: StaffToken): void => { | |
| if (doneTokens.has(token.index)) | |
| return; | |
| //console.log("appendLink:", staff, oldStaff, token); | |
| const dy = staff.y - oldStaff.y; | |
| const measure = staff.measures.find(measure => measure.noteRange.end >= token.x); | |
| if (measure) { | |
| const newToken = new StaffToken({...token, symbols: new Set(), y: token.y - dy, ry: token.ry - dy}); | |
| token.addSymbol("ACROSS_STAVES"); | |
| newToken.addSymbol("ACROSS_STAVES"); | |
| newToken.addSymbol("DUPLICATED"); | |
| measure.tokens.push(newToken); | |
| } | |
| else | |
| console.warn("appendLink failed, because no fit measure:", staff.measures, token); | |
| doneTokens.add(token.index); | |
| }; | |
| this.pages.forEach(page => { | |
| const tokens: StaffToken[] = (page.systems | |
| .map(system => system.staves | |
| .map(staff => staff.measures | |
| .map(measure => measure.tokens))) as any).flat(3); | |
| const tokenStaffTable: Record<number, SheetStaff> = page.systems | |
| .reduce((table, system) => system.staves | |
| .reduce((table, staff) => staff.measures | |
| .reduce((table, measure) => measure.tokens | |
| .reduce((table, token) => { | |
| table[token.index] = staff; | |
| return table; | |
| }, table), table), table), {}); | |
| //console.log("tokenStaffTable:", tokenStaffTable); | |
| tokens.forEach(token => { | |
| if (token.stems) { | |
| const staff = tokenStaffTable[token.index]; | |
| token.stems.forEach(stem => { | |
| if (tokenStaffTable[stem] !== staff) | |
| appendLink(tokenStaffTable[stem], staff, token); | |
| }); | |
| } | |
| }); | |
| }); | |
| } | |
| }; | |
| export default SheetDocument; | |