287 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 unknownPropFields(node) {
return Object.entries(node.unknownProps ?? {})
.map(([key, value]) => ({ key, value: String(value), editable: false }));
}
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 === 'article') {
const markDirty = () => props.store.markDirty();
children.push(
h('h4', H4, `Article: ${node.name}`),
h('ul', { class: 'se-article-props', style: 'list-style:none;padding:0;margin:0' },
node.properties.map((p, idx) => {
const isOverwrite = p.scope === 'overwrites';
const flip = () => { p.scope = isOverwrite ? 'defaults' : 'overwrites'; markDirty(); };
return h('li', { key: idx, style: 'display:flex;gap:0.5rem;align-items:center;padding:0.25rem 0' }, [
h('span', { style: 'flex:1' }, `${p.name} = ${p.value}`),
h('button', {
class: 'se-toggle',
role: 'switch',
'aria-checked': String(isOverwrite),
onClick: flip,
}),
h('span', { class: 'se-toggle-label active' }, isOverwrite ? 'overwrite' : 'default'),
]);
})
),
);
} 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.map(m => m.label).join(', ') || '—', editable: false },
{ key: 'offsets', value: String(node.offsets.length), editable: false },
],
onChange: null,
}),
);
} else if (node.type === 'offset') {
const snLabel = sn => `${sn.pitch}${sn.effLength != null ? ' /' + sn.effLength : ''}${sn.clauses.length ? ' ×' + sn.clauses.length : ''}`;
children.push(
h('h4', H4, `Tick: ${node.tick}`),
h(ObjectExtended, {
fields: [
{ key: 'tick', value: node.tick, editable: false },
{ key: 'stem notes', value: node.stemNotes.map(snLabel).join(', ') || '—', editable: false },
node.motifs?.length
? { key: 'motifs', value: node.motifs.map(m => m.chord ? `${m.label}(${m.chord})` : m.label).join(', '), editable: false }
: null,
...unknownPropFields(node),
].filter(Boolean),
onChange: null,
}),
);
} else if (node.type === 'motif') {
const pitchLabel = sn => (Number.isInteger(sn.pitch) ? `(ref${sn.pitch !== 0 ? sn.pitch : ''})` : String(sn.pitch))
+ (sn.clauses.length ? ' ×' + sn.clauses.length : '');
children.push(
h('h4', H4, `Motif: ${node.label}`),
h(ObjectExtended, {
fields: [
{ key: 'label', value: node.label, editable: false },
{ key: 'static', value: node.isStatic, editable: false, type: 'boolean' },
{ key: 'stem notes', value: node.stemNotes.map(pitchLabel).join(', ') || '—', editable: false },
...unknownPropFields(node),
],
onChange: null,
}),
);
} 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(
h('h4', H4, `Stem note: ${node.pitch}`),
h(ObjectExtended, {
fields: [
{ key: 'pitch', value: node.pitch, editable: true },
{ key: 'length', value: node.length ?? '', editable: true },
{ key: 'weight', value: node.weight != null ? String(node.weight) : '', editable: true, type: 'number' },
{ key: 'adj_stress', value: node.adjStress != null ? String(node.adjStress) : '', editable: true, type: 'number' },
{ key: 'chain', value: node.chainText, editable: true },
{ key: 'clauses', value: String(node.clauses.length), editable: false },
...unknownPropFields(node),
],
onChange: ({ key, value }) => {
if (key === 'pitch') node.pitch = value;
if (key === 'length') node.length = value || null;
if (key === 'weight') node.weight = value === '' ? null : Number(value);
if (key === 'adj_stress') node.adjStress = value === '' ? null : Number(value);
if (key === 'chain') node.chainText = value;
markDirty();
},
}),
);
} 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,
]);
};
},
};