refactor: functional style in exporter.js and subobject-kinds.js
This commit is contained in:
@@ -20,12 +20,12 @@ function _serializeShape(shape) {
|
|||||||
|
|
||||||
// RFC §3.2.1.1.6-7: FM = FREQUENCY ["f"/"F"] ["@" OSC] ["[" SHAPE "]"] ";" MOD ":" BASE
|
// RFC §3.2.1.1.6-7: FM = FREQUENCY ["f"/"F"] ["@" OSC] ["[" SHAPE "]"] ";" MOD ":" BASE
|
||||||
|
|
||||||
function _serializeModulation(m) {
|
function _serializeModulation(modulation) {
|
||||||
let s = String(m.frequency ?? '');
|
let s = String(modulation.frequency ?? '');
|
||||||
if (m.oscillator) s += `@${m.oscillator}`;
|
if (modulation.oscillator) s += `@${modulation.oscillator}`;
|
||||||
if (m.shape) s += `[${_serializeShape(m.shape)}]`;
|
if (modulation.shape) s += `[${_serializeShape(modulation.shape)}]`;
|
||||||
s += `;${m.mod_share ?? ''}:${m.base_share ?? ''}`;
|
s += `;${modulation.mod_share ?? ''}:${modulation.base_share ?? ''}`;
|
||||||
if (m.init_phase != null) s += m.init_phase >= 0 ? `+${m.init_phase}` : String(m.init_phase);
|
if (modulation.init_phase != null) s += modulation.init_phase >= 0 ? `+${modulation.init_phase}` : String(modulation.init_phase);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,17 +33,15 @@ function _serializeModulation(m) {
|
|||||||
|
|
||||||
function _basicPropLines(bp) {
|
function _basicPropLines(bp) {
|
||||||
if (!bp) return [];
|
if (!bp) return [];
|
||||||
const lines = [];
|
const a = _serializeShape(bp.A), s = _serializeShape(bp.S), r = _serializeShape(bp.R);
|
||||||
if (bp.oscillator) lines.push(`O: ${bp.oscillator}`);
|
return [
|
||||||
const a = _serializeShape(bp.A);
|
bp.oscillator ? `O: ${bp.oscillator}` : null,
|
||||||
if (a) lines.push(`A: "${a}"`);
|
a ? `A: "${a}"` : null,
|
||||||
const s = _serializeShape(bp.S);
|
s ? `S: "${s}"` : null,
|
||||||
if (s) lines.push(`S: "${s}"`);
|
r ? `R: "${r}"` : null,
|
||||||
const r = _serializeShape(bp.R);
|
...(bp.fmModulations ?? []).map(fm => `FM: "${_serializeModulation(fm)}"`),
|
||||||
if (r) lines.push(`R: "${r}"`);
|
...(bp.amModulations ?? []).map(am => `AM: "${_serializeModulation(am)}"`),
|
||||||
for (const fm of (bp.fmModulations ?? [])) lines.push(`FM: "${_serializeModulation(fm)}"`);
|
].filter(Boolean);
|
||||||
for (const am of (bp.amModulations ?? [])) lines.push(`AM: "${_serializeModulation(am)}"`);
|
|
||||||
return lines;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC §3.2.1.2: label name (3+ lowercase chars) is the MAPPING KEY directly.
|
// RFC §3.2.1.2: label name (3+ lowercase chars) is the MAPPING KEY directly.
|
||||||
@@ -56,21 +54,22 @@ function _labelSpecLines(ls) {
|
|||||||
|
|
||||||
// RFC §3.2.1.3: VOLUMES, TIMBRE are variation properties, not instrument-level.
|
// RFC §3.2.1.3: VOLUMES, TIMBRE are variation properties, not instrument-level.
|
||||||
|
|
||||||
function _variationLines(v) {
|
function _variationLines(variation) {
|
||||||
const lines = [];
|
const rc = _serializeShape(variation.railsbackCurve);
|
||||||
if (v.dependsOn) lines.push(`ATTR: ${v.dependsOn}`);
|
const vol = _serializeShape(variation.volumes);
|
||||||
lines.push(..._basicPropLines(v.basicProperties));
|
const timbre = _serializeShape(variation.timbre);
|
||||||
for (const ls of (v.labelSpecs ?? [])) lines.push(..._labelSpecLines(ls));
|
return [
|
||||||
if (v.spread?.length) lines.push(`SPREAD: [${v.spread.join(', ')}]`);
|
variation.dependsOn ? `ATTR: ${variation.dependsOn}` : null,
|
||||||
if (v.railsbackCurve) { const rc = _serializeShape(v.railsbackCurve); if (rc) lines.push(`RAILSBACK_CURVE: "${rc}"`); }
|
..._basicPropLines(variation.basicProperties),
|
||||||
const vol = _serializeShape(v.volumes);
|
...(variation.labelSpecs ?? []).flatMap(_labelSpecLines),
|
||||||
if (vol) lines.push(`VOLUMES: "${vol}"`);
|
variation.spread?.length ? `SPREAD: [${variation.spread.join(', ')}]` : null,
|
||||||
const timbre = _serializeShape(v.timbre);
|
rc ? `RAILSBACK_CURVE: "${rc}"` : null,
|
||||||
if (timbre) lines.push(`TIMBRE: "${timbre}"`);
|
vol ? `VOLUMES: "${vol}"` : null,
|
||||||
for (const fm of (v.fmModulations ?? [])) lines.push(`FM: "${_serializeModulation(fm)}"`);
|
timbre ? `TIMBRE: "${timbre}"` : null,
|
||||||
for (const am of (v.amModulations ?? [])) lines.push(`AM: "${_serializeModulation(am)}"`);
|
...(variation.fmModulations ?? []).map(fm => `FM: "${_serializeModulation(fm)}"`),
|
||||||
for (const sv of (v.subvariations ?? [])) lines.push(..._variationLines(sv));
|
...(variation.amModulations ?? []).map(am => `AM: "${_serializeModulation(am)}"`),
|
||||||
return lines;
|
...(variation.subvariations ?? []).flatMap(_variationLines),
|
||||||
|
].filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
// VOLUMES, TIMBRE, FM are variation properties (RFC §3.2.1.3). The AST parser
|
// VOLUMES, TIMBRE, FM are variation properties (RFC §3.2.1.3). The AST parser
|
||||||
@@ -106,14 +105,11 @@ function _instrCharacterLines(instr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Multiple variations — RFC MAYBE_LIST<VARIATION> as YAML sequence.
|
// Multiple variations — RFC MAYBE_LIST<VARIATION> as YAML sequence.
|
||||||
const result = [];
|
return allVariations.flatMap(v => {
|
||||||
for (const v of allVariations) {
|
|
||||||
const vLines = _variationLines(v);
|
const vLines = _variationLines(v);
|
||||||
if (!vLines.length) continue;
|
if (!vLines.length) return [];
|
||||||
result.push(` - ${vLines[0]}`);
|
return [` - ${vLines[0]}`, ...vLines.slice(1).map(l => ` ${l}`)];
|
||||||
for (const l of vLines.slice(1)) result.push(` ${l}`);
|
});
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC §4.4: embedded instrument key is "instrument NAME:" not "instrument: 'NAME'"
|
// RFC §4.4: embedded instrument key is "instrument NAME:" not "instrument: 'NAME'"
|
||||||
@@ -136,14 +132,12 @@ function _serializeArticleValue(v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _buildArticlesBlock(articles) {
|
function _buildArticlesBlock(articles) {
|
||||||
const lines = ['articles:'];
|
const body = articles.flatMap(art => {
|
||||||
for (const art of articles) {
|
|
||||||
const props = (art.properties ?? []).filter(p => p.name);
|
const props = (art.properties ?? []).filter(p => p.name);
|
||||||
if (!props.length) continue;
|
if (!props.length) return [];
|
||||||
lines.push(` ${art.name}:`);
|
return [` ${art.name}:`, ...props.map(p => ` ${p.name}: ${_serializeArticleValue(p.value)}`)];
|
||||||
for (const p of props) lines.push(` ${p.name}: ${_serializeArticleValue(p.value)}`);
|
});
|
||||||
}
|
return body.length ? ['articles:', ...body].join('\n') : null;
|
||||||
return lines.length > 1 ? lines.join('\n') : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _patchArticles(text, articles) {
|
function _patchArticles(text, articles) {
|
||||||
@@ -173,12 +167,8 @@ function _patchMetadata(text, info) {
|
|||||||
const replaced = new Set();
|
const replaced = new Set();
|
||||||
|
|
||||||
const out = lines.map(line => {
|
const out = lines.map(line => {
|
||||||
for (const key of META_KEYS) {
|
const key = META_KEYS.find(k => line.startsWith(k + ':') && info[k] != null && info[k] !== '');
|
||||||
if (line.startsWith(key + ':') && info[key] != null && info[key] !== '') {
|
if (key) { replaced.add(key); return `${key}: ${info[key]}`; }
|
||||||
replaced.add(key);
|
|
||||||
return `${key}: ${info[key]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return line;
|
return line;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,11 +183,11 @@ function _patchMetadata(text, info) {
|
|||||||
function _patchInstrumentHeader(text, instruments) {
|
function _patchInstrumentHeader(text, instruments) {
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
const result = [];
|
const result = [];
|
||||||
const instrMap = {};
|
const instrMap = Object.fromEntries(instruments.flatMap(instr => {
|
||||||
for (const instr of instruments) {
|
const entries = [[instr.name, instr]];
|
||||||
instrMap[instr.name] = instr;
|
if (instr.name.includes('/')) entries.push([instr.name.split('/').pop(), instr]);
|
||||||
if (instr.name.includes('/')) instrMap[instr.name.split('/').pop()] = instr;
|
return entries;
|
||||||
}
|
}));
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
@@ -283,41 +273,40 @@ export function patchScore(rawScoreText, instruments, bars = [], info = null, ar
|
|||||||
const dirtyArticles = articles.filter(a => a.isDirty);
|
const dirtyArticles = articles.filter(a => a.isDirty);
|
||||||
if (dirtyArticles.length || flags.articlesModified) {
|
if (dirtyArticles.length || flags.articlesModified) {
|
||||||
patchedHeader = _patchArticles(patchedHeader, articles);
|
patchedHeader = _patchArticles(patchedHeader, articles);
|
||||||
for (const a of dirtyArticles) log.push({ level: 'changed', path: `articles / ${a.name}` });
|
dirtyArticles.forEach(a => log.push({ level: 'changed', path: `articles / ${a.name}` }));
|
||||||
}
|
}
|
||||||
|
|
||||||
patchedHeader = _patchInstrumentHeader(patchedHeader, instruments);
|
patchedHeader = _patchInstrumentHeader(patchedHeader, instruments);
|
||||||
for (const i of instruments) {
|
instruments.forEach(i => {
|
||||||
if (i.deleted) log.push({ level: 'changed', path: `instrument / ${i.name} (deleted)` });
|
if (i.deleted) log.push({ level: 'changed', path: `instrument / ${i.name} (deleted)` });
|
||||||
else if (i.isDirty) log.push({ level: 'changed', path: `instrument / ${i.name}` });
|
else if (i.isDirty) log.push({ level: 'changed', path: `instrument / ${i.name}` });
|
||||||
}
|
});
|
||||||
|
|
||||||
const barMap = {};
|
const barMap = Object.fromEntries(bars.map(b => [b.id, b]));
|
||||||
for (const bar of bars) barMap[bar.id] = bar;
|
const newBars = bars.filter(b => b.isNew && !b.deleted);
|
||||||
|
|
||||||
if (!barDocs.length) {
|
if (!barDocs.length) {
|
||||||
const newBarDocs = bars.filter(b => b.isNew && !b.deleted).map(b => _buildNewBarDoc(b));
|
newBars.forEach(b => log.push({ level: 'changed', path: `bar / ${b.id}` }));
|
||||||
for (const b of bars.filter(b => b.isNew && !b.deleted)) log.push({ level: 'changed', path: `bar / ${b.id}` });
|
return { text: [patchedHeader, ...newBars.map(_buildNewBarDoc)].join(SEP), log };
|
||||||
return { text: [patchedHeader, ...newBarDocs].join(SEP), log };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let passedThrough = 0;
|
let passedThrough = 0;
|
||||||
const patchedBarDocs = [];
|
const patchedBarDocs = [];
|
||||||
for (const doc of barDocs) {
|
barDocs.forEach(doc => {
|
||||||
const m = doc.match(/^_id:\s*(\S+)/m);
|
const m = doc.match(/^_id:\s*(\S+)/m);
|
||||||
if (!m) { passedThrough++; patchedBarDocs.push(doc); continue; }
|
if (!m) { passedThrough++; patchedBarDocs.push(doc); return; }
|
||||||
const bar = barMap[m[1]];
|
const bar = barMap[m[1]];
|
||||||
if (!bar) { passedThrough++; patchedBarDocs.push(doc); continue; }
|
if (!bar) { passedThrough++; patchedBarDocs.push(doc); return; }
|
||||||
if (bar.deleted) continue;
|
if (bar.deleted) return;
|
||||||
if (!bar.isDirty) { passedThrough++; patchedBarDocs.push(doc); continue; }
|
if (!bar.isDirty) { passedThrough++; patchedBarDocs.push(doc); return; }
|
||||||
log.push({ level: 'changed', path: `bar / ${bar.id}` });
|
log.push({ level: 'changed', path: `bar / ${bar.id}` });
|
||||||
patchedBarDocs.push(_patchBarMeta(doc, bar));
|
patchedBarDocs.push(_patchBarMeta(doc, bar));
|
||||||
}
|
});
|
||||||
|
|
||||||
for (const bar of bars.filter(b => b.isNew && !b.deleted)) {
|
newBars.forEach(bar => {
|
||||||
patchedBarDocs.push(_buildNewBarDoc(bar));
|
patchedBarDocs.push(_buildNewBarDoc(bar));
|
||||||
log.push({ level: 'changed', path: `bar / ${bar.id}` });
|
log.push({ level: 'changed', path: `bar / ${bar.id}` });
|
||||||
}
|
});
|
||||||
|
|
||||||
if (passedThrough > 0) {
|
if (passedThrough > 0) {
|
||||||
log.push({ level: 'info', message: `${passedThrough} bar document${passedThrough !== 1 ? 's' : ''} passed through unchanged` });
|
log.push({ level: 'info', message: `${passedThrough} bar document${passedThrough !== 1 ? 's' : ''} passed through unchanged` });
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
// Group a node's sub-objects by KIND (the SLOT side of SLOT.SUBTYPE in the AST).
|
|
||||||
// Per the editor design: items sharing a kind share one pane; different kinds
|
|
||||||
// produce separate panes whose handles render in the AppShell bottom bar.
|
|
||||||
// Each group is { kind, items: [{ kind, node, label, hasChildren, readOnly? }] }.
|
|
||||||
// The returned order is the display order for the handle bar.
|
|
||||||
|
|
||||||
export const KIND_LABEL = {
|
export const KIND_LABEL = {
|
||||||
tuning: 'TU',
|
tuning: 'TU',
|
||||||
stage: 'ST',
|
stage: 'ST',
|
||||||
@@ -30,10 +24,10 @@ export function getKindGroups(node) {
|
|||||||
]});
|
]});
|
||||||
}
|
}
|
||||||
|
|
||||||
const stage = [];
|
const stage = [
|
||||||
if (node.stageCone) stage.push({ kind: 'stage', node: node.stageCone, label: 'cone (orchestra)', hasChildren: false });
|
...(node.stageCone ? [{ kind: 'stage', node: node.stageCone, label: 'cone (orchestra)', hasChildren: false }] : []),
|
||||||
for (const sv of (node.stageVoices ?? []))
|
...(node.stageVoices ?? []).map(sv => ({ kind: 'stage', node: sv, label: sv.name, hasChildren: false })),
|
||||||
stage.push({ kind: 'stage', node: sv, label: sv.name, hasChildren: false });
|
];
|
||||||
if (stage.length) groups.push({ kind: 'stage', items: stage });
|
if (stage.length) groups.push({ kind: 'stage', items: stage });
|
||||||
|
|
||||||
if (node.instruments.length) {
|
if (node.instruments.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user