phases 4-6: article system, stem note fields, edit-state cleanup

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
This commit is contained in:
c0dev0id
2026-07-11 12:07:05 +02:00
parent e2b7af77d6
commit 7ffa0a9d2d
7 changed files with 166 additions and 129 deletions

View File

@@ -32,7 +32,7 @@ export const PaneCP = {
try {
const text = await fetchAstLog();
props.store.scoreModel = buildModel(parseAstLog(text));
props.store.exportLog = [];
props.store.resetEditState();
} catch (e) {
importError.value = e.message;
} finally {
@@ -49,9 +49,7 @@ 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 };

View File

@@ -61,10 +61,10 @@ export const PaneFO = {
// Guard: first edit to a linked instrument triggers embed-or-discard before committing.
function makeChangeHandler(instr) {
return (info) => {
if (instr.isLinked && !instr.isDirty) {
if (instr.isLinked && !instr._modified) {
pendingEdit.value = { instr, undo: info?.undo };
} else {
instr.isDirty = true;
instr._modified = true;
props.store.markDirty();
}
};
@@ -73,7 +73,7 @@ export const PaneFO = {
function embedInstrument(instr) {
instr.name = instr.name.split('/').pop();
instr.isLinked = false;
instr.isDirty = true;
instr._modified = true;
pendingEdit.value = null;
props.store.markDirty();
}
@@ -152,7 +152,7 @@ export const PaneFO = {
: null,
);
} else if (node.type === 'article') {
const markDirty = () => { node.isDirty = true; props.store.markDirty(); };
const markDirty = () => { props.store.markDirty(); };
const rowStyle = 'display:flex;gap:0.4rem;align-items:center;padding:0.2rem 0';
children.push(
h('h4', H4, `Article: ${node.name}`),
@@ -209,12 +209,12 @@ export const PaneFO = {
]),
);
} else if (node.type === 'bar') {
const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); };
const markBarDirty = () => { props.store.markDirty(); };
children.push(
h('h4', H4, `Bar: ${node.id}`),
h(ObjectExtended, {
fields: [
{ key: 'id', value: node.id, editable: node.isNew ?? 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 },
],
@@ -234,7 +234,7 @@ export const PaneFO = {
h('h4', H4, `Voice: ${node.name}`),
h(ObjectExtended, {
fields: [
{ key: 'articles', value: node.articles.join(', ') || '—', editable: false },
{ key: 'articles', value: node.articles.map(a => a.name).join(', ') || '—', editable: false },
{ key: 'motifs', value: node.motifs.map(m => m.label).join(', ') || '—', editable: false },
{ key: 'offsets', value: String(node.offsets.length), editable: false },
],
@@ -273,10 +273,7 @@ export const PaneFO = {
}),
);
} else if (node.type === 'stem_note') {
const bar = props.store.focusPath.find(n => n.type === 'bar') ?? null;
const markDirty = () => {
node.isDirty = true;
if (bar) bar.isDirty = true;
props.store.markDirty();
};
children.push(

View File

@@ -23,38 +23,39 @@ export const PaneSubObjects = {
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') {
const idx = model.instruments.indexOf(node);
if (idx !== -1) { model.instruments.splice(idx, 1); store.markDirty(); }
_spliceNode(model.instruments, node);
} else if (kind === 'articles') {
const idx = (model.articles ?? []).indexOf(node);
if (idx !== -1) { model.articles.splice(idx, 1); model.articlesModified = true; store.markDirty(); }
_spliceNode(model.articles, node);
} else if (kind === 'bar') {
node.deleted = true;
node.isDirty = true;
store.markDirty();
_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.isDirty = true; store.markDirty(); return; }
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.isDirty = true; store.markDirty(); return; }
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.isDirty = true; store.markDirty(); return; }
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.isDirty = true; store.markDirty(); return; }
if (idx2 !== -1) { sv.labelSpecs.splice(idx2, 1); instr._modified = true; store.markDirty(); return; }
}
}
}
@@ -69,11 +70,11 @@ export const PaneSubObjects = {
if (afterId) {
newId = _incrementId(afterId);
} else {
const liveBars = (model.bars ?? []).filter(b => !b.deleted);
const liveBars = model.bars ?? [];
newId = liveBars.length ? _incrementId(liveBars[liveBars.length - 1].id) : 'bar001';
}
const newBar = {
type: 'bar', id: newId, isDirty: true, isNew: true,
type: 'bar', id: newId, _isNew: true,
stressor: null, tempoLevels: null,
upperStressBound: null, lowerStressBound: null, tempoShape: null,
voices: {},
@@ -95,11 +96,9 @@ export const PaneSubObjects = {
if (!items.length && props.kind !== 'bar') return h('div', null, h('em', null, 'No sub-objects'));
if (props.kind === 'bar') {
const visibleItems = items.filter(item => !item.node.deleted);
const barGroups = [];
const seen = new Map();
for (const item of visibleItems) {
for (const item of items) {
const key = _barGroupKey(item.label);
if (!seen.has(key)) {
const g = { key, items: [] };