import { h, ref } from 'vue'; 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'; const H4 = { style: 'margin:0 0 0.5rem' }; 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) { return [ { key: 'title', value: info?.title ?? '', editable: true }, { key: 'composer', value: info?.composer ?? '', editable: true }, { key: 'source', value: info?.source ?? '', editable: true }, { key: 'encrypter', value: info?.encrypter ?? '', editable: true }, ]; } function instrFields(instr) { return [ { 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 }]; } 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) { const pendingEdit = ref(null); // { instr, undo? } function focused() { const fp = props.store.focusPath; return fp.length ? fp[fp.length - 1] : null; } // 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, undo: info?.undo }; } else { instr.isDirty = true; props.store.markDirty(); } }; } function embedInstrument(instr) { instr.name = instr.name.split('/').pop(); instr.isLinked = false; instr.isDirty = true; pendingEdit.value = null; props.store.markDirty(); } function discardEdit() { pendingEdit.value?.undo?.(); pendingEdit.value = null; } function instrOnChange(instr) { return instr ? makeChangeHandler(instr) : () => props.store.markDirty(); } return () => { const node = focused(); const children = []; if (!node || node.type === 'score') { 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', H4, 'Score'), h(ObjectExtended, { fields: scoreInfoFields(model.info), onChange: ({ key, value }) => { if (!model.info) model.info = {}; model.info[key] = value; props.store.markDirty(); }, }), ]); } if (node.type === 'instrument') { children.push( h('h4', H4, `Instrument: ${node.name}`), h(ObjectExtended, { fields: instrFields(node), onChange: null }), ); } else if (node.type === 'variation') { const instr = props.store.scoreModel.instruments.find( i => i.variations.includes(node) || i.variations.some(v => v.subvariations.includes(node)) ) ?? null; const onChange = instrOnChange(instr); children.push( h('h4', H4, 'Variation'), h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => { if (key === 'depends_on') { const old = node.dependsOn; node.dependsOn = value; onChange({ undo: () => { node.dependsOn = old; } }); } else { onChange({}); } }}), node.basicProperties ? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange }) : 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)) ) ) ?? null; const onChange = instrOnChange(instr); children.push( h('h4', H4, `Label: ${node.label}`), node.basicProperties ? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange }) : null, ); } else if (node.type === 'bar') { const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); }; children.push( 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 }, ], onChange: ({ key, value }) => { if (key === 'beats_per_minute') node.tempoLevels = isNaN(value) ? null : value; if (key === 'stress_pattern') node.stressor = parseStressor(value); markBarDirty(); }, }), 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', H4, `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}]`; const noteItem = (text, i) => h('li', { class: 'se-object-item', key: i }, h('span', { class: 'se-object-label' }, text)); children.push( 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) => 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) => noteItem(clusterStr(c), i))), ]) : null, ); } else { children.push(h('pre', { style: 'font-size:0.75rem;white-space:pre-wrap' }, JSON.stringify(node, null, 2))); } return h('div', { class: 'se-fo-pane' }, [ ...children, pendingEdit.value ? h(LinkedInstrumentModal, { instrumentName: pendingEdit.value.instr.name, onEmbed: () => embedInstrument(pendingEdit.value.instr), onDiscard: discardEdit, }) : null, ]); }; }, };