Add motif/stem_note display and offset note hierarchy
- parseAstLog: abort on depth jump >1 or missing slot (depth>0) - buildStemNote: full clause tree (notes/pauses/stacks), editable fields - buildClause: replaces buildCluster; filters nested clause.chain silently - buildMotif: builds from voice.motif; sets isStatic - buildOffset: uses buildStemNote; drops old clusters/chains arrays - buildVoice: motifs are now objects, not label strings - PaneSubObjects: static motifs before offsets; motif/offset→stem_note drill-down - PaneFO: motif + stem_note FO cases; unknownProps display helper - PaneCP: motif + stem_note shortView cases - test-parser.mjs: 21 new synthetic tests for motif/clause/stack/guards
This commit is contained in:
@@ -42,6 +42,11 @@ export function parseAstLog(text) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (depth > stack[stack.length - 1].depth + 1)
|
||||
throw new Error(`AST log malformed: depth jump to ${depth} at: ${line}`);
|
||||
if (depth > 0 && !slotFull.includes('.'))
|
||||
throw new Error(`AST log malformed: missing slot at depth ${depth}: ${line}`);
|
||||
|
||||
stack[stack.length - 1].children.push(node);
|
||||
stack.push(node);
|
||||
}
|
||||
@@ -103,6 +108,10 @@ function parseRest(rest) {
|
||||
|
||||
// ── Second pass: build typed model ──────────────────────────────────────────
|
||||
|
||||
function collectUnknownProps(nodeProps, knownKeys) {
|
||||
return Object.fromEntries(Object.entries(nodeProps).filter(([k]) => !knownKeys.has(k)));
|
||||
}
|
||||
|
||||
export function buildModel(rawTree) {
|
||||
const score = {
|
||||
type: 'score',
|
||||
@@ -415,7 +424,7 @@ function buildVoice(node) {
|
||||
voice.articles.push(child.positionals[0]);
|
||||
break;
|
||||
case 'voice.motif':
|
||||
voice.motifs.push(child.props.label);
|
||||
voice.motifs.push(buildMotif(child));
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
@@ -430,61 +439,61 @@ function buildOffset(node) {
|
||||
type: 'offset',
|
||||
tick: node.props.tick,
|
||||
stemNotes: [],
|
||||
clusters: [],
|
||||
chains: [],
|
||||
unknownProps: collectUnknownProps(node.props, new Set(['tick'])),
|
||||
};
|
||||
|
||||
for (const child of node.children) {
|
||||
const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot;
|
||||
switch (fqSlot) {
|
||||
case 'offset.stem_note':
|
||||
offset.stemNotes.push({ pitch: child.props.pitch, effLength: child.props.eff_length });
|
||||
break;
|
||||
case 'offset.cluster':
|
||||
offset.clusters.push(buildCluster(child));
|
||||
break;
|
||||
case 'offset.chain':
|
||||
offset.chains.push({ index: child.positionals[0], children: child.children.map(buildGeneric) });
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
}
|
||||
if (child.parentSlot === 'offset' && child.slot === 'stem_note')
|
||||
offset.stemNotes.push(buildStemNote(child));
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
function buildCluster(node) {
|
||||
const cluster = {
|
||||
type: 'cluster',
|
||||
index: node.positionals[0],
|
||||
repeat: node.props.repeat ?? null,
|
||||
notes: [],
|
||||
pauses: [],
|
||||
groups: [],
|
||||
subchains: [],
|
||||
function buildStemNote(node) {
|
||||
const KNOWN = new Set(['pitch', 'eff_length', 'adj_stress', 'adjacent']);
|
||||
return {
|
||||
type: 'stem_note',
|
||||
pitch: node.props.pitch,
|
||||
effLength: node.props.eff_length ?? null,
|
||||
adjacent: node.props.adjacent ?? null,
|
||||
adjStress: node.props.adj_stress ?? null,
|
||||
length: null,
|
||||
weight: null,
|
||||
chainText: node.children.find(c => c.slot === 'chain')?.props._tmp_string ?? '',
|
||||
clauses: node.children
|
||||
.filter(c => c.parentSlot === 'chain' && c.slot === 'clause')
|
||||
.map(buildClause),
|
||||
isDirty: false,
|
||||
unknownProps: collectUnknownProps(node.props, KNOWN),
|
||||
};
|
||||
}
|
||||
|
||||
function buildClause(node) {
|
||||
return {
|
||||
type: 'clause',
|
||||
index: node.positionals[0] ?? 0,
|
||||
repeat: node.props.repeat ?? null,
|
||||
notes: node.children.filter(c => c.slot === 'note' && c.parentSlot === 'clause').map(c => ({ ...c.props })),
|
||||
pauses: node.children.filter(c => c.slot === 'pause').map(c => ({ length: c.props.length })),
|
||||
stacks: node.children.filter(c => c.slot === 'stack').map(c => ({
|
||||
length: c.props.length, netlength: c.props.netlength,
|
||||
notes: c.children.filter(cn => cn.slot === 'note').map(cn => ({ ...cn.props })),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function buildMotif(node) {
|
||||
const m = {
|
||||
type: 'motif',
|
||||
label: node.props.label,
|
||||
stemNotes: [],
|
||||
unknownProps: collectUnknownProps(node.props, new Set(['label'])),
|
||||
};
|
||||
for (const child of node.children) {
|
||||
switch (child.slot) {
|
||||
case 'note':
|
||||
cluster.notes.push({ ...child.props });
|
||||
break;
|
||||
case 'pause':
|
||||
cluster.pauses.push({ length: child.props.length });
|
||||
break;
|
||||
case 'group':
|
||||
cluster.groups.push({ length: child.props.length, netlength: child.props.netlength });
|
||||
break;
|
||||
case 'subchain':
|
||||
cluster.subchains.push({ length: child.props.length, children: child.children.map(buildGeneric) });
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
}
|
||||
if (child.parentSlot === 'motif' && child.slot === 'stem_note')
|
||||
m.stemNotes.push(buildStemNote(child));
|
||||
}
|
||||
|
||||
return cluster;
|
||||
m.isStatic = m.stemNotes.length > 0 && !m.stemNotes.some(sn => Number.isInteger(sn.pitch));
|
||||
return m;
|
||||
}
|
||||
|
||||
function buildGeneric(node) {
|
||||
|
||||
Reference in New Issue
Block a user