// 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 []; }