diff --git a/static/ast-parser.js b/static/ast-parser.js index f785233..d1457b8 100644 --- a/static/ast-parser.js +++ b/static/ast-parser.js @@ -7,7 +7,7 @@ function coerce(s) { if (s === 'False' || s === 'N' || s === 'off' || s === 'false') return false; if (s === '') return s; const n = Number(s); - if (!isNaN(n) && s.trim() !== '') return n; + if (!isNaN(n)) return n; return s; } diff --git a/static/components/ObjectBasic.js b/static/components/ObjectBasic.js deleted file mode 100644 index b83eb20..0000000 --- a/static/components/ObjectBasic.js +++ /dev/null @@ -1,15 +0,0 @@ -import { h } from 'vue'; - -// Inline identifier + a few key props — used for list rows. -export const ObjectBasic = { - props: ['label', 'meta'], // meta: array of {key, value} pairs - setup(props) { - return () => h('div', { class: 'se-object-label' }, [ - h('strong', null, props.label), - ...(props.meta ?? []).map(({ key, value }) => - h('span', { style: 'color:#888;margin-left:0.5rem;font-size:0.8em' }, - `${key}=${value}`) - ), - ]); - }, -}; diff --git a/static/components/ObjectShort.js b/static/components/ObjectShort.js index 7f11d6c..bcebc58 100644 --- a/static/components/ObjectShort.js +++ b/static/components/ObjectShort.js @@ -2,7 +2,7 @@ import { h } from 'vue'; // One-line summary row with a drill-down chevron. export const ObjectShort = { - props: ['node', 'label', 'typeTag', 'focused', 'hasChildren'], + props: ['label', 'typeTag', 'focused', 'hasChildren'], emits: ['focus', 'drillDown'], setup(props, { emit }) { return () => h('li', { diff --git a/static/components/PaneCP.js b/static/components/PaneCP.js index b35ac21..fa1a5b4 100644 --- a/static/components/PaneCP.js +++ b/static/components/PaneCP.js @@ -49,7 +49,7 @@ export const PaneCP = { props.store.rawScoreText = raw; const patched = patchScore(raw, props.store.scoreModel.instruments, props.store.scoreModel.bars); await putScoreText(patched, props.store.credentials); - props.store.synthesisStatus = { frozen: false, progress: 0 }; + props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 }; } catch (e) { exportError.value = e.message; } finally { diff --git a/static/components/PaneFO.js b/static/components/PaneFO.js index a61311e..1954be7 100644 --- a/static/components/PaneFO.js +++ b/static/components/PaneFO.js @@ -3,11 +3,9 @@ import { ObjectExtended } from './ObjectExtended.js'; import { EnvelopeEditor } from './EnvelopeEditor.js'; import { ShapeEditor } from './ShapeEditor.js'; import { LinkedInstrumentModal } from './LinkedInstrumentModal.js'; +import { stressorToString } from '../exporter.js'; -function stressorToString(s) { - if (!s?.groups?.length) return ''; - return s.groups.map(g => g.join(',')).join(';'); -} +const H4 = { style: 'margin:0 0 0.5rem' }; function parseStressor(str) { const groups = str.split(';').map(seg => @@ -27,38 +25,42 @@ function scoreInfoFields(info) { function instrFields(instr) { return [ - { key: 'name', value: instr.name, editable: false }, - { key: 'linked', value: instr.isLinked, editable: false, type: 'boolean' }, + { key: 'name', value: instr.name, editable: false }, + { key: 'linked', value: instr.isLinked, editable: false, type: 'boolean' }, { key: 'NOT_CHANGED_SINCE', value: instr.notChangedSince ?? '—', editable: false }, ]; } function variationFields(v) { - return [ - { key: 'depends_on', value: v.dependsOn ?? '—', editable: true }, - ]; + return [{ key: 'depends_on', value: v.dependsOn ?? '—', editable: true }]; +} + +function shapeSection(label, shape, onChange) { + if (!shape) return null; + return h('div', { style: 'margin-top:0.5rem' }, [ + h('strong', null, label), + h(ShapeEditor, { shape, onChange }), + ]); } export const PaneFO = { props: ['store'], setup(props) { - // Pending edit held while the linked-instrument modal is shown. - const pendingEdit = ref(null); // { instr, apply: fn } + const pendingEdit = ref(null); // { instr, undo? } function focused() { const fp = props.store.focusPath; return fp.length ? fp[fp.length - 1] : null; } - // Returns a change handler that intercepts the first edit to a linked - // instrument and shows the embed-or-discard modal before applying it. - // `info` may carry { undo } forwarded from ShapeEditor / EnvelopeEditor. - function makeChangeHandler(instr, apply) { + // Intercepts the first edit to a linked instrument: + // shows embed-or-discard modal before committing. `info.undo` + // (forwarded from ShapeEditor/EnvelopeEditor) reverts the mutation on discard. + function makeChangeHandler(instr) { return (info) => { if (instr.isLinked && !instr.isDirty) { - pendingEdit.value = { instr, apply, undo: info?.undo }; + pendingEdit.value = { instr, undo: info?.undo }; } else { - apply(); instr.isDirty = true; props.store.markDirty(); } @@ -69,7 +71,6 @@ export const PaneFO = { instr.name = instr.name.split('/').pop(); instr.isLinked = false; instr.isDirty = true; - pendingEdit.value.apply(); pendingEdit.value = null; props.store.markDirty(); } @@ -79,6 +80,12 @@ export const PaneFO = { pendingEdit.value = null; } + function instrOnChange(instr) { + return instr + ? makeChangeHandler(instr) + : () => props.store.markDirty(); + } + return () => { const node = focused(); const children = []; @@ -87,7 +94,7 @@ export const PaneFO = { const model = props.store.scoreModel; if (!model) return h('div', { class: 'se-fo-pane' }, 'No score loaded'); return h('div', { class: 'se-fo-pane' }, [ - h('h4', { style: 'margin:0 0 0.5rem' }, 'Score'), + h('h4', H4, 'Score'), h(ObjectExtended, { fields: scoreInfoFields(model.info), onChange: ({ key, value }) => { @@ -101,22 +108,18 @@ export const PaneFO = { if (node.type === 'instrument') { children.push( - h('h4', { style: 'margin:0 0 0.5rem' }, `Instrument: ${node.name}`), + h('h4', H4, `Instrument: ${node.name}`), h(ObjectExtended, { fields: instrFields(node), onChange: null }), ); } else if (node.type === 'variation') { - // Find the ancestor instrument for linked-instrument gating. - const instr = props.store.scoreModel?.instruments.find( - i => i.variations?.includes(node) || - i.variations?.some(v => v.subvariations?.includes(node)) + const instr = props.store.scoreModel.instruments.find( + i => i.variations.includes(node) || + i.variations.some(v => v.subvariations.includes(node)) ) ?? null; - - const onChange = instr - ? makeChangeHandler(instr, () => { props.store.markDirty(); }) - : () => props.store.markDirty(); + const onChange = instrOnChange(instr); children.push( - h('h4', { style: 'margin:0 0 0.5rem' }, 'Variation'), + h('h4', H4, 'Variation'), h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => { if (key === 'depends_on') { const old = node.dependsOn; @@ -131,19 +134,16 @@ export const PaneFO = { : null, ); } else if (node.type === 'label_spec') { - const instr = props.store.scoreModel?.instruments.find( - i => i.variations?.some(v => - v.labelSpecs?.includes(node) || - v.subvariations?.some(sv => sv.labelSpecs?.includes(node)) + const instr = props.store.scoreModel.instruments.find( + i => i.variations.some(v => + v.labelSpecs.includes(node) || + v.subvariations.some(sv => sv.labelSpecs.includes(node)) ) ) ?? null; - - const onChange = instr - ? makeChangeHandler(instr, () => { props.store.markDirty(); }) - : () => props.store.markDirty(); + const onChange = instrOnChange(instr); children.push( - h('h4', { style: 'margin:0 0 0.5rem' }, `Label: ${node.label}`), + h('h4', H4, `Label: ${node.label}`), node.basicProperties ? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange }) : null, @@ -151,35 +151,26 @@ export const PaneFO = { } else if (node.type === 'bar') { const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); }; children.push( - h('h4', { style: 'margin:0 0 0.5rem' }, `Bar: ${node.id}`), + h('h4', H4, `Bar: ${node.id}`), h(ObjectExtended, { fields: [ - { key: 'id', value: node.id, editable: false }, - { key: 'beats_per_minute', value: node.tempoLevels ?? '', editable: true, type: 'number' }, - { key: 'stress_pattern', value: stressorToString(node.stressor), editable: true }, + { key: 'id', value: node.id, editable: false }, + { 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); + 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, + shapeSection('Upper stress bound', node.upperStressBound, markBarDirty), + shapeSection('Lower stress bound', node.lowerStressBound, markBarDirty), + shapeSection('Tempo shape', node.tempoShape, markBarDirty), ); } else if (node.type === 'voice') { children.push( - h('h4', { style: 'margin:0 0 0.5rem' }, `Voice: ${node.name}`), + h('h4', H4, `Voice: ${node.name}`), h(ObjectExtended, { fields: [ { key: 'articles', value: node.articles.join(', ') || '—', editable: false }, @@ -190,27 +181,26 @@ export const PaneFO = { }), ); } else if (node.type === 'offset') { - const noteStr = n => `${n.pitch}${n.effLength != null ? ' ' + n.effLength : ''}`; + 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}]`; + const noteItem = (text, i) => h('li', { class: 'se-object-item', key: i }, + h('span', { class: 'se-object-label' }, text)); + children.push( - h('h4', { style: 'margin:0 0 0.5rem' }, `Tick: ${node.tick}`), + h('h4', H4, `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))))), + h('ul', { class: 'se-object-list' }, node.stemNotes.map((n, i) => noteItem(noteStr(n), i))), ]) : 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))))), + h('ul', { class: 'se-object-list' }, node.clusters.map((c, i) => noteItem(clusterStr(c), i))), ]) : null, ); } else { @@ -223,8 +213,8 @@ export const PaneFO = { pendingEdit.value ? h(LinkedInstrumentModal, { instrumentName: pendingEdit.value.instr.name, - onEmbed: () => embedInstrument(pendingEdit.value.instr), - onDiscard: discardEdit, + onEmbed: () => embedInstrument(pendingEdit.value.instr), + onDiscard: discardEdit, }) : null, ]); diff --git a/static/components/PaneSubObjects.js b/static/components/PaneSubObjects.js index 70b0454..ed2b9cc 100644 --- a/static/components/PaneSubObjects.js +++ b/static/components/PaneSubObjects.js @@ -1,7 +1,6 @@ import { h } from 'vue'; import { ObjectShort } from './ObjectShort.js'; -// Shows sub-objects of the currently focused node — variations, bars, voices, etc. export const PaneSubObjects = { props: ['store', 'onFocusFO'], setup(props) { @@ -15,17 +14,17 @@ export const PaneSubObjects = { if (node.type === 'score') { const items = []; if (node.info) - items.push({ kind: 'info', node: node.info, label: node.info.title ?? '(no title)', hasChildren: false }); + items.push({ kind: 'info', node: node.info, label: node.info.title ?? '(no title)', hasChildren: false }); if (node.tuning) - items.push({ kind: 'tuning', node: node.tuning, label: `base ${node.tuning.base ?? '?'}`, hasChildren: false }); + items.push({ kind: 'tuning', node: node.tuning, label: `base ${node.tuning.base ?? '?'}`, hasChildren: false }); for (const a of (node.articles ?? [])) - items.push({ kind: 'article', node: a, label: a.name, hasChildren: false }); + items.push({ kind: 'article', node: a, label: a.name, hasChildren: false }); for (const sv of (node.stageVoices ?? [])) - items.push({ kind: 'stage', node: sv, label: sv.name, hasChildren: false }); + items.push({ kind: 'stage', node: sv, label: sv.name, hasChildren: false }); for (const i of node.instruments) - items.push({ kind: 'instrument', node: i, label: i.name, hasChildren: true }); + items.push({ kind: 'instrument', node: i, label: i.name, hasChildren: true }); for (const b of node.bars) - items.push({ kind: 'bar', node: b, label: b.id, hasChildren: false }); + items.push({ kind: 'bar', node: b, label: b.id, hasChildren: Object.keys(b.voices).length > 0 }); return items; } if (node.type === 'instrument') { @@ -33,12 +32,13 @@ export const PaneSubObjects = { kind: 'variation', node: v, label: `variation ${idx + 1}${v.dependsOn ? ` (${v.dependsOn})` : ''}`, + hasChildren: true, })); } if (node.type === 'variation') { return [ ...node.labelSpecs.map(ls => ({ - kind: 'label_spec', node: ls, label: ls.label ?? '(no label)', + kind: 'label_spec', node: ls, label: ls.label ?? '(no label)', hasChildren: false, })), ...node.subvariations.map((sv, idx) => { const dep = sv.dependsOn; @@ -56,9 +56,7 @@ export const PaneSubObjects = { } if (node.type === 'voice') { return node.offsets.map((o, idx) => ({ - kind: 'offset', node: o, - label: `tick ${o.tick ?? idx}`, - hasChildren: false, + kind: 'offset', node: o, label: `tick ${o.tick ?? idx}`, hasChildren: false, })); } return []; @@ -69,20 +67,19 @@ export const PaneSubObjects = { const items = subItems(node); if (!items.length) return h('div', null, h('em', null, 'No sub-objects')); - return h('div', null, [ + return h('div', null, h('ul', { class: 'se-object-list' }, items.map((item, idx) => h(ObjectShort, { key: idx, - node: item.node, label: item.label, typeTag: item.kind, focused: props.store.focusPath.includes(item.node), - hasChildren: item.hasChildren ?? (item.kind !== 'bar' && item.kind !== 'voice'), - onFocus: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); }, + hasChildren: item.hasChildren, + onFocus: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); }, onDrillDown: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); }, }) - )), - ]); + )) + ); }; }, }; diff --git a/static/exporter.js b/static/exporter.js index 10c0724..aecbec1 100644 --- a/static/exporter.js +++ b/static/exporter.js @@ -171,10 +171,15 @@ function patchInstrumentHeader(text, instruments) { return result.join('\n'); } +export function stressorToString(s) { + if (!s?.groups?.length) return ''; + return s.groups.map(g => g.join(',')).join(';'); +} + function patchBarMeta(doc, bar) { const props = []; - if (bar.stressor?.groups?.length) - props.push(` stress_pattern: ${bar.stressor.groups.map(g => g.join(',')).join(';')}`); + const sp = stressorToString(bar.stressor); + if (sp) props.push(` stress_pattern: ${sp}`); if (bar.tempoLevels != null) props.push(` beats_per_minute: ${bar.tempoLevels}`); const ub = serializeShape(bar.upperStressBound);