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.
|
// One-line summary row with a drill-down chevron.
|
||||||
export const ObjectShort = {
|
export const ObjectShort = {
|
||||||
props: ['label', 'typeTag', 'focused', 'hasChildren', 'readOnly'],
|
props: ['label', 'typeTag', 'focused', 'hasChildren', 'readOnly', 'deletable'],
|
||||||
emits: ['focus', 'drillDown'],
|
emits: ['focus', 'drillDown', 'delete'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
return () => h('li', {
|
return () => h('li', {
|
||||||
class: ['se-object-item', props.focused ? 'focused' : null, props.readOnly ? 'read-only' : null],
|
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,
|
props.typeTag ? h('span', { class: 'se-object-type' }, props.typeTag) : null,
|
||||||
h('span', { class: 'se-object-label' }, props.label),
|
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
|
props.hasChildren
|
||||||
? h('button', {
|
? h('button', {
|
||||||
class: 'se-chevron',
|
class: 'se-chevron',
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ export const PaneCP = {
|
|||||||
const model = props.store.scoreModel;
|
const model = props.store.scoreModel;
|
||||||
const { text: patched, log } = patchScore(
|
const { text: patched, log } = patchScore(
|
||||||
raw, model.instruments, model.bars, model.info, model.articles ?? [],
|
raw, model.instruments, model.bars, model.info, model.articles ?? [],
|
||||||
|
{ articlesModified: !!model.articlesModified },
|
||||||
);
|
);
|
||||||
|
model.articlesModified = false;
|
||||||
props.store.exportLog = log;
|
props.store.exportLog = log;
|
||||||
await putScoreText(patched);
|
await putScoreText(patched);
|
||||||
props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 };
|
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('h4', H4, `Bar: ${node.id}`),
|
||||||
h(ObjectExtended, {
|
h(ObjectExtended, {
|
||||||
fields: [
|
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: 'beats_per_minute', value: node.tempoLevels ?? '', editable: true, type: 'number' },
|
||||||
{ key: 'stress_pattern', value: stressorToString(node.stressor), editable: true },
|
{ key: 'stress_pattern', value: stressorToString(node.stressor), editable: true },
|
||||||
],
|
],
|
||||||
onChange: ({ key, value }) => {
|
onChange: ({ key, value }) => {
|
||||||
|
if (key === 'id') { node.id = value; markBarDirty(); return; }
|
||||||
if (key === 'beats_per_minute') node.tempoLevels = isNaN(value) ? null : value;
|
if (key === 'beats_per_minute') node.tempoLevels = isNaN(value) ? null : value;
|
||||||
if (key === 'stress_pattern') node.stressor = parseStressor(value);
|
if (key === 'stress_pattern') node.stressor = parseStressor(value);
|
||||||
markBarDirty();
|
markBarDirty();
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ function barGroupKey(id) {
|
|||||||
return m ? id.slice(0, m.index) : 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 = {
|
export const PaneSubObjects = {
|
||||||
props: ['store', 'kind', 'onFocusFO'],
|
props: ['store', 'kind', 'onFocusFO'],
|
||||||
setup(props) {
|
setup(props) {
|
||||||
@@ -17,6 +26,67 @@ export const PaneSubObjects = {
|
|||||||
return fp.length ? fp[fp.length - 1] : props.store.scoreModel;
|
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 () => {
|
return () => {
|
||||||
const node = focused();
|
const node = focused();
|
||||||
const groups = getKindGroups(node);
|
const groups = getKindGroups(node);
|
||||||
@@ -25,13 +95,14 @@ export const PaneSubObjects = {
|
|||||||
: groups[0];
|
: groups[0];
|
||||||
const items = group?.items ?? [];
|
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') {
|
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 barGroups = [];
|
||||||
const seen = new Map();
|
const seen = new Map();
|
||||||
for (const item of items) {
|
for (const item of visibleItems) {
|
||||||
const key = barGroupKey(item.label);
|
const key = barGroupKey(item.label);
|
||||||
if (!seen.has(key)) {
|
if (!seen.has(key)) {
|
||||||
const g = { key, items: [] };
|
const g = { key, items: [] };
|
||||||
@@ -41,17 +112,39 @@ export const PaneSubObjects = {
|
|||||||
seen.get(key).items.push(item);
|
seen.get(key).items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
return h('div', { class: 'se-bar-groups' }, barGroups.map(g =>
|
const groupEls = barGroups.map(g => {
|
||||||
h('div', { key: g.key, class: 'se-bar-group' },
|
const lastId = g.items[g.items.length - 1].label;
|
||||||
g.items.map((item, idx) =>
|
return h('div', { key: g.key, class: 'se-bar-group' }, [
|
||||||
h('button', {
|
...g.items.map((item, idx) =>
|
||||||
|
h('span', {
|
||||||
key: idx,
|
key: idx,
|
||||||
class: ['se-bar-chip', props.store.focusPath.includes(item.node) ? 'focused' : null],
|
class: ['se-bar-chip', props.store.focusPath.includes(item.node) ? 'focused' : null],
|
||||||
|
}, [
|
||||||
|
h('span', {
|
||||||
|
class: 'se-bar-chip-label',
|
||||||
onClick: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
onClick: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
||||||
}, item.label)
|
}, 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,
|
return h('div', null,
|
||||||
@@ -63,8 +156,10 @@ export const PaneSubObjects = {
|
|||||||
focused: props.store.focusPath.includes(item.node),
|
focused: props.store.focusPath.includes(item.node),
|
||||||
hasChildren: item.hasChildren,
|
hasChildren: item.hasChildren,
|
||||||
readOnly: item.readOnly ?? false,
|
readOnly: item.readOnly ?? false,
|
||||||
|
deletable: item.deletable ?? false,
|
||||||
onFocus: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
onFocus: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
|
||||||
onDrillDown: () => { 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) {
|
if (m) {
|
||||||
const rawName = m[1].replace(/^'|'$/g, '');
|
const rawName = m[1].replace(/^'|'$/g, '');
|
||||||
const instr = instrMap[rawName];
|
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++;
|
i++;
|
||||||
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) i++;
|
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) i++;
|
||||||
result.push(exportInstrument(instr));
|
result.push(exportInstrument(instr));
|
||||||
@@ -280,7 +283,15 @@ function patchBarMeta(doc, bar) {
|
|||||||
return out.join('\n');
|
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 log = [];
|
||||||
const SEP = '\n---\n';
|
const SEP = '\n---\n';
|
||||||
const [header, ...barDocs] = rawScoreText.split(SEP);
|
const [header, ...barDocs] = rawScoreText.split(SEP);
|
||||||
@@ -288,28 +299,43 @@ export function patchScore(rawScoreText, instruments, bars = [], info = null, ar
|
|||||||
let patchedHeader = patchMetadata(header, info);
|
let patchedHeader = patchMetadata(header, info);
|
||||||
|
|
||||||
const dirtyArticles = articles.filter(a => a.isDirty);
|
const dirtyArticles = articles.filter(a => a.isDirty);
|
||||||
if (dirtyArticles.length) {
|
if (dirtyArticles.length || flags.articlesModified) {
|
||||||
patchedHeader = patchArticles(patchedHeader, articles);
|
patchedHeader = patchArticles(patchedHeader, articles);
|
||||||
for (const a of dirtyArticles) log.push({ level: 'changed', path: `articles / ${a.name}` });
|
for (const a of dirtyArticles) log.push({ level: 'changed', path: `articles / ${a.name}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
patchedHeader = patchInstrumentHeader(patchedHeader, instruments);
|
patchedHeader = patchInstrumentHeader(patchedHeader, instruments);
|
||||||
for (const i of instruments.filter(i => i.isDirty)) log.push({ level: 'changed', path: `instrument / ${i.name}` });
|
for (const i of instruments) {
|
||||||
|
if (i.deleted) log.push({ level: 'changed', path: `instrument / ${i.name} (deleted)` });
|
||||||
if (!barDocs.length) return { text: patchedHeader, log };
|
else if (i.isDirty) log.push({ level: 'changed', path: `instrument / ${i.name}` });
|
||||||
|
}
|
||||||
|
|
||||||
const barMap = {};
|
const barMap = {};
|
||||||
for (const bar of bars) barMap[bar.id] = bar;
|
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;
|
let passedThrough = 0;
|
||||||
const patchedBarDocs = barDocs.map(doc => {
|
const patchedBarDocs = [];
|
||||||
|
for (const doc of barDocs) {
|
||||||
const m = doc.match(/^_id:\s*(\S+)/m);
|
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]];
|
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}` });
|
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) {
|
if (passedThrough > 0) {
|
||||||
log.push({ level: 'info', message: `${passedThrough} bar document${passedThrough !== 1 ? 's' : ''} passed through unchanged` });
|
log.push({ level: 'info', message: `${passedThrough} bar document${passedThrough !== 1 ? 's' : ''} passed through unchanged` });
|
||||||
|
|||||||
@@ -320,21 +320,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.se-bar-chip {
|
.se-bar-chip {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
padding: 0.1rem 0.25rem;
|
|
||||||
border: 1px solid #383838;
|
border: 1px solid #383838;
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
color: #777;
|
color: #777;
|
||||||
cursor: pointer;
|
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
vertical-align: middle;
|
||||||
|
|
||||||
.se-bar-chip:hover {
|
|
||||||
color: #ccc;
|
|
||||||
border-color: #555;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.se-bar-chip.focused {
|
.se-bar-chip.focused {
|
||||||
@@ -343,6 +338,49 @@
|
|||||||
border-color: #4a7aaa;
|
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) */
|
/* Export log (shown in CP pane after export) */
|
||||||
.se-export-log {
|
.se-export-log {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
@@ -387,3 +425,17 @@
|
|||||||
color: #e06060;
|
color: #e06060;
|
||||||
border-color: #602020;
|
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) {
|
if (node.instruments.length) {
|
||||||
groups.push({ kind: 'instrument', items: node.instruments.map(i => ({
|
groups.push({ kind: 'instrument', items: node.instruments.map(i => ({
|
||||||
kind: 'instrument', node: i, label: i.name, hasChildren: !i.isLinked, readOnly: i.isLinked,
|
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;
|
acc[p.scope] = (acc[p.scope] ?? 0) + 1; return acc;
|
||||||
}, {});
|
}, {});
|
||||||
const suffix = Object.entries(counts).map(([s, n]) => `${n} ${s}`).join(', ');
|
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) {
|
if (node.bars.length) {
|
||||||
groups.push({ kind: 'bar', items: node.bars.map(b => ({
|
groups.push({ kind: 'bar', items: node.bars.map(b => ({
|
||||||
kind: 'bar', node: b, label: b.id, hasChildren: Object.keys(b.voices).length > 0,
|
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) => ({
|
return [{ kind: 'variation', items: node.variations.map((v, idx) => ({
|
||||||
kind: 'variation', node: v,
|
kind: 'variation', node: v,
|
||||||
label: `variation ${idx + 1}${v.dependsOn ? ` (${v.dependsOn})` : ''}`,
|
label: `variation ${idx + 1}${v.dependsOn ? ` (${v.dependsOn})` : ''}`,
|
||||||
hasChildren: true,
|
hasChildren: true, deletable: true,
|
||||||
}))}];
|
}))}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ export function getKindGroups(node) {
|
|||||||
const groups = [];
|
const groups = [];
|
||||||
if (node.labelSpecs.length) {
|
if (node.labelSpecs.length) {
|
||||||
groups.push({ kind: 'label_spec', items: node.labelSpecs.map(ls => ({
|
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) {
|
if (node.subvariations.length) {
|
||||||
@@ -83,7 +85,7 @@ export function getKindGroups(node) {
|
|||||||
const label = dep == null ? `subvariation ${idx + 1}`
|
const label = dep == null ? `subvariation ${idx + 1}`
|
||||||
: isNaN(Number(dep)) ? `ATTR: ${dep}`
|
: isNaN(Number(dep)) ? `ATTR: ${dep}`
|
||||||
: String(dep);
|
: String(dep);
|
||||||
return { kind: 'variation', node: sv, label, hasChildren: true };
|
return { kind: 'variation', node: sv, label, hasChildren: true, deletable: true };
|
||||||
})});
|
})});
|
||||||
}
|
}
|
||||||
return groups;
|
return groups;
|
||||||
|
|||||||
@@ -488,6 +488,43 @@ const artClean = [{ name: 'f', isDirty: false, properties: [{ name: 'add_stress'
|
|||||||
const { text: artUntouched } = patchScore(ART_SCORE, [], [], null, artClean);
|
const { text: artUntouched } = patchScore(ART_SCORE, [], [], null, artClean);
|
||||||
ok('clean articles section left verbatim', artUntouched.includes('{ add_stress: 3 }'));
|
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 ─────────────────────────────────────────────────────────
|
// ── Motif parsing ─────────────────────────────────────────────────────────
|
||||||
section('Motif parsing — dynamic motif');
|
section('Motif parsing — dynamic motif');
|
||||||
const DYN_MOTIF = `00 bar '001P1L1M1'
|
const DYN_MOTIF = `00 bar '001P1L1M1'
|
||||||
|
|||||||
Reference in New Issue
Block a user