Articles: key by label across subtypes, scope per property, FO toggle widget
A single article label (e.g. 'f') may emit on multiple `articles.<subtype>`
lines — `defaults` today, `overwrites` planned — with disjoint property
sets that apply at different stages. Model now merges those emissions:
{ name: 'f', properties: [{ name, value, scope: 'defaults'|'overwrites' }] }
Per-label entries in the AR pane show a property-count breakdown
(`f (1 defaults, 2 overwrites)`). The Article FO pane lists each property
with a `(O-) default` / `(-O) overwrite` toggle — click flips the scope
and marks the score dirty. ASCII glyph mimics a physical switch position
so the active scope is visible at a glance.
Fixture tests cover both the existing single-subtype shape and a synthetic
two-subtype merge (177 assertions total).
This commit is contained in:
parent
913114bb02
commit
133117922a
@ -172,7 +172,7 @@ export function buildModel(rawTree) {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (node.parentSlot === 'articles') {
|
if (node.parentSlot === 'articles') {
|
||||||
score.articles.push(buildArticle(node));
|
mergeArticleEntry(score, node);
|
||||||
} else {
|
} else {
|
||||||
score[node.slot] = buildGeneric(node);
|
score[node.slot] = buildGeneric(node);
|
||||||
}
|
}
|
||||||
@ -182,6 +182,24 @@ export function buildModel(rawTree) {
|
|||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Articles are keyed by label. The AST emits one line per (label, subtype)
|
||||||
|
// — e.g. `articles.defaults 'f'` and (future) `articles.overwrites 'f'`.
|
||||||
|
// We merge them into one entry per label whose properties carry the scope
|
||||||
|
// of their originating subtype, so the UI can toggle scope per property.
|
||||||
|
function mergeArticleEntry(score, node) {
|
||||||
|
const name = node.positionals[0];
|
||||||
|
let entry = score.articles.find(a => a.name === name);
|
||||||
|
if (!entry) {
|
||||||
|
entry = { type: 'article', name, properties: [] };
|
||||||
|
score.articles.push(entry);
|
||||||
|
}
|
||||||
|
const scope = node.slot; // 'defaults' | 'overwrites' | future subtype
|
||||||
|
for (const [key, value] of Object.entries(node.props)) {
|
||||||
|
entry.properties.push({ name: key, value, scope });
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
function buildTuning(node) {
|
function buildTuning(node) {
|
||||||
const t = { type: 'tuning', base: node.props.base, scales: {}, chords: {}, frequencyFactors: null };
|
const t = { type: 'tuning', base: node.props.base, scales: {}, chords: {}, frequencyFactors: null };
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
@ -199,15 +217,6 @@ function buildTuning(node) {
|
|||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildArticle(node) {
|
|
||||||
return {
|
|
||||||
type: 'article',
|
|
||||||
subtype: node.slot,
|
|
||||||
name: node.positionals[0],
|
|
||||||
props: { ...node.props },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildInstrument(node) {
|
function buildInstrument(node) {
|
||||||
const instr = {
|
const instr = {
|
||||||
type: 'instrument',
|
type: 'instrument',
|
||||||
|
|||||||
@ -153,6 +153,29 @@ export const PaneFO = {
|
|||||||
? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange })
|
? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange })
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
} else if (node.type === 'article') {
|
||||||
|
const markDirty = () => props.store.markDirty();
|
||||||
|
const SCOPES = ['defaults', 'overwrites'];
|
||||||
|
const scopeLabel = s => s === 'defaults' ? '(O-) default' : '(-O) overwrite';
|
||||||
|
children.push(
|
||||||
|
h('h4', H4, `Article: ${node.name}`),
|
||||||
|
h('ul', { class: 'se-article-props', style: 'list-style:none;padding:0;margin:0' },
|
||||||
|
node.properties.map((p, idx) =>
|
||||||
|
h('li', { key: idx, style: 'display:flex;gap:0.5rem;align-items:center;padding:0.25rem 0' }, [
|
||||||
|
h('span', { style: 'flex:1' }, `${p.name} = ${p.value}`),
|
||||||
|
h('button', {
|
||||||
|
class: ['se-scope-toggle', `scope-${p.scope}`],
|
||||||
|
style: 'font-family:monospace;min-width:8em',
|
||||||
|
onClick: () => {
|
||||||
|
const i = SCOPES.indexOf(p.scope);
|
||||||
|
p.scope = SCOPES[(i + 1) % SCOPES.length];
|
||||||
|
markDirty();
|
||||||
|
},
|
||||||
|
}, scopeLabel(p.scope)),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (node.type === 'bar') {
|
} else if (node.type === 'bar') {
|
||||||
const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); };
|
const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); };
|
||||||
children.push(
|
children.push(
|
||||||
|
|||||||
@ -43,9 +43,13 @@ export function getKindGroups(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((node.articles ?? []).length) {
|
if ((node.articles ?? []).length) {
|
||||||
groups.push({ kind: 'articles', items: node.articles.map(a => ({
|
groups.push({ kind: 'articles', items: node.articles.map(a => {
|
||||||
kind: 'articles', node: a, label: `${a.subtype}: ${a.name}`, hasChildren: false,
|
const counts = a.properties.reduce((acc, p) => {
|
||||||
}))});
|
acc[p.scope] = (acc[p.scope] ?? 0) + 1; return acc;
|
||||||
|
}, {});
|
||||||
|
const suffix = Object.entries(counts).map(([s, n]) => `${n} ${s}`).join(', ');
|
||||||
|
return { kind: 'articles', node: a, label: `${a.name} (${suffix})`, hasChildren: false };
|
||||||
|
})});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.bars.length) {
|
if (node.bars.length) {
|
||||||
|
|||||||
@ -48,9 +48,24 @@ ok('stageVoices parsed', model.stageVoices.length === 3);
|
|||||||
ok('stageVoices[0] is "pi"', model.stageVoices[0].name === 'pi');
|
ok('stageVoices[0] is "pi"', model.stageVoices[0].name === 'pi');
|
||||||
ok('stageVoices[0].direction', model.stageVoices[0].direction === '1|1');
|
ok('stageVoices[0].direction', model.stageVoices[0].direction === '1|1');
|
||||||
ok('articles array non-empty', model.articles.length > 0);
|
ok('articles array non-empty', model.articles.length > 0);
|
||||||
ok('articles[0].subtype defaults', model.articles[0].subtype === 'defaults');
|
|
||||||
ok('articles[0].name "f"', model.articles[0].name === 'f');
|
ok('articles[0].name "f"', model.articles[0].name === 'f');
|
||||||
ok('articles[0].props.add_stress', model.articles[0].props.add_stress === 3);
|
ok('articles[0].properties[]', Array.isArray(model.articles[0].properties));
|
||||||
|
const fProp = model.articles[0].properties.find(p => p.name === 'add_stress');
|
||||||
|
ok('articles[0] add_stress prop', fProp && fProp.value === 3 && fProp.scope === 'defaults');
|
||||||
|
|
||||||
|
// Merge across subtypes: same label, different subtypes → one entry, multi-scope props
|
||||||
|
const MERGE_FIXTURE = `01 articles.defaults 'g' add_stress=2
|
||||||
|
01 articles.overwrites 'g' pitch_bend=0.05
|
||||||
|
`;
|
||||||
|
const mergeRoot = parseAstLog('00 tuning base=\'x\'\n' + MERGE_FIXTURE);
|
||||||
|
const mergeModel = buildModel(mergeRoot);
|
||||||
|
ok('merge: single entry per label', mergeModel.articles.length === 1);
|
||||||
|
ok('merge: label "g"', mergeModel.articles[0].name === 'g');
|
||||||
|
ok('merge: 2 properties', mergeModel.articles[0].properties.length === 2);
|
||||||
|
const gDef = mergeModel.articles[0].properties.find(p => p.scope === 'defaults');
|
||||||
|
const gOver = mergeModel.articles[0].properties.find(p => p.scope === 'overwrites');
|
||||||
|
ok('merge: default scope present', gDef && gDef.name === 'add_stress' && gDef.value === 2);
|
||||||
|
ok('merge: overwrite scope present', gOver && gOver.name === 'pitch_bend' && gOver.value === 0.05);
|
||||||
|
|
||||||
// ── Bar IDs ────────────────────────────────────────────────────────────────
|
// ── Bar IDs ────────────────────────────────────────────────────────────────
|
||||||
// Bar IDs are opaque auto-increment strings; only the raw id string matters.
|
// Bar IDs are opaque auto-increment strings; only the raw id string matters.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user