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:
@@ -7,6 +7,11 @@ import { stressorToString } from '../exporter.js';
|
||||
|
||||
const H4 = { style: 'margin:0 0 0.5rem' };
|
||||
|
||||
function unknownPropFields(node) {
|
||||
return Object.entries(node.unknownProps ?? {})
|
||||
.map(([key, value]) => ({ key, value: String(value), editable: false }));
|
||||
}
|
||||
|
||||
function parseStressor(str) {
|
||||
const groups = str.split(';').map(seg =>
|
||||
seg.split(',').map(n => parseInt(n, 10)).filter(n => !isNaN(n))
|
||||
@@ -174,34 +179,68 @@ export const PaneFO = {
|
||||
h(ObjectExtended, {
|
||||
fields: [
|
||||
{ key: 'articles', value: node.articles.join(', ') || '—', editable: false },
|
||||
{ key: 'motifs', value: node.motifs.join(', ') || '—', editable: false },
|
||||
{ key: 'motifs', value: node.motifs.map(m => m.label).join(', ') || '—', editable: false },
|
||||
{ key: 'offsets', value: String(node.offsets.length), editable: false },
|
||||
],
|
||||
onChange: null,
|
||||
}),
|
||||
);
|
||||
} else if (node.type === 'offset') {
|
||||
const noteStr = n => `${n.pitch}${n.effLength != null ? ' ' + n.effLength : ''}`;
|
||||
const clusterStr = c => c.notes.length
|
||||
? c.notes.map(n => `${n.letter ?? ''}${n.shift != null ? n.shift : ''}${n.length != null ? ' ' + n.length : ''}`).join(', ')
|
||||
: `cluster[${c.index}]`;
|
||||
const noteItem = (text, i) => h('li', { class: 'se-object-item', key: i },
|
||||
h('span', { class: 'se-object-label' }, text));
|
||||
|
||||
const snLabel = sn => `${sn.pitch}${sn.effLength != null ? ' /' + sn.effLength : ''}${sn.clauses.length ? ' ×' + sn.clauses.length : ''}`;
|
||||
children.push(
|
||||
h('h4', H4, `Tick: ${node.tick}`),
|
||||
h(ObjectExtended, {
|
||||
fields: [{ key: 'tick', value: node.tick, editable: false }],
|
||||
fields: [
|
||||
{ key: 'tick', value: node.tick, editable: false },
|
||||
{ key: 'stem notes', value: node.stemNotes.map(snLabel).join(', ') || '—', editable: false },
|
||||
...unknownPropFields(node),
|
||||
],
|
||||
onChange: null,
|
||||
}),
|
||||
node.stemNotes.length ? h('div', { style: 'margin-top:0.5rem' }, [
|
||||
h('strong', null, 'Stem notes'),
|
||||
h('ul', { class: 'se-object-list' }, node.stemNotes.map((n, i) => noteItem(noteStr(n), i))),
|
||||
]) : null,
|
||||
node.clusters.length ? h('div', { style: 'margin-top:0.5rem' }, [
|
||||
h('strong', null, 'Clusters'),
|
||||
h('ul', { class: 'se-object-list' }, node.clusters.map((c, i) => noteItem(clusterStr(c), i))),
|
||||
]) : null,
|
||||
);
|
||||
} else if (node.type === 'motif') {
|
||||
const pitchLabel = sn => (Number.isInteger(sn.pitch) ? `(ref${sn.pitch !== 0 ? sn.pitch : ''})` : String(sn.pitch))
|
||||
+ (sn.clauses.length ? ' ×' + sn.clauses.length : '');
|
||||
children.push(
|
||||
h('h4', H4, `Motif: ${node.label}`),
|
||||
h(ObjectExtended, {
|
||||
fields: [
|
||||
{ key: 'label', value: node.label, editable: false },
|
||||
{ key: 'static', value: node.isStatic, editable: false, type: 'boolean' },
|
||||
{ key: 'stem notes', value: node.stemNotes.map(pitchLabel).join(', ') || '—', editable: false },
|
||||
...unknownPropFields(node),
|
||||
],
|
||||
onChange: null,
|
||||
}),
|
||||
);
|
||||
} else if (node.type === 'stem_note') {
|
||||
const bar = props.store.focusPath.find(n => n.type === 'bar') ?? null;
|
||||
const markDirty = () => {
|
||||
node.isDirty = true;
|
||||
if (bar) bar.isDirty = true;
|
||||
props.store.markDirty();
|
||||
};
|
||||
children.push(
|
||||
h('h4', H4, `Stem note: ${node.pitch}`),
|
||||
h(ObjectExtended, {
|
||||
fields: [
|
||||
{ key: 'pitch', value: node.pitch, editable: true },
|
||||
{ key: 'length', value: node.length ?? '', editable: true },
|
||||
{ key: 'weight', value: node.weight != null ? String(node.weight) : '', editable: true, type: 'number' },
|
||||
{ key: 'adj_stress', value: node.adjStress != null ? String(node.adjStress) : '', editable: true, type: 'number' },
|
||||
{ key: 'chain', value: node.chainText, editable: true },
|
||||
{ key: 'clauses', value: String(node.clauses.length), editable: false },
|
||||
...unknownPropFields(node),
|
||||
],
|
||||
onChange: ({ key, value }) => {
|
||||
if (key === 'pitch') node.pitch = value;
|
||||
if (key === 'length') node.length = value || null;
|
||||
if (key === 'weight') node.weight = value === '' ? null : Number(value);
|
||||
if (key === 'adj_stress') node.adjStress = value === '' ? null : Number(value);
|
||||
if (key === 'chain') node.chainText = value;
|
||||
markDirty();
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
children.push(h('pre', { style: 'font-size:0.75rem;white-space:pre-wrap' },
|
||||
|
||||
Reference in New Issue
Block a user