Add delete buttons and bar add/create to sub-objects pane
This commit is contained in:
@@ -2,8 +2,8 @@ import { h } from 'vue';
|
||||
|
||||
// One-line summary row with a drill-down chevron.
|
||||
export const ObjectShort = {
|
||||
props: ['label', 'typeTag', 'focused', 'hasChildren', 'readOnly'],
|
||||
emits: ['focus', 'drillDown'],
|
||||
props: ['label', 'typeTag', 'focused', 'hasChildren', 'readOnly', 'deletable'],
|
||||
emits: ['focus', 'drillDown', 'delete'],
|
||||
setup(props, { emit }) {
|
||||
return () => h('li', {
|
||||
class: ['se-object-item', props.focused ? 'focused' : null, props.readOnly ? 'read-only' : null],
|
||||
@@ -11,6 +11,13 @@ export const ObjectShort = {
|
||||
}, [
|
||||
props.typeTag ? h('span', { class: 'se-object-type' }, props.typeTag) : null,
|
||||
h('span', { class: 'se-object-label' }, props.label),
|
||||
props.deletable
|
||||
? h('button', {
|
||||
class: 'se-btn-delete',
|
||||
title: 'Delete',
|
||||
onClick: e => { e.stopPropagation(); emit('delete'); },
|
||||
}, '×')
|
||||
: null,
|
||||
props.hasChildren
|
||||
? h('button', {
|
||||
class: 'se-chevron',
|
||||
|
||||
@@ -88,7 +88,9 @@ export const PaneCP = {
|
||||
const model = props.store.scoreModel;
|
||||
const { text: patched, log } = patchScore(
|
||||
raw, model.instruments, model.bars, model.info, model.articles ?? [],
|
||||
{ articlesModified: !!model.articlesModified },
|
||||
);
|
||||
model.articlesModified = false;
|
||||
props.store.exportLog = log;
|
||||
await putScoreText(patched);
|
||||
props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 };
|
||||
|
||||
@@ -217,11 +217,12 @@ export const PaneFO = {
|
||||
h('h4', H4, `Bar: ${node.id}`),
|
||||
h(ObjectExtended, {
|
||||
fields: [
|
||||
{ key: 'id', value: node.id, editable: false },
|
||||
{ key: 'id', value: node.id, editable: node.isNew ?? false },
|
||||
{ key: 'beats_per_minute', value: node.tempoLevels ?? '', editable: true, type: 'number' },
|
||||
{ key: 'stress_pattern', value: stressorToString(node.stressor), editable: true },
|
||||
],
|
||||
onChange: ({ key, value }) => {
|
||||
if (key === 'id') { node.id = value; markBarDirty(); return; }
|
||||
if (key === 'beats_per_minute') node.tempoLevels = isNaN(value) ? null : value;
|
||||
if (key === 'stress_pattern') node.stressor = parseStressor(value);
|
||||
markBarDirty();
|
||||
|
||||
@@ -9,6 +9,15 @@ function barGroupKey(id) {
|
||||
return m ? id.slice(0, m.index) : id;
|
||||
}
|
||||
|
||||
// Increment the trailing numeric run of an ID, preserving zero-padding width.
|
||||
function incrementId(id) {
|
||||
const m = id.match(/(\d+)$/);
|
||||
if (!m) return id + '1';
|
||||
const num = parseInt(m[1], 10) + 1;
|
||||
const padded = String(num).padStart(m[1].length, '0');
|
||||
return id.slice(0, m.index) + padded;
|
||||
}
|
||||
|
||||
export const PaneSubObjects = {
|
||||
props: ['store', 'kind', 'onFocusFO'],
|
||||
setup(props) {
|
||||
@@ -17,6 +26,67 @@ export const PaneSubObjects = {
|
||||
return fp.length ? fp[fp.length - 1] : props.store.scoreModel;
|
||||
}
|
||||
|
||||
function deleteItem(kind, node) {
|
||||
const store = props.store;
|
||||
const model = store.scoreModel;
|
||||
if (!model) return;
|
||||
|
||||
if (kind === 'instrument') {
|
||||
const idx = model.instruments.indexOf(node);
|
||||
if (idx !== -1) { model.instruments.splice(idx, 1); store.markDirty(); }
|
||||
} else if (kind === 'articles') {
|
||||
const idx = (model.articles ?? []).indexOf(node);
|
||||
if (idx !== -1) { model.articles.splice(idx, 1); model.articlesModified = true; store.markDirty(); }
|
||||
} else if (kind === 'bar') {
|
||||
node.deleted = true;
|
||||
node.isDirty = true;
|
||||
store.markDirty();
|
||||
} else if (kind === 'variation') {
|
||||
for (const instr of model.instruments) {
|
||||
const idx = instr.variations.indexOf(node);
|
||||
if (idx !== -1) { instr.variations.splice(idx, 1); instr.isDirty = true; store.markDirty(); return; }
|
||||
for (const v of instr.variations) {
|
||||
const sidx = v.subvariations.indexOf(node);
|
||||
if (sidx !== -1) { v.subvariations.splice(sidx, 1); instr.isDirty = true; store.markDirty(); return; }
|
||||
}
|
||||
}
|
||||
} else if (kind === 'label_spec') {
|
||||
for (const instr of model.instruments) {
|
||||
for (const v of instr.variations) {
|
||||
const idx = v.labelSpecs.indexOf(node);
|
||||
if (idx !== -1) { v.labelSpecs.splice(idx, 1); instr.isDirty = true; store.markDirty(); return; }
|
||||
for (const sv of v.subvariations) {
|
||||
const idx2 = sv.labelSpecs.indexOf(node);
|
||||
if (idx2 !== -1) { sv.labelSpecs.splice(idx2, 1); instr.isDirty = true; store.markDirty(); return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addBar(afterId) {
|
||||
const store = props.store;
|
||||
const model = store.scoreModel;
|
||||
if (!model) return;
|
||||
let newId;
|
||||
if (afterId) {
|
||||
newId = incrementId(afterId);
|
||||
} else {
|
||||
const liveBars = (model.bars ?? []).filter(b => !b.deleted);
|
||||
newId = liveBars.length ? incrementId(liveBars[liveBars.length - 1].id) : 'bar001';
|
||||
}
|
||||
const newBar = {
|
||||
type: 'bar', id: newId, isDirty: true, isNew: true,
|
||||
stressor: null, tempoLevels: null,
|
||||
upperStressBound: null, lowerStressBound: null, tempoShape: null,
|
||||
voices: {},
|
||||
};
|
||||
model.bars.push(newBar);
|
||||
store.markDirty();
|
||||
store.pushFocus(newBar);
|
||||
props.onFocusFO?.();
|
||||
}
|
||||
|
||||
return () => {
|
||||
const node = focused();
|
||||
const groups = getKindGroups(node);
|
||||
@@ -25,13 +95,14 @@ export const PaneSubObjects = {
|
||||
: groups[0];
|
||||
const items = group?.items ?? [];
|
||||
|
||||
if (!items.length) return h('div', null, h('em', null, 'No sub-objects'));
|
||||
if (!items.length && props.kind !== 'bar') return h('div', null, h('em', null, 'No sub-objects'));
|
||||
|
||||
if (props.kind === 'bar') {
|
||||
// Group bars by shared P?L? key; render each group as a row of inline chips.
|
||||
const visibleItems = items.filter(item => !item.node.deleted);
|
||||
|
||||
const barGroups = [];
|
||||
const seen = new Map();
|
||||
for (const item of items) {
|
||||
for (const item of visibleItems) {
|
||||
const key = barGroupKey(item.label);
|
||||
if (!seen.has(key)) {
|
||||
const g = { key, items: [] };
|
||||
@@ -41,17 +112,39 @@ export const PaneSubObjects = {
|
||||
seen.get(key).items.push(item);
|
||||
}
|
||||
|
||||
return h('div', { class: 'se-bar-groups' }, barGroups.map(g =>
|
||||
h('div', { key: g.key, class: 'se-bar-group' },
|
||||
g.items.map((item, idx) =>
|
||||
h('button', {
|
||||
const groupEls = barGroups.map(g => {
|
||||
const lastId = g.items[g.items.length - 1].label;
|
||||
return h('div', { key: g.key, class: 'se-bar-group' }, [
|
||||
...g.items.map((item, idx) =>
|
||||
h('span', {
|
||||
key: idx,
|
||||
class: ['se-bar-chip', props.store.focusPath.includes(item.node) ? 'focused' : null],
|
||||
onClick: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
||||
}, item.label)
|
||||
)
|
||||
)
|
||||
));
|
||||
}, [
|
||||
h('span', {
|
||||
class: 'se-bar-chip-label',
|
||||
onClick: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
||||
}, item.label),
|
||||
h('button', {
|
||||
class: 'se-bar-chip-delete',
|
||||
title: 'Delete bar',
|
||||
onClick: e => { e.stopPropagation(); deleteItem('bar', item.node); },
|
||||
}, '×'),
|
||||
])
|
||||
),
|
||||
h('button', {
|
||||
class: 'se-bar-add',
|
||||
title: `Add bar after ${lastId}`,
|
||||
onClick: () => addBar(lastId),
|
||||
}, '+ add'),
|
||||
]);
|
||||
});
|
||||
|
||||
return h('div', { class: 'se-bar-groups' }, [
|
||||
...groupEls,
|
||||
h('div', { class: 'se-bar-group' },
|
||||
h('button', { class: 'se-bar-add', onClick: () => addBar(null) }, '+ add bar')
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return h('div', null,
|
||||
@@ -63,8 +156,10 @@ export const PaneSubObjects = {
|
||||
focused: props.store.focusPath.includes(item.node),
|
||||
hasChildren: item.hasChildren,
|
||||
readOnly: item.readOnly ?? false,
|
||||
onFocus: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
||||
deletable: item.deletable ?? false,
|
||||
onFocus: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
||||
onDrillDown: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
||||
onDelete: item.deletable ? () => deleteItem(item.kind, item.node) : undefined,
|
||||
})
|
||||
))
|
||||
);
|
||||
|
||||
@@ -220,7 +220,10 @@ function patchInstrumentHeader(text, instruments) {
|
||||
if (m) {
|
||||
const rawName = m[1].replace(/^'|'$/g, '');
|
||||
const instr = instrMap[rawName];
|
||||
if (instr && instr.isDirty) {
|
||||
if (instr && instr.deleted) {
|
||||
i++;
|
||||
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) i++;
|
||||
} else if (instr && instr.isDirty) {
|
||||
i++;
|
||||
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) i++;
|
||||
result.push(exportInstrument(instr));
|
||||
@@ -280,7 +283,15 @@ function patchBarMeta(doc, bar) {
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
export function patchScore(rawScoreText, instruments, bars = [], info = null, articles = []) {
|
||||
function buildNewBarDoc(bar) {
|
||||
const lines = [`_id: ${bar.id}`, '_meta:'];
|
||||
const sp = stressorToString(bar.stressor);
|
||||
if (sp) lines.push(` stress_pattern: ${sp}`);
|
||||
if (bar.tempoLevels != null) lines.push(` beats_per_minute: ${bar.tempoLevels}`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function patchScore(rawScoreText, instruments, bars = [], info = null, articles = [], flags = {}) {
|
||||
const log = [];
|
||||
const SEP = '\n---\n';
|
||||
const [header, ...barDocs] = rawScoreText.split(SEP);
|
||||
@@ -288,28 +299,43 @@ export function patchScore(rawScoreText, instruments, bars = [], info = null, ar
|
||||
let patchedHeader = patchMetadata(header, info);
|
||||
|
||||
const dirtyArticles = articles.filter(a => a.isDirty);
|
||||
if (dirtyArticles.length) {
|
||||
if (dirtyArticles.length || flags.articlesModified) {
|
||||
patchedHeader = patchArticles(patchedHeader, articles);
|
||||
for (const a of dirtyArticles) log.push({ level: 'changed', path: `articles / ${a.name}` });
|
||||
}
|
||||
|
||||
patchedHeader = patchInstrumentHeader(patchedHeader, instruments);
|
||||
for (const i of instruments.filter(i => i.isDirty)) log.push({ level: 'changed', path: `instrument / ${i.name}` });
|
||||
|
||||
if (!barDocs.length) return { text: patchedHeader, log };
|
||||
for (const i of instruments) {
|
||||
if (i.deleted) log.push({ level: 'changed', path: `instrument / ${i.name} (deleted)` });
|
||||
else if (i.isDirty) log.push({ level: 'changed', path: `instrument / ${i.name}` });
|
||||
}
|
||||
|
||||
const barMap = {};
|
||||
for (const bar of bars) barMap[bar.id] = bar;
|
||||
|
||||
if (!barDocs.length) {
|
||||
const newBarDocs = bars.filter(b => b.isNew && !b.deleted).map(b => buildNewBarDoc(b));
|
||||
for (const b of bars.filter(b => b.isNew && !b.deleted)) log.push({ level: 'changed', path: `bar / ${b.id}` });
|
||||
return { text: [patchedHeader, ...newBarDocs].join(SEP), log };
|
||||
}
|
||||
|
||||
let passedThrough = 0;
|
||||
const patchedBarDocs = barDocs.map(doc => {
|
||||
const patchedBarDocs = [];
|
||||
for (const doc of barDocs) {
|
||||
const m = doc.match(/^_id:\s*(\S+)/m);
|
||||
if (!m) { passedThrough++; return doc; }
|
||||
if (!m) { passedThrough++; patchedBarDocs.push(doc); continue; }
|
||||
const bar = barMap[m[1]];
|
||||
if (!bar?.isDirty) { passedThrough++; return doc; }
|
||||
if (!bar) { passedThrough++; patchedBarDocs.push(doc); continue; }
|
||||
if (bar.deleted) continue;
|
||||
if (!bar.isDirty) { passedThrough++; patchedBarDocs.push(doc); continue; }
|
||||
log.push({ level: 'changed', path: `bar / ${bar.id}` });
|
||||
return patchBarMeta(doc, bar);
|
||||
});
|
||||
patchedBarDocs.push(patchBarMeta(doc, bar));
|
||||
}
|
||||
|
||||
for (const bar of bars.filter(b => b.isNew && !b.deleted)) {
|
||||
patchedBarDocs.push(buildNewBarDoc(bar));
|
||||
log.push({ level: 'changed', path: `bar / ${bar.id}` });
|
||||
}
|
||||
|
||||
if (passedThrough > 0) {
|
||||
log.push({ level: 'info', message: `${passedThrough} bar document${passedThrough !== 1 ? 's' : ''} passed through unchanged` });
|
||||
|
||||
@@ -320,21 +320,16 @@
|
||||
}
|
||||
|
||||
.se-bar-chip {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.7rem;
|
||||
font-family: monospace;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border: 1px solid #383838;
|
||||
background: #1e1e1e;
|
||||
color: #777;
|
||||
cursor: pointer;
|
||||
margin: 1px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.se-bar-chip:hover {
|
||||
color: #ccc;
|
||||
border-color: #555;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.se-bar-chip.focused {
|
||||
@@ -343,6 +338,49 @@
|
||||
border-color: #4a7aaa;
|
||||
}
|
||||
|
||||
.se-bar-chip-label {
|
||||
padding: 0.1rem 0.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.se-bar-chip:hover .se-bar-chip-label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.se-bar-chip-delete {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-left: 1px solid #383838;
|
||||
color: #604040;
|
||||
cursor: pointer;
|
||||
padding: 0.1rem 0.2rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.se-bar-chip-delete:hover {
|
||||
color: #e06060;
|
||||
}
|
||||
|
||||
.se-bar-add {
|
||||
display: inline-block;
|
||||
font-size: 0.65rem;
|
||||
font-family: monospace;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border: 1px dashed #383838;
|
||||
background: transparent;
|
||||
color: #505050;
|
||||
cursor: pointer;
|
||||
margin: 1px;
|
||||
line-height: 1.4;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.se-bar-add:hover {
|
||||
color: #999;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
/* Export log (shown in CP pane after export) */
|
||||
.se-export-log {
|
||||
font-size: 0.75rem;
|
||||
@@ -387,3 +425,17 @@
|
||||
color: #e06060;
|
||||
border-color: #602020;
|
||||
}
|
||||
|
||||
.se-btn-delete {
|
||||
background: transparent;
|
||||
color: #e06060;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0.3rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
margin-left: 0.15rem;
|
||||
}
|
||||
.se-btn-delete:hover {
|
||||
color: #ff8080;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export function getKindGroups(node) {
|
||||
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,
|
||||
deletable: true,
|
||||
}))});
|
||||
}
|
||||
|
||||
@@ -48,13 +49,14 @@ export function getKindGroups(node) {
|
||||
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 };
|
||||
return { kind: 'articles', node: a, label: `${a.name} (${suffix})`, hasChildren: false, deletable: true };
|
||||
})});
|
||||
}
|
||||
|
||||
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,
|
||||
deletable: true,
|
||||
}))});
|
||||
}
|
||||
|
||||
@@ -66,7 +68,7 @@ export function getKindGroups(node) {
|
||||
return [{ kind: 'variation', items: node.variations.map((v, idx) => ({
|
||||
kind: 'variation', node: v,
|
||||
label: `variation ${idx + 1}${v.dependsOn ? ` (${v.dependsOn})` : ''}`,
|
||||
hasChildren: true,
|
||||
hasChildren: true, deletable: true,
|
||||
}))}];
|
||||
}
|
||||
|
||||
@@ -74,7 +76,7 @@ export function getKindGroups(node) {
|
||||
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,
|
||||
kind: 'label_spec', node: ls, label: ls.label ?? '(no label)', hasChildren: false, deletable: true,
|
||||
}))});
|
||||
}
|
||||
if (node.subvariations.length) {
|
||||
@@ -83,7 +85,7 @@ export function getKindGroups(node) {
|
||||
const label = dep == null ? `subvariation ${idx + 1}`
|
||||
: isNaN(Number(dep)) ? `ATTR: ${dep}`
|
||||
: String(dep);
|
||||
return { kind: 'variation', node: sv, label, hasChildren: true };
|
||||
return { kind: 'variation', node: sv, label, hasChildren: true, deletable: true };
|
||||
})});
|
||||
}
|
||||
return groups;
|
||||
|
||||
@@ -488,6 +488,43 @@ const artClean = [{ name: 'f', isDirty: false, properties: [{ name: 'add_stress'
|
||||
const { text: artUntouched } = patchScore(ART_SCORE, [], [], null, artClean);
|
||||
ok('clean articles section left verbatim', artUntouched.includes('{ add_stress: 3 }'));
|
||||
|
||||
// ── patchScore articlesModified flag ──────────────────────────────────────
|
||||
section('patchScore articlesModified flag');
|
||||
const artCleanForFlag = [{ name: 'f', isDirty: false, properties: [{ name: 'add_stress', value: 99, scope: 'defaults' }] }];
|
||||
const { text: artFlagPatched, log: artFlagLog } = patchScore(ART_SCORE, [], [], null, artCleanForFlag, { articlesModified: true });
|
||||
ok('articlesModified forces re-serialization', artFlagPatched.includes('add_stress: 99'));
|
||||
ok('articlesModified: old flow-style replaced', !artFlagPatched.includes('{ add_stress: 3 }'));
|
||||
|
||||
// ── patchScore deleted bars ────────────────────────────────────────────────
|
||||
section('patchScore deleted bars');
|
||||
const delBar1 = { id: '001P1L1M1', isDirty: true, deleted: true, stressor: null, tempoLevels: null, upperStressBound: null, lowerStressBound: null, tempoShape: null };
|
||||
const delBar2 = { id: '001P1L1M2', isDirty: false, stressor: null, tempoLevels: null, upperStressBound: null, lowerStressBound: null, tempoShape: null };
|
||||
const { text: delBarPatched } = patchScore(RAW_SCORE_WITH_BARS, [], [delBar1, delBar2]);
|
||||
ok('deleted bar removed from output', !delBarPatched.includes('_id: 001P1L1M1'));
|
||||
ok('non-deleted bar preserved', delBarPatched.includes('_id: 001P1L1M2'));
|
||||
ok('deleted bar reduces document count', (delBarPatched.match(/\n---\n/g) ?? []).length === 1);
|
||||
|
||||
// ── patchScore new bars ────────────────────────────────────────────────────
|
||||
section('patchScore new bars');
|
||||
const newBarEntry = { id: '001P1L1M3', isDirty: true, isNew: true, stressor: null, tempoLevels: 120, upperStressBound: null, lowerStressBound: null, tempoShape: null };
|
||||
const { text: newBarPatched, log: newBarLog } = patchScore(RAW_SCORE_WITH_BARS, [], [newBarEntry]);
|
||||
ok('new bar appended to output', newBarPatched.includes('_id: 001P1L1M3'));
|
||||
ok('new bar has _meta with BPM', newBarPatched.includes('beats_per_minute: 120'));
|
||||
ok('new bar appended as extra document', (newBarPatched.match(/\n---\n/g) ?? []).length === 3);
|
||||
ok('new bar logged as changed', newBarLog.some(e => e.level === 'changed' && e.path === 'bar / 001P1L1M3'));
|
||||
|
||||
// ── patchScore deleted instrument ─────────────────────────────────────────
|
||||
section('patchScore deleted instrument');
|
||||
const delInstrScore = `instrument alpha:\n character:\n A: "1:0,10;1,0"\n\ninstrument ki:\n character:\n A: "1:0,5;1,0"\n`;
|
||||
const delInstrList = [
|
||||
{ name: 'alpha', deleted: true, isDirty: false, variations: [], basicProperties: null, volumes: null, timbre: null, fmModulations: [], amModulations: [] },
|
||||
{ name: 'ki', deleted: false, isDirty: false, variations: [], basicProperties: null, volumes: null, timbre: null, fmModulations: [], amModulations: [] },
|
||||
];
|
||||
const { text: delInstrPatched, log: delInstrLog } = patchScore(delInstrScore, delInstrList);
|
||||
ok('deleted instrument removed from output', !delInstrPatched.includes('instrument alpha:'));
|
||||
ok('non-deleted instrument preserved', delInstrPatched.includes('instrument ki:'));
|
||||
ok('deleted instrument logged', delInstrLog.some(e => e.level === 'changed' && e.path?.includes('alpha') && e.path?.includes('deleted')));
|
||||
|
||||
// ── Motif parsing ─────────────────────────────────────────────────────────
|
||||
section('Motif parsing — dynamic motif');
|
||||
const DYN_MOTIF = `00 bar '001P1L1M1'
|
||||
|
||||
Reference in New Issue
Block a user