forked from flow/vue3js-app-proposal-for-sdk-claude
Make bars navigable and editable with _meta export
Navigation: - SubObjects: bar → voice (with chevron when offsets exist) → offsets - PaneCP shortView handles voice and offset types FO pane: - Bar: editable beats_per_minute, stress_pattern (serialized as groups string), upper/lower stress bounds and tempo shape via ShapeEditor; sets bar.isDirty on change - Voice: read-only name, articles, motifs, offset count - Offset: read-only tick, stem notes and clusters as label strings (note re-serialization deferred) Export: - exporter.patchScore() now accepts optional bars[] parameter - Splits rawScoreText by \n---\n; instruments patched in header, dirty bar _meta blocks regenerated, voice note lines kept verbatim - ast-parser buildBar() adds isDirty:false - PaneCP passes scoreModel.bars to patchScore
This commit is contained in:
parent
338ea5be49
commit
9eb4add695
@ -328,6 +328,7 @@ function buildBar(node) {
|
|||||||
const bar = {
|
const bar = {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
id: node.positionals[0] ?? '',
|
id: node.positionals[0] ?? '',
|
||||||
|
isDirty: false,
|
||||||
stressor: null,
|
stressor: null,
|
||||||
tempoShape: null,
|
tempoShape: null,
|
||||||
tempoLevels: null,
|
tempoLevels: null,
|
||||||
|
|||||||
@ -26,6 +26,10 @@ function shortView(node) {
|
|||||||
return { typeTag: 'label', label: node.label ?? '(no label)', meta: [] };
|
return { typeTag: 'label', label: node.label ?? '(no label)', meta: [] };
|
||||||
case 'bar':
|
case 'bar':
|
||||||
return { typeTag: 'bar', label: node.id, meta: [] };
|
return { typeTag: 'bar', label: node.id, meta: [] };
|
||||||
|
case 'voice':
|
||||||
|
return { typeTag: 'voice', label: node.name, meta: [] };
|
||||||
|
case 'offset':
|
||||||
|
return { typeTag: 'tick', label: String(node.tick ?? '?'), meta: [] };
|
||||||
default:
|
default:
|
||||||
return { typeTag: node.type, label: node.type, meta: [] };
|
return { typeTag: node.type, label: node.type, meta: [] };
|
||||||
}
|
}
|
||||||
@ -43,7 +47,7 @@ export const PaneCP = {
|
|||||||
try {
|
try {
|
||||||
const raw = await fetchScoreText(props.store.credentials);
|
const raw = await fetchScoreText(props.store.credentials);
|
||||||
props.store.rawScoreText = raw;
|
props.store.rawScoreText = raw;
|
||||||
const patched = patchScore(raw, props.store.scoreModel.instruments);
|
const patched = patchScore(raw, props.store.scoreModel.instruments, props.store.scoreModel.bars);
|
||||||
await putScoreText(patched, props.store.credentials);
|
await putScoreText(patched, props.store.credentials);
|
||||||
props.store.synthesisStatus = { frozen: false, progress: 0 };
|
props.store.synthesisStatus = { frozen: false, progress: 0 };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -1,8 +1,21 @@
|
|||||||
import { h, ref } from 'vue';
|
import { h, ref } from 'vue';
|
||||||
import { ObjectExtended } from './ObjectExtended.js';
|
import { ObjectExtended } from './ObjectExtended.js';
|
||||||
import { EnvelopeEditor } from './EnvelopeEditor.js';
|
import { EnvelopeEditor } from './EnvelopeEditor.js';
|
||||||
|
import { ShapeEditor } from './ShapeEditor.js';
|
||||||
import { LinkedInstrumentModal } from './LinkedInstrumentModal.js';
|
import { LinkedInstrumentModal } from './LinkedInstrumentModal.js';
|
||||||
|
|
||||||
|
function stressorToString(s) {
|
||||||
|
if (!s?.groups?.length) return '';
|
||||||
|
return s.groups.map(g => g.join(',')).join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStressor(str) {
|
||||||
|
const groups = str.split(';').map(seg =>
|
||||||
|
seg.split(',').map(n => parseInt(n, 10)).filter(n => !isNaN(n))
|
||||||
|
).filter(g => g.length > 0);
|
||||||
|
return groups.length ? { type: 'stressor', groups } : null;
|
||||||
|
}
|
||||||
|
|
||||||
function scoreInfoFields(info) {
|
function scoreInfoFields(info) {
|
||||||
return [
|
return [
|
||||||
{ key: 'title', value: info?.title ?? '', editable: true },
|
{ key: 'title', value: info?.title ?? '', editable: true },
|
||||||
@ -136,11 +149,69 @@ export const PaneFO = {
|
|||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
} else if (node.type === 'bar') {
|
} else if (node.type === 'bar') {
|
||||||
|
const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); };
|
||||||
children.push(
|
children.push(
|
||||||
h('h4', { style: 'margin:0 0 0.5rem' }, `Bar: ${node.id}`),
|
h('h4', { style: 'margin:0 0 0.5rem' }, `Bar: ${node.id}`),
|
||||||
h(ObjectExtended, { fields: [
|
h(ObjectExtended, {
|
||||||
|
fields: [
|
||||||
{ key: 'id', value: node.id, editable: false },
|
{ key: 'id', value: node.id, editable: false },
|
||||||
], onChange: null }),
|
{ 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 === 'beats_per_minute') node.tempoLevels = isNaN(value) ? null : value;
|
||||||
|
if (key === 'stress_pattern') node.stressor = parseStressor(value);
|
||||||
|
markBarDirty();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
node.upperStressBound ? h('div', { style: 'margin-top:0.5rem' }, [
|
||||||
|
h('strong', null, 'Upper stress bound'),
|
||||||
|
h(ShapeEditor, { shape: node.upperStressBound, onChange: markBarDirty }),
|
||||||
|
]) : null,
|
||||||
|
node.lowerStressBound ? h('div', { style: 'margin-top:0.5rem' }, [
|
||||||
|
h('strong', null, 'Lower stress bound'),
|
||||||
|
h(ShapeEditor, { shape: node.lowerStressBound, onChange: markBarDirty }),
|
||||||
|
]) : null,
|
||||||
|
node.tempoShape ? h('div', { style: 'margin-top:0.5rem' }, [
|
||||||
|
h('strong', null, 'Tempo shape'),
|
||||||
|
h(ShapeEditor, { shape: node.tempoShape, onChange: markBarDirty }),
|
||||||
|
]) : null,
|
||||||
|
);
|
||||||
|
} else if (node.type === 'voice') {
|
||||||
|
children.push(
|
||||||
|
h('h4', { style: 'margin:0 0 0.5rem' }, `Voice: ${node.name}`),
|
||||||
|
h(ObjectExtended, {
|
||||||
|
fields: [
|
||||||
|
{ key: 'articles', value: node.articles.join(', ') || '—', editable: false },
|
||||||
|
{ key: 'motifs', value: node.motifs.join(', ') || '—', editable: false },
|
||||||
|
{ key: 'offsets', value: String(node.offsets.length), editable: false },
|
||||||
|
],
|
||||||
|
onChange: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (node.type === 'offset') {
|
||||||
|
const noteStr = n => `${n.pitch}${n.effLength != null ? ' ' + n.effLength : ''}`;
|
||||||
|
const clusterStr = c => c.notes.length
|
||||||
|
? c.notes.map(n => `${n.letter ?? ''}${n.shift != null ? n.shift : ''}${n.length != null ? ' ' + n.length : ''}`).join(', ')
|
||||||
|
: `cluster[${c.index}]`;
|
||||||
|
children.push(
|
||||||
|
h('h4', { style: 'margin:0 0 0.5rem' }, `Tick: ${node.tick}`),
|
||||||
|
h(ObjectExtended, {
|
||||||
|
fields: [{ key: 'tick', value: node.tick, editable: false }],
|
||||||
|
onChange: null,
|
||||||
|
}),
|
||||||
|
node.stemNotes.length ? h('div', { style: 'margin-top:0.5rem' }, [
|
||||||
|
h('strong', null, 'Stem notes'),
|
||||||
|
h('ul', { class: 'se-object-list' }, node.stemNotes.map((n, i) =>
|
||||||
|
h('li', { class: 'se-object-item', key: i },
|
||||||
|
h('span', { class: 'se-object-label' }, noteStr(n))))),
|
||||||
|
]) : null,
|
||||||
|
node.clusters.length ? h('div', { style: 'margin-top:0.5rem' }, [
|
||||||
|
h('strong', null, 'Clusters'),
|
||||||
|
h('ul', { class: 'se-object-list' }, node.clusters.map((c, i) =>
|
||||||
|
h('li', { class: 'se-object-item', key: i },
|
||||||
|
h('span', { class: 'se-object-label' }, clusterStr(c))))),
|
||||||
|
]) : null,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
children.push(h('pre', { style: 'font-size:0.75rem;white-space:pre-wrap' },
|
children.push(h('pre', { style: 'font-size:0.75rem;white-space:pre-wrap' },
|
||||||
|
|||||||
@ -51,7 +51,14 @@ export const PaneSubObjects = {
|
|||||||
}
|
}
|
||||||
if (node.type === 'bar') {
|
if (node.type === 'bar') {
|
||||||
return Object.entries(node.voices).map(([name, v]) => ({
|
return Object.entries(node.voices).map(([name, v]) => ({
|
||||||
kind: 'voice', node: v, label: name,
|
kind: 'voice', node: v, label: name, hasChildren: v.offsets.length > 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (node.type === 'voice') {
|
||||||
|
return node.offsets.map((o, idx) => ({
|
||||||
|
kind: 'offset', node: o,
|
||||||
|
label: `tick ${o.tick ?? idx}`,
|
||||||
|
hasChildren: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@ -134,11 +134,11 @@ export function exportInstrument(instr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Score patch ────────────────────────────────────────────────────────────
|
// ── Score patch ────────────────────────────────────────────────────────────
|
||||||
// Replace dirty instrument blocks in rawScoreText with RFC-serialized output.
|
// Replace dirty instrument blocks and dirty bar _meta blocks.
|
||||||
// Non-dirty instruments are left verbatim.
|
// Voice note content in bar documents is left verbatim.
|
||||||
|
|
||||||
export function patchScore(rawScoreText, instruments) {
|
function patchInstrumentHeader(text, instruments) {
|
||||||
const lines = rawScoreText.split('\n');
|
const lines = text.split('\n');
|
||||||
const result = [];
|
const result = [];
|
||||||
const instrMap = {};
|
const instrMap = {};
|
||||||
for (const instr of instruments) {
|
for (const instr of instruments) {
|
||||||
@ -149,7 +149,6 @@ export function patchScore(rawScoreText, instruments) {
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
// RFC §4.4: "instrument NAME:" — strip optional quotes around name
|
|
||||||
const m = line.match(/^instrument\s+(.+?)\s*:/);
|
const m = line.match(/^instrument\s+(.+?)\s*:/);
|
||||||
if (m) {
|
if (m) {
|
||||||
const rawName = m[1].replace(/^'|'$/g, '');
|
const rawName = m[1].replace(/^'|'$/g, '');
|
||||||
@ -171,3 +170,62 @@ export function patchScore(rawScoreText, instruments) {
|
|||||||
|
|
||||||
return result.join('\n');
|
return result.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patchBarMeta(doc, bar) {
|
||||||
|
const props = [];
|
||||||
|
if (bar.stressor?.groups?.length)
|
||||||
|
props.push(` stress_pattern: ${bar.stressor.groups.map(g => g.join(',')).join(';')}`);
|
||||||
|
if (bar.tempoLevels != null)
|
||||||
|
props.push(` beats_per_minute: ${bar.tempoLevels}`);
|
||||||
|
const ub = serializeShape(bar.upperStressBound);
|
||||||
|
if (ub) props.push(` upper_stress_bound: ${ub}`);
|
||||||
|
const lb = serializeShape(bar.lowerStressBound);
|
||||||
|
if (lb) props.push(` lower_stress_bound: ${lb}`);
|
||||||
|
if (bar.tempoShape) { const ts = serializeShape(bar.tempoShape); if (ts) props.push(` tempo_shape: "${ts}"`); }
|
||||||
|
|
||||||
|
const lines = doc.split('\n');
|
||||||
|
const out = [];
|
||||||
|
let i = 0;
|
||||||
|
let replaced = false;
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
if (lines[i] === '_meta:') {
|
||||||
|
replaced = true;
|
||||||
|
i++;
|
||||||
|
while (i < lines.length && lines[i].startsWith(' ')) i++;
|
||||||
|
if (props.length) { out.push('_meta:'); out.push(...props); }
|
||||||
|
} else {
|
||||||
|
out.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!replaced && props.length) {
|
||||||
|
const idIdx = out.findIndex(l => /^_id:/.test(l));
|
||||||
|
if (idIdx !== -1) out.splice(idIdx + 1, 0, '_meta:', ...props);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchScore(rawScoreText, instruments, bars = []) {
|
||||||
|
const SEP = '\n---\n';
|
||||||
|
const [header, ...barDocs] = rawScoreText.split(SEP);
|
||||||
|
|
||||||
|
const patchedHeader = patchInstrumentHeader(header, instruments);
|
||||||
|
|
||||||
|
if (!barDocs.length) return patchedHeader;
|
||||||
|
|
||||||
|
const barMap = {};
|
||||||
|
for (const bar of bars) barMap[bar.id] = bar;
|
||||||
|
|
||||||
|
const patchedBarDocs = barDocs.map(doc => {
|
||||||
|
const m = doc.match(/^_id:\s*(\S+)/m);
|
||||||
|
if (!m) return doc;
|
||||||
|
const bar = barMap[m[1]];
|
||||||
|
if (!bar?.isDirty) return doc;
|
||||||
|
return patchBarMeta(doc, bar);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [patchedHeader, ...patchedBarDocs].join(SEP);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user