Spaces:
Sleeping
Sleeping
import {POS_PRECISION, constants} from "./utils"; | |
import type StaffToken from "./staffToken"; | |
import type {SheetStaff} from "./sheetDocument"; | |
import type TextSource from "../textSource"; | |
interface Rect { | |
left: number; | |
right: number; | |
top: number; | |
bottom: number; | |
}; | |
type IConnection = {y: number, height: number}; | |
class LineStack { | |
lines: StaffToken[]; | |
translation = {x: 0, y: 0}; | |
systemIndex?: number; | |
staffIndex?: number; | |
_rect?: Rect; | |
constructor (root) { | |
this.lines = [root]; | |
} | |
// the bottom line | |
get tip (): StaffToken { | |
return this.lines[this.lines.length - 1]; | |
} | |
get rect (): {left: number, right: number, top: number, bottom: number} { | |
if (!this._rect) { | |
const ys = this.lines.map(token => token.y + token.height / 2); | |
this._rect = { | |
left: Math.min(...this.lines.map(token => token.x)), | |
right: Math.max(...this.lines.map(token => token.x + token.width)), | |
top: ys[0], | |
bottom: ys[ys.length - 1], | |
}; | |
} | |
return this._rect; | |
} | |
tryAppend (line: StaffToken): boolean { | |
if (line.ry - this.tip.ry === 1 && Math.abs(line.x - this.tip.x) < 2) { | |
this.lines.push(line); | |
this._rect = null; | |
return true; | |
} | |
return false; | |
} | |
tryAttachConnection (connection: IConnection, index: number): boolean { | |
const {top, bottom} = this.rect; | |
//console.log("connection:", connection.y + connection.height, top - 1.2); | |
const y = connection.y + this.translation.y; | |
if (bottom + 1.6 > y && top - 1.6 < y + connection.height) { | |
this.systemIndex = index; | |
return true; | |
} | |
return false; | |
} | |
tryAttachStaff (y: number, index: number): boolean { | |
const {top, bottom} = this.rect; | |
y += this.translation.y; | |
if (bottom + 3.2 > y && top - 3.2 < y) { | |
this.staffIndex = index; | |
return true; | |
} | |
return false; | |
} | |
contains (token: StaffToken) { | |
const {left, right, top, bottom} = this.rect; | |
const x = token.x + this.translation.x; | |
const y = token.y + this.translation.y; | |
return x > left - 1.6 && x < right - 1 && y > top - 0.6 && y < bottom + 0.6; | |
} | |
translate ({x = 0, y = 0} = {}) { | |
this.translation.x += x; | |
this.translation.y += y; | |
} | |
}; | |
const parseAdditionalLineStacks = (tokens: StaffToken[]): LineStack[] => { | |
const lines = tokens.filter(token => token.is("ADDITIONAL_LINE")).sort((t1, t2) => t1.y - t2.y); | |
const stacks: LineStack[] = []; | |
lines.forEach(line => { | |
for (const stack of stacks) { | |
if (stack.tryAppend(line)) | |
return; | |
} | |
stacks.push(new LineStack(line)); | |
}); | |
return stacks; | |
}; | |
const tokensSystemsSplit = (tokens: StaffToken[], logger) => { | |
if (!tokens.length) { | |
logger.append("tokensSystemsSplit.emptyTokens"); | |
return []; | |
} | |
const pageHeight = Math.max(...tokens.map(token => token.y)); | |
const pageTile = Array(Math.round(pageHeight)).fill(-1); | |
let crossedCount = 0; | |
const connections: IConnection[] = tokens.filter(token => token.is("STAVES_CONNECTION")) as IConnection[]; | |
if (!connections.length) { | |
// single line system, split by staff lines | |
const lines = tokens.filter(token => token.is("STAFF_LINE")); | |
lines.forEach(line => pageTile[Math.round(line.y)] = 0); | |
// non-staff page | |
if (!lines.length) { | |
logger.append("tokensSystemsSplit.noConnetionsOrLines", {tokens}); | |
return []; | |
} | |
let index = -1; | |
let outStaff = true; | |
for (let y = 0; y < pageTile.length; ++y) { | |
const out = pageTile[y] < 0; | |
if (outStaff && !out) { | |
++index; | |
// append connection placeholder | |
connections.push({y, height: 4}); | |
} | |
if (!out) | |
pageTile[y] = index; | |
outStaff = out; | |
} | |
} | |
else { | |
connections.forEach((connection, i) => { | |
const start = Math.round(connection.y) - 1; | |
const end = Math.round(connection.y + connection.height) + 1; | |
let index = i - crossedCount; | |
for (let y = start; y <= end; ++y) { | |
if (pageTile[y] >= 0) { | |
index = pageTile[y]; | |
++crossedCount; | |
break; | |
} | |
} | |
for (let y = start; y <= end; ++y) | |
pageTile[y] = index; | |
}); | |
} | |
//logger.append("tokensSystemsSplit.pageTile.0", [...pageTile]); | |
//logger.append("tokensSystemsSplit.connections", connections); | |
const lineStacks = parseAdditionalLineStacks(tokens); | |
lineStacks.forEach(stack => { | |
for (let i = 0; i < connections.length; ++i) { | |
if (stack.tryAttachConnection(connections[i], i)) | |
break; | |
} | |
}); | |
//logger.append("tokensSystemsSplit.lineStacks", lineStacks); | |
const validLineStacks = lineStacks.filter(stack => stack.systemIndex >= 0); | |
if (validLineStacks.length < lineStacks.length) | |
logger.append("tokensSystemsSplit.invalidLineStacks", lineStacks.filter(stack => !(stack.systemIndex >= 0))); | |
// fill page tile by line stacks | |
validLineStacks.forEach(stack => { | |
const {top, bottom} = stack.rect; | |
for (let y = Math.floor(top) - 1; y < Math.ceil(bottom); ++y) { | |
if (pageTile[y] < 0) | |
pageTile[y] = stack.systemIndex; | |
} | |
}); | |
// fill interval between top tokens and system top | |
const topTokens = tokens.filter(token => token.topAtSystem); | |
topTokens.forEach(token => { | |
const nextIndex = pageTile.find((index, y) => y > token.y && index >= 0); | |
for (let y = Math.floor(token.y) - 1; y < pageHeight; ++y) { | |
if (pageTile[y] >= 0) | |
break; | |
pageTile[y] = nextIndex; | |
} | |
}); | |
//logger.append("tokensSystemsSplit.octaveAs", octaveAs); | |
// enlarge page tile by intersection stems | |
const interStems = tokens.filter(token => token.is("NOTE_STEM") | |
&& pageTile[Math.round(token.y)] === -1 | |
&& pageTile[Math.round(token.y)] !== pageTile[Math.round(token.y + token.height)]); | |
interStems.forEach(stem => { | |
const bottomIndex = pageTile[Math.round(stem.y + stem.height)]; | |
if (bottomIndex > 0) { | |
for (let y = Math.round(stem.y + stem.height) - 1; y >= Math.round(stem.y); --y) | |
pageTile[y] = bottomIndex; | |
} | |
}); | |
//logger.append("tokensSystemsSplit.pageTile.2", pageTile); | |
const systemBoundaries = pageTile.reduce((boundaries, index, y) => { | |
if (index >= boundaries.length) | |
boundaries.push(y - 1); | |
return boundaries; | |
}, []); | |
systemBoundaries[0] = -Infinity; | |
//logger.append("tokensSystemsSplit.systemBoundaries", systemBoundaries); | |
const systems = Array(systemBoundaries.length).fill(null).map(() => ({tokens: [], stacks: []})); | |
validLineStacks.forEach(stack => systems[stack.systemIndex] && systems[stack.systemIndex].stacks.push(stack)); | |
//logger.append("tokensSystemsSplit.validLineStacks", {systems, validLineStacks}); | |
tokens.forEach(token => { | |
for (const stack of validLineStacks) { | |
if (stack.contains(token)) { | |
if (systems[stack.systemIndex]) { | |
systems[stack.systemIndex].tokens.push(token); | |
return; | |
} | |
else | |
logger.append("tokensSystemsSplit.invalidStackSystemIndex", {stack, systems}); | |
} | |
} | |
if (token.withUp || token.withDown) { | |
let index = 0; | |
if (token.withUp) | |
index = connections.filter(c => c.y + c.height < token.y).length; | |
else | |
index = Math.max(connections.filter(c => c.y < token.y).length - 1, 0); | |
if (systems[index]) | |
systems[index].tokens.push(token); | |
else | |
console.warn("tokensSystemsSplit: invalid system index:", index, systems.length, token.source); | |
return; | |
} | |
const y = token.logicY; | |
for (let i = 0; i < systemBoundaries.length; ++i) { | |
if (y >= systemBoundaries[i] && (i >= systemBoundaries.length - 1 || y < systemBoundaries[i + 1])) { | |
systems[i].tokens.push(token); | |
return; | |
} | |
} | |
}); | |
systems.forEach(system => system.tokens = system.tokens.sort((t1, t2) => t1.logicX - t2.logicX)); | |
return systems; | |
}; | |
const parseChordsByStems = (tokens: StaffToken[], logger) => { | |
const stems = tokens.filter(token => token.is("NOTE_STEM")); | |
const notes = tokens.filter(token => token.is("NOTEHEAD") || token.is("TEMPO_NOTEHEAD")); | |
stems.forEach(stem => { | |
const rightAttached = notes.filter(note => stem.stemAttached({ | |
x: note.x, | |
y: note.y + constants.NOTE_TYPE_JOINT_Y[note.noteType] * (note.scale || 1), | |
href: note.href, | |
})); | |
const leftAttached = notes.filter(note => stem.stemAttached({ | |
x: note.x + constants.NOTE_TYPE_WIDTHS[note.noteType] * (note.scale || 1), | |
y: note.y - constants.NOTE_TYPE_JOINT_Y[note.noteType] * (note.scale || 1), | |
href: note.href, | |
})); | |
if (rightAttached.length + leftAttached.length <= 0) { | |
logger.append("parseChordsByStems.baldStem:", stem); | |
//console.warn("bald stem:", stem); | |
stem.addSymbol("BALD"); | |
return; | |
} | |
const ys = [...rightAttached.map(n => n.y), ...leftAttached.map(n => n.y)]; | |
const top = Math.abs(stem.y - Math.min(...ys)); | |
const bottom = Math.abs(stem.y + stem.height - Math.max(...ys)); | |
const up = top < bottom; | |
//console.assert(up || leftAttached.length, "unexpected stem, downwards but no left-attached notes."); | |
//console.assert(!up || rightAttached.length, "unexpected stem, upwards but no right-attached notes."); | |
stem.stemUp = !up; | |
const anchorNote = up ? rightAttached[0] : leftAttached[0]; | |
const anchorToken = anchorNote || stem; | |
const assign = note => { | |
note.stemX = anchorToken.x; | |
note.stemUp = !up; | |
note.stems = note.stems || []; | |
note.stems.push(stem.index); | |
}; | |
rightAttached.forEach(assign); | |
leftAttached.forEach(assign); | |
if (!anchorNote) { | |
stem.addSymbol("NOTICE"); | |
logger.append("parseChordsByStems.unexpectedStem", {stem, ys, rightAttached, leftAttached}); | |
} | |
else if (anchorNote.is("HALF")) | |
stem.division = 1; | |
}); | |
}; | |
const isSystemToken = token => token.is("STAVES_CONNECTION") || token.is("BRACE") || token.is("VERTICAL_LINE"); | |
//const roundJoin = (x, y) => `${Math.round(x)},${Math.round(y)}`; | |
const parseTokenSystem = (tokens: StaffToken[], stacks: LineStack[], logger) => { | |
const separatorYs : Set<number> = new Set(); | |
const measureSeparators = tokens.filter(token => token.is("MEASURE_SEPARATOR")); | |
measureSeparators.forEach(token => separatorYs.add(token.ry)); | |
//logger.append("parseTokenSystem.measureSeparators", Array.from(measureSeparators)); | |
// remove separator Y from fake MEASURE_SEPARATOR | |
for (const y of Array.from(separatorYs).sort()) { | |
if (separatorYs.has(y - 4) && separatorYs.has(y + 4)) { | |
separatorYs.delete(y); | |
measureSeparators.filter(token => token.ry === y).forEach(token => { | |
token.removeSymbol("MEASURE_SEPARATOR"); | |
token.addSymbol("VERTICAL_LINE"); | |
}); | |
} | |
} | |
//logger.append("parseTokenSystem.separatorYs", Array.from(separatorYs)); | |
const staffLines = tokens.filter(token => token.is("STAFF_LINE")).reduce((lines, token) => { | |
if (!lines[token.ry] || lines[token.ry].x > token.x) | |
lines[token.ry] = token; | |
return lines; | |
}, {}); | |
//logger.append("parseTokenSystem.staffLines", Object.keys(staffLines)); | |
// construct staff Y from staff lines when no separators | |
if (!separatorYs.size) { | |
const ys = Object.keys(staffLines).map(Number); | |
const topLineYs = ys.filter(y => staffLines[y + 3] && staffLines[y + 4]); | |
topLineYs.forEach(y => separatorYs.add(y)); | |
} | |
const staffYs = Array.from(separatorYs) | |
.filter(y => staffLines[y] || staffLines[y + POS_PRECISION]) | |
.map(y => staffLines[y] ? y : y + POS_PRECISION).map(y => y + 2) | |
.sort((y1, y2) => y1 - y2) | |
.filter(y => staffLines[y - 2] && staffLines[y] && staffLines[y + 2]); | |
//logger.append("parseTokenSystem.staffYs", staffYs); | |
const additionalLines = tokens.filter(token => token.is("ADDITIONAL_LINE")).sort((l1, l2) => l1.y - l2.y); | |
const additionalLinesYs = additionalLines.reduce((ys, token) => { | |
ys.add(token.ry); | |
return ys; | |
}, new Set()); | |
//logger.append("parseTokenSystem.additionalLinesYs", Array.from(additionalLinesYs)); | |
/*for (const y of staffYs) { | |
console.assert(staffLines[y - 2] && staffLines[y] && staffLines[y + 2], | |
"no corresponding staff lines for separator", y - 2, Object.keys(staffLines)); | |
}*/ | |
const systemY = staffYs[0] - 2; | |
const systemX = staffLines[systemY] && staffLines[systemY].rx; | |
const noteYs = tokens | |
.filter(token => token.is("NOTE") && !token.is("TEMPO_NOTEHEAD")) | |
.map(token => token.ry) | |
.concat(Object.keys(staffLines).map(Number)); | |
const top = Math.min(...noteYs) - systemY; | |
const bottom = Math.max(...noteYs) - systemY; | |
//console.log("additionalLinesYs:", additionalLinesYs); | |
const splitters = []; | |
for (let i = 0; i < staffYs.length - 1; ++i) { | |
let up = staffYs[i] + 2; | |
while (additionalLinesYs.has(up + 1)) | |
++up; | |
let down = staffYs[i + 1] - 2; | |
while (additionalLinesYs.has(down - 1)) | |
--down; | |
//const splitter = Math.min(Math.max((staffYs[i] + staffYs[i + 1]) / 2, up + 1), down - 1) - systemY; | |
const splitter = (up + down) / 2 - systemY; | |
splitters.push(splitter); | |
//logger.append("parseTokenSystem.splitter", {splitter, up, down, systemY}); | |
} | |
splitters.push(Infinity); | |
stacks.forEach(stack => { | |
for (let i = 0; i < staffYs.length; ++i) { | |
if (stack.tryAttachStaff(staffYs[i], i)) | |
return; | |
} | |
}); | |
const findStaffByStacks = token => { | |
for (const stack of stacks) { | |
if (stack.contains(token)) | |
return stack.staffIndex; | |
} | |
}; | |
const localTokens = tokens.map(token => token.translate({x: -systemX, y: -systemY})); | |
const stems = localTokens.filter(token => token.is("NOTE_STEM")); | |
stems.forEach(stem => stem.division = 2); | |
const slashes = localTokens.filter(token => token.is("LINE") && token.target && token.target.x > 0 && token.target.y < 0); | |
const backSlashes = localTokens.filter(token => token.is("LINE") && token.target && token.target.x > 0 && token.target.y > 0); | |
const staffTokens = []; | |
//console.log("splitters:", splitters); | |
const appendToken = (token: StaffToken) => { | |
if (token.is("BEAM")) { | |
const jointStems = stems.filter(stem => Math.abs(stem.centerX - token.x) < 0.1 | |
&& (Math.abs(token.y - stem.y) < 0.2 || Math.abs(token.y - (stem.y + stem.height)) < 0.2)); | |
const k = (token.target.y - token.start.y) / (token.target.x - token.start.x); | |
const contactedStems = stems.filter(stem => { | |
const dy = (stem.x - (token.x + token.start.x)) * k; | |
return stem.centerX - (token.x + token.start.x) > -0.1 && stem.centerX - (token.x + token.target.x) < 0.1 | |
&& token.y + dy - stem.y > -0.2 && token.y + dy - (stem.y + stem.height) < 0.2; | |
}); | |
if (!contactedStems.length) { | |
token.removeSymbol("NOTETAIL"); | |
token.removeSymbol("JOINT"); | |
} | |
else { | |
token.stems = contactedStems.map(stem => stem.index); | |
if (jointStems.length) | |
token.addSymbol("CAPITAL_BEAM"); | |
const k = (token.target.y - token.start.y) / (token.target.x - token.start.x); | |
// append stem division | |
const crossedStems = stems.filter(stem => | |
stem.centerX > token.x - 0.1 && stem.centerX < token.x + token.target.x + 0.1 | |
&& stem.y < Math.max(token.y, token.y + token.target.y) + 0.2 | |
&& stem.y + stem.height > Math.min(token.y, token.y + token.target.y) - 0.2); | |
crossedStems.forEach(stem => { | |
const beamY = (stem.centerX - token.x + token.start.x) * k + token.y + token.start.y; | |
if (beamY > stem.y - 0.2 && beamY < stem.y + stem.height + 0.2) { | |
const atTip = stem.stemUp ? beamY < stem.y + 3.2 : beamY > stem.y + stem.height - 3.2; | |
if (atTip) { | |
++stem.division; | |
if (token.is("CAPITAL_BEAM")) | |
stem.beam = token.index; | |
} | |
} | |
}); | |
} | |
} | |
if (token.is("FLAG UP")) { | |
const stem = stems.find(stem => Math.abs(stem.x + stem.width - token.x) < 0.04 && Math.abs(stem.y - token.y) < 0.1); | |
if (stem) { | |
token.stem = stem.index; | |
stem.division = token.flagNumber; | |
} | |
else | |
token.addSymbol("SUSPENDED"); | |
} | |
if (token.is("FLAG DOWN")) { | |
const stem = stems.find(stem => Math.abs(stem.x + stem.width - token.x) < 0.04 && Math.abs(stem.y + stem.height - token.y) < 0.1); | |
if (stem) { | |
token.stem = stem.index; | |
stem.division = token.flagNumber; | |
} | |
else | |
token.addSymbol("SUSPENDED"); | |
} | |
if (slashes.includes(token)) { | |
const partner = backSlashes.find(t => t.x === token.x && t.target.y === - token.target.y); | |
if (partner) { | |
if (token.y <= partner.y) { | |
token.addSymbol("WEDGE CRESCENDO TOP"); | |
partner.addSymbol("WEDGE CRESCENDO BOTTOM"); | |
} | |
else if (token.y > partner.y) { | |
token.addSymbol("WEDGE DECRESCENDO BOTTOM"); | |
partner.addSymbol("WEDGE DECRESCENDO TOP"); | |
} | |
} | |
} | |
let index = 0; | |
if (token.withUp || token.withDown) { | |
if (token.withUp) | |
index = staffYs.filter(sy => sy + 2 < token.y + systemY).length; | |
else if (token.withDown) | |
index = Math.max(staffYs.filter(sy => sy - 2 < token.y + systemY).length - 1, 0); | |
} | |
else { | |
let y = token.logicY; | |
//const indexInMap = indicesMap[roundJoin(token.x + systemX, y + systemY)]; | |
const indexByStacks = findStaffByStacks(token); | |
if (Number.isInteger(indexByStacks)) | |
index = indexByStacks; | |
else { | |
// affiliate beam to a stem | |
if (token.is("NOTETAIL") && token.is("JOINT")) { | |
const stem = stems.find(stem => Math.abs(stem.centerX - token.x) < 0.1 | |
&& token.y > stem.y - 0.2 && token.y < stem.y + stem.height + 0.2); | |
if (stem) | |
y = stem.logicY; | |
//else | |
// console.debug("isolated beam:", token); | |
} | |
//if (token.is("NOTEHEAD")) | |
// console.log("omit note:", token.href, roundJoin(token.x + systemX, y + systemY)); | |
while (y > splitters[index]) | |
++index; | |
} | |
} | |
staffTokens[index] = staffTokens[index] || []; | |
staffTokens[index].push(token); | |
}; | |
stacks.forEach(stack => stack.translate({x: systemX, y: systemY})); | |
//logger.append("parseTokenSystem.stacks", stacks); | |
parseChordsByStems(localTokens, logger); | |
localTokens | |
.filter(token => !isSystemToken(token)) | |
.forEach(appendToken); | |
// measure ranges | |
const notes = localTokens.filter(token => token.is("NOTE")); | |
const separatorXsRaw = Array.from(new Set(localTokens | |
.filter(token => token.is("MEASURE_SEPARATOR")) | |
.map(token => token.logicX))).sort((x1: number, x2: number) => x1 - x2); | |
// supplement for empty measure separator staff, maybe some lilypond bug if not at end. | |
if (!separatorXsRaw.length) | |
separatorXsRaw.push(localTokens[localTokens.length - 1].x + 1); | |
const measureRanges = separatorXsRaw.map((x, i) => { | |
const left = i > 0 ? separatorXsRaw[i - 1] : -Infinity; | |
return { | |
x, | |
notes: notes.filter(note => note.x > left && note.x < x), | |
}; | |
}).filter(({notes}) => notes.length).map(({x, notes}) => ({ | |
headX: notes[0].x - 1.5, | |
noteRange: {begin: notes[0].x, end: x}, | |
})); | |
//logger.append("parseTokenSystem.measureRanges", measureRanges); | |
return { | |
x: systemX, | |
y: systemY, | |
top, | |
bottom, | |
tokens: localTokens.filter(isSystemToken), | |
staves: staffYs.map((y, i) => staffTokens[i] && parseTokenStaff({ | |
tokens: staffTokens[i], | |
y: y - systemY, | |
top: splitters[i] - (y - systemY), | |
measureRanges, | |
logger, | |
})), | |
}; | |
}; | |
const isStaffToken = token => token.is("STAFF_LINE") || token.is("MEASURE_SEPARATOR"); | |
const parseTokenStaff = ({tokens, y, top, measureRanges, logger}): SheetStaff => { | |
const localTokens = tokens.map(token => token.translate({y: -y})); | |
const notes = localTokens.filter(token => token.is("NOTE")); | |
//logger.append("parseTokenStaff.localTokens", localTokens); | |
const headX = measureRanges[0] ? measureRanges[0].headX : 0; | |
const alters = localTokens.filter(token => token.is("ALTER")); | |
let lastAlter = null; | |
// mark key alters | |
for (const alter of alters) { | |
// far distance alter may be chordmode element | |
if (alter.y > 3 || alter.y < -3) | |
continue; | |
if ((alter.source && alter.source.substr(0, 4) === "\\key")) | |
lastAlter = alter; | |
// break key chain at large gap | |
else if (lastAlter && alter.x - lastAlter.x > 2) | |
break; | |
else if (alter.x < headX) | |
lastAlter = alter; | |
// continue key chain | |
else if (lastAlter && alter.x - lastAlter.x < 1.2) | |
lastAlter = alter; | |
else | |
break; | |
alter.addSymbol("KEY"); | |
} | |
// affiliate accidental alters to notes | |
const accs = alters.filter(alter => !alter.is("KEY") && !alter.href); | |
accs.forEach(alter => { | |
const notehead = notes.find(note => note.ry === alter.ry && note.x > alter.x && note.x - alter.x < 5); | |
if (notehead) | |
alter.stemX = notehead.logicX - constants.EPSILON; | |
else { | |
alter.addSymbol("NOTICE"); | |
logger.append("orphanAlter", alter); | |
} | |
}); | |
//logger.append("measureRanges:", {measureRanges, accs}); | |
const measures = measureRanges.map((range, i) => { | |
const left = i > 0 ? measureRanges[i - 1].noteRange.end : -Infinity; | |
const tokens = localTokens.filter(token => !isStaffToken(token) && token.logicX > left | |
&& (token.logicX < range.noteRange.end || i === measureRanges.length - 1)) | |
.sort((t1, t2) => t1.logicX - t2.logicX); | |
const leftNoteX = Math.min(...tokens.filter(token => token.is("NOTE")).map(note => note.x), left + 2.9); | |
// mark volta repeat dots | |
const dots = tokens.filter(token => token.is("DOT") && Math.abs(token.ry) === 0.5); | |
const dotsL = dots.filter(dot => dot.x < leftNoteX); // double lines will enlarge left line interval | |
const dotsR = dots.filter(dot => dot.x > range.noteRange.end - 1); | |
[dotsL, dotsR].forEach((pair, i) => { | |
if (pair.length === 2 && pair[0].ry * pair[1].ry < 0) { | |
pair.forEach(dot => { | |
dot.addSymbol(i ? "RIGHT" : "LEFT"); | |
dot.addSymbol("VOLTA"); | |
}); | |
} | |
}); | |
return { | |
tokens, | |
noteRange: range.noteRange, | |
headX: range.headX, | |
}; | |
}); | |
const headWidth = measures[0] ? measures[0].headX : 0; | |
return { | |
x: 0, y, | |
headWidth, | |
top, | |
tokens: localTokens.filter(isStaffToken), | |
measures, | |
}; | |
}; | |
const isPageToken = token => token.is("TEXT") && !token.source; | |
const organizeTokens = (tokens: StaffToken[], source: TextSource, {logger, viewBox, width, height}: any = {}) => { | |
//logger.append("organizeTokens", tokens); | |
// added source on tokens | |
tokens.forEach(token => { | |
const pos = token.sourcePosition; | |
if (pos) { | |
//token.source = lyLines[pos.line - 1].substr(pos.start, Math.max(pos.end - pos.start, 8)); | |
token.source = source.slice(pos.line, [pos.start, Math.max(pos.end, pos.start + 8)]); | |
// enlarge token source range for command tokens | |
if (/^\\/.test(token.source)) { | |
for (let len = token.source.length + 1; len < 80; ++len) { | |
const captures = token.source.match(/\s+/g); | |
if (captures && captures.length >= 2) | |
break; | |
token.source = source.slice(pos.line, [pos.start, pos.start + len]); | |
} | |
} | |
} | |
}); | |
const meaningfulTokens = tokens.filter(token => !token.is("NULL")); | |
//logger.append("organizeTokens.meaningfulTokens", meaningfulTokens); | |
const pageTokens = meaningfulTokens.filter(isPageToken); | |
meaningfulTokens.forEach(token => { | |
if (token.source) { | |
// process tempo noteheads | |
if (token.source.substr(0, 6) === "\\tempo" && token.is("NOTEHEAD")) { | |
token.removeSymbol("NOTEHEAD"); | |
token.addSymbol("TEMPO_NOTEHEAD"); | |
} | |
// process ped dot | |
if (token.is("DOT") && /^\\sustain/.test(token.source)) { | |
token.removeSymbol("DOT"); | |
token.addSymbol("SUSTAIN", "PED_DOT"); | |
} | |
// tremolo beams pierced | |
if (token.is("BEAM") && /^:\d+/.test(token.source)) { | |
token.removeSymbol("NOTETAIL"); | |
token.removeSymbol("JOINT"); | |
token.addSymbol("TREMOLO_BEAM"); | |
token.addSymbol("PIERCED"); | |
} | |
// tremolo beams paired | |
if (token.is("BEAM") && /repeat tremolo/.test(token.source)) { | |
token.removeSymbol("NOTETAIL"); | |
token.addSymbol("TREMOLO_BEAM"); | |
token.addSymbol("TREMOLO_PAIR"); | |
} | |
// glissando | |
if (/^\\glissando/.test(token.source)) { | |
token.removeSymbol("TR_WAVE"); | |
token.addSymbol("GLISSANDO"); | |
} | |
// arpeggio | |
if (/^\\arpeggio/.test(token.source)) | |
token.addSymbol("ARPEGGIO"); | |
} | |
}); | |
const systemDatas = tokensSystemsSplit(meaningfulTokens.filter(token => !isPageToken(token)), logger); | |
//logger.append("organizeTokens.systemDatas", systemDatas); | |
const systems = systemDatas.map(({tokens, stacks}) => parseTokenSystem(tokens, stacks, logger)) | |
.filter(system => system.staves.length > 0); | |
return { | |
tokens: pageTokens, | |
systems, | |
viewBox, | |
width, | |
height, | |
}; | |
}; | |
export default organizeTokens; | |