Multi-pane Sub-objects driven by SLOT of SLOT.SUBTYPE
The bottom handle bar is now dynamic. Fixed CP + FO handles, then one
handle per kind of sub-object the focused node has. "Kind" is the SLOT
part of the AST's SLOT.SUBTYPE namespace: sub-objects sharing a SLOT
(e.g. `line.stem_note` + `line.motif`, or `articles.<subtypeA>` +
`articles.<subtypeB>`) collapse into one pane; different SLOTs split.
Score-level kinds emit, in this order: TU (tuning), ST (stage cone +
voices), IN (instruments — linked ones listed read-only), AR (articles),
BA (bars). info metadata is folded into the FO pane (was previously
listed as a sub-object).
Handles are 2-letter labels for now, intended as alt-text for icons in
a later release.
- `subobject-kinds.js` new: `getKindGroups(node)` returns ordered
`[{ kind, items }]`; `KIND_LABEL` maps kind -> 2-letter label.
- `PaneSubObjects.js` now parameterized by `kind` prop; renders one group.
- `AppShell.js` builds the handle bar from `getKindGroups(focusedNode)`;
drops back to FO when the active sub-pane disappears after a focus change.
- `ObjectShort.js` accepts a `readOnly` flag (used for linked instruments).
This commit is contained in:
123
static/subobject-kinds.js
Normal file
123
static/subobject-kinds.js
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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 = {
|
||||
tuning: 'TU',
|
||||
stage: 'ST',
|
||||
instrument: 'IN',
|
||||
articles: 'AR',
|
||||
bar: 'BA',
|
||||
variation: 'VR',
|
||||
label_spec: 'LA',
|
||||
voice: 'VO',
|
||||
motif: 'MO',
|
||||
offset: 'OF',
|
||||
stem_note: 'SN',
|
||||
};
|
||||
|
||||
export function getKindGroups(node) {
|
||||
if (!node) return [];
|
||||
|
||||
if (node.type === 'score') {
|
||||
const groups = [];
|
||||
|
||||
if (node.tuning) {
|
||||
groups.push({ kind: 'tuning', items: [
|
||||
{ kind: 'tuning', node: node.tuning, label: `base ${node.tuning.base ?? '?'}`, hasChildren: false },
|
||||
]});
|
||||
}
|
||||
|
||||
const stage = [];
|
||||
if (node.stageCone) stage.push({ kind: 'stage', node: node.stageCone, label: 'cone (orchestra)', hasChildren: false });
|
||||
for (const sv of (node.stageVoices ?? []))
|
||||
stage.push({ kind: 'stage', node: sv, label: sv.name, hasChildren: false });
|
||||
if (stage.length) groups.push({ kind: 'stage', items: stage });
|
||||
|
||||
if (node.instruments.length) {
|
||||
groups.push({ kind: 'instrument', items: node.instruments.map(i => ({
|
||||
kind: 'instrument', node: i, label: i.name, hasChildren: !i.isLinked, readOnly: i.isLinked,
|
||||
}))});
|
||||
}
|
||||
|
||||
if ((node.articles ?? []).length) {
|
||||
groups.push({ kind: 'articles', items: node.articles.map(a => ({
|
||||
kind: 'articles', node: a, label: `${a.subtype}: ${a.name}`, hasChildren: false,
|
||||
}))});
|
||||
}
|
||||
|
||||
if (node.bars.length) {
|
||||
groups.push({ kind: 'bar', items: node.bars.map(b => ({
|
||||
kind: 'bar', node: b, label: b.id, hasChildren: Object.keys(b.voices).length > 0,
|
||||
}))});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
if (node.type === 'instrument') {
|
||||
if (!node.variations.length) return [];
|
||||
return [{ kind: 'variation', items: node.variations.map((v, idx) => ({
|
||||
kind: 'variation', node: v,
|
||||
label: `variation ${idx + 1}${v.dependsOn ? ` (${v.dependsOn})` : ''}`,
|
||||
hasChildren: true,
|
||||
}))}];
|
||||
}
|
||||
|
||||
if (node.type === 'variation') {
|
||||
const groups = [];
|
||||
if (node.labelSpecs.length) {
|
||||
groups.push({ kind: 'label_spec', items: node.labelSpecs.map(ls => ({
|
||||
kind: 'label_spec', node: ls, label: ls.label ?? '(no label)', hasChildren: false,
|
||||
}))});
|
||||
}
|
||||
if (node.subvariations.length) {
|
||||
groups.push({ kind: 'variation', items: node.subvariations.map((sv, idx) => {
|
||||
const dep = sv.dependsOn;
|
||||
const label = dep == null ? `subvariation ${idx + 1}`
|
||||
: isNaN(Number(dep)) ? `ATTR: ${dep}`
|
||||
: String(dep);
|
||||
return { kind: 'variation', node: sv, label, hasChildren: true };
|
||||
})});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
if (node.type === 'bar') {
|
||||
const entries = Object.entries(node.voices);
|
||||
if (!entries.length) return [];
|
||||
return [{ kind: 'voice', items: entries.map(([name, v]) => ({
|
||||
kind: 'voice', node: v, label: name,
|
||||
hasChildren: v.offsets.length > 0 || v.motifs.some(m => m.isStatic),
|
||||
}))}];
|
||||
}
|
||||
|
||||
if (node.type === 'voice') {
|
||||
const groups = [];
|
||||
const staticMotifs = node.motifs.filter(m => m.isStatic);
|
||||
if (staticMotifs.length) {
|
||||
groups.push({ kind: 'motif', items: staticMotifs.map(m => ({
|
||||
kind: 'motif', node: m, label: m.label, hasChildren: m.stemNotes.length > 0,
|
||||
}))});
|
||||
}
|
||||
if (node.offsets.length) {
|
||||
groups.push({ kind: 'offset', items: node.offsets.map((o, idx) => ({
|
||||
kind: 'offset', node: o, label: `tick ${o.tick ?? idx}`, hasChildren: o.stemNotes.length > 0,
|
||||
}))});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
if (node.type === 'motif' || node.type === 'offset') {
|
||||
if (!node.stemNotes.length) return [];
|
||||
return [{ kind: 'stem_note', items: node.stemNotes.map(sn => ({
|
||||
kind: 'stem_note', node: sn,
|
||||
label: `pitch ${sn.pitch}${sn.clauses.length ? ` (${sn.clauses.length} clause${sn.clauses.length > 1 ? 's' : ''})` : ''}`,
|
||||
hasChildren: false,
|
||||
}))}];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user