Files
vue3js-app-proposal-for-sdk…/static/components/PaneCP.js
c0dev0id 7ffa0a9d2d 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
2026-07-11 12:07:05 +02:00

147 lines
6.2 KiB
JavaScript

import { h, ref, watch, onMounted } from 'vue';
import { fetchAstLog, fetchScoreText, putScoreText, fetchAudioWidget, URLS } from '../api.js';
import { parseAstLog, buildModel } from '../ast-parser.js';
import { patchScore } from '../exporter.js';
import { shortView } from '../node-views.js';
import { StatusPoller } from './StatusPoller.js';
export const PaneCP = {
props: ['store', 'importOnLoad', 'onFocusFO'],
setup(props) {
const importing = ref(false);
const importError = ref('');
const exporting = ref(false);
const exportError = ref('');
const audioWidgetHtml = ref('');
watch(
() => props.store.synthesisStatus,
async (s) => {
if (!s?.frozen) { audioWidgetHtml.value = ''; return; }
audioWidgetHtml.value = await fetchAudioWidget(
s.file_accomplished ? URLS.result : null,
s.errors ?? null,
);
},
);
async function doImport() {
if (props.store.isDirty) return;
importError.value = '';
importing.value = true;
try {
const text = await fetchAstLog();
props.store.scoreModel = buildModel(parseAstLog(text));
props.store.resetEditState();
} catch (e) {
importError.value = e.message;
} finally {
importing.value = false;
}
}
async function doExport() {
exportError.value = '';
exporting.value = true;
try {
const raw = await fetchScoreText();
props.store.rawScoreText = raw;
const model = props.store.scoreModel;
const { text: patched, log } = patchScore(
raw, model.instruments, model.bars, model.info, model.articles ?? [],
);
props.store.exportLog = log;
await putScoreText(patched);
props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 };
} catch (e) {
exportError.value = e.message;
} finally {
exporting.value = false;
}
}
onMounted(() => { if (props.importOnLoad) doImport(); });
return () => {
const store = props.store;
const model = store.scoreModel;
const fp = store.focusPath;
// Build vertical path list: score root + each focus node.
const pathItems = model ? [
{ node: model, idx: -1 },
...fp.map((node, idx) => ({ node, idx })),
] : [];
return h('div', null, [
// Header
h('div', { class: 'se-cp-header' }, [
h('button', {
class: 'se-btn',
disabled: store.isDirty || importing.value,
title: store.isDirty ? 'Save or discard edits before re-importing' : 'Import from server',
onClick: doImport,
}, importing.value ? 'Importing…' : '→ Import'),
h('span', { class: 'se-cp-title' },
model ? (model.info?.title ?? 'Untitled score') : 'No score loaded'),
model ? h('button', {
class: 'se-btn se-btn-primary',
disabled: !store.isDirty || exporting.value,
onClick: doExport,
}, exporting.value ? 'Exporting…' : 'Export ↑') : null,
]),
// Vertical focus path — short views, clickable to navigate up
pathItems.length ? h('ul', { class: 'se-object-list se-cp-path' },
pathItems.map(({ node, idx }) => {
const { typeTag, label, meta } = shortView(node);
const isCurrent = idx === fp.length - 1 || (idx === -1 && fp.length === 0);
return h('li', {
class: ['se-object-item', isCurrent ? 'focused' : null],
onClick: () => {
if (idx === -1) store.setFocus([]);
else store.setFocus(fp.slice(0, idx + 1));
props.onFocusFO?.();
},
}, [
h('span', { class: 'se-object-type' }, typeTag),
h('span', { class: 'se-object-label' }, [
h('strong', null, label),
...meta.map(({ key, value }) =>
h('span', { style: 'color:#888;margin-left:0.5rem;font-size:0.8em' },
`${key}=${value}`)
),
]),
]);
})
) : null,
// Import / export errors
importError.value ? h('div', { class: 'se-error' }, importError.value) : null,
exportError.value ? h('div', { class: 'se-error' }, exportError.value) : null,
// Export log (shown after export, cleared on next import)
store.exportLog?.length
? h('div', { class: 'se-export-log' },
store.exportLog.map((entry, i) =>
h('div', {
key: i,
class: entry.level === 'changed' ? 'se-export-changed' : 'se-export-info',
}, entry.path ? `${entry.path}` : entry.message)
)
)
: null,
// Status poller (while running)
store.synthesisStatus && !store.synthesisStatus.frozen
? h(StatusPoller, { store }) : null,
// Audio widget (rendered server-side from audiowidget.tmpl)
audioWidgetHtml.value
? h('div', { innerHTML: audioWidgetHtml.value, style: 'margin-top:0.5rem' })
: null,
]);
};
},
};