Phase 4 — remove edit-state flags from domain objects: - ast-parser.js: drop isDirty from buildInstrument, buildBar, buildStemNote - store.js: add resetEditState() - PaneCP.js: use resetEditState() on import; drop flags arg from patchScore - PaneFO.js: isDirty -> _modified for linked instruments; no per-node dirty tracking - PaneSubObjects.js: _spliceNode helper; deletion via splice; _isNew on new bars - exporter.js: on-demand traversal; absence from model means deleted Phase 5 — article parser and exporter: - ast-parser.js: _buildArticleEntry replaces _mergeArticleEntry; stage.article and voice.article dispatch; article properties carry scope and overwritten flag - exporter.js: _buildArticlesBlock emits -important: list for overwrites scope Phase 6 — stem note dead fields: - ast-parser.js: parse weight and articulatory on stem notes; flatten article.definite properties onto stem note model sibling to pitch/chain
165 lines
6.8 KiB
JavaScript
165 lines
6.8 KiB
JavaScript
import { h } from 'vue';
|
||
import { ObjectShort } from './ObjectShort.js';
|
||
import { getKindGroups } from '../subobject-kinds.js';
|
||
|
||
function _barGroupKey(id) {
|
||
const m = id.match(/(\d+|\D+)$/);
|
||
return m ? id.slice(0, m.index) : id;
|
||
}
|
||
|
||
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) {
|
||
function focused() {
|
||
const fp = props.store.focusPath;
|
||
return fp.length ? fp[fp.length - 1] : props.store.scoreModel;
|
||
}
|
||
|
||
function _spliceNode(arr, node) {
|
||
const idx = arr.indexOf(node);
|
||
if (idx !== -1) { arr.splice(idx, 1); props.store.markDirty(); }
|
||
}
|
||
|
||
function deleteItem(kind, node) {
|
||
const store = props.store;
|
||
const model = store.scoreModel;
|
||
if (!model) return;
|
||
|
||
if (kind === 'instrument') {
|
||
_spliceNode(model.instruments, node);
|
||
} else if (kind === 'articles') {
|
||
_spliceNode(model.articles, node);
|
||
} else if (kind === 'bar') {
|
||
_spliceNode(model.bars, node);
|
||
} 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._modified = 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._modified = 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._modified = 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._modified = 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 ?? [];
|
||
newId = liveBars.length ? _incrementId(liveBars[liveBars.length - 1].id) : 'bar001';
|
||
}
|
||
const newBar = {
|
||
type: 'bar', id: newId, _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);
|
||
const group = props.kind
|
||
? groups.find(g => g.kind === props.kind)
|
||
: groups[0];
|
||
const items = group?.items ?? [];
|
||
|
||
if (!items.length && props.kind !== 'bar') return h('div', null, h('em', null, 'No sub-objects'));
|
||
|
||
if (props.kind === 'bar') {
|
||
const barGroups = [];
|
||
const seen = new Map();
|
||
for (const item of items) {
|
||
const key = _barGroupKey(item.label);
|
||
if (!seen.has(key)) {
|
||
const g = { key, items: [] };
|
||
barGroups.push(g);
|
||
seen.set(key, g);
|
||
}
|
||
seen.get(key).items.push(item);
|
||
}
|
||
|
||
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],
|
||
}, [
|
||
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,
|
||
h('ul', { class: 'se-object-list' }, items.map((item, idx) =>
|
||
h(ObjectShort, {
|
||
key: idx,
|
||
label: item.label,
|
||
typeTag: item.kind,
|
||
focused: props.store.focusPath.includes(item.node),
|
||
hasChildren: item.hasChildren,
|
||
readOnly: item.readOnly ?? false,
|
||
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,
|
||
})
|
||
))
|
||
);
|
||
};
|
||
},
|
||
};
|