Fix subvariation display, progress bar, and linked-instrument discard

- ast-parser: read for_value= prop for sub-variations (was always null,
  causing fallback to positional "subvariation N" label)
- StatusPoller: calculate progress from currently_rendered_notes /
  notes_in_total; show remaining_time string from status.json
- ShapeEditor/EnvelopeEditor: pass { undo } through onChange so callers
  can revert in-place mutations
- PaneFO: makeChangeHandler stashes undo from info arg; discardEdit
  calls undo() before clearing pendingEdit; depends_on handler provides
  its own undo for the inline node.dependsOn mutation
This commit is contained in:
c0dev0id 2026-06-23 21:28:53 +02:00
parent 2060f109b6
commit 4c392927cf
5 changed files with 48 additions and 50 deletions

View File

@ -212,7 +212,7 @@ function buildInstrument(node) {
function buildVariation(node) {
const v = {
type: 'variation',
dependsOn: node.props.depends_on ?? null,
dependsOn: node.props.depends_on ?? node.props.for_value ?? null,
basicProperties: null,
labelSpecs: [],
subvariations: [],

View File

@ -24,12 +24,9 @@ export const EnvelopeEditor = {
setup(props) {
function toggle(section) {
const bp = props.basicProperties;
if (bp[section]) {
bp[section] = null;
} else {
bp[section] = defaultShape(section);
}
props.onChange?.();
const old = bp[section];
bp[section] = old ? null : defaultShape(section);
props.onChange?.({ undo: () => { bp[section] = old; } });
}
return () => {
@ -57,7 +54,7 @@ export const EnvelopeEditor = {
? h('div', { class: disabled ? 'se-envelope-disabled' : null }, [
h(ShapeEditor, {
shape: bp[key],
onChange: props.onChange,
onChange: info => props.onChange?.(info),
}),
])
: null,

View File

@ -39,11 +39,11 @@ export const PaneFO = {
// 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) {
return () => {
return (info) => {
if (instr.isLinked && !instr.isDirty) {
// Stash the apply callback and show the modal.
pendingEdit.value = { instr, apply };
pendingEdit.value = { instr, apply, undo: info?.undo };
} else {
apply();
instr.isDirty = true;
@ -62,6 +62,7 @@ export const PaneFO = {
}
function discardEdit() {
pendingEdit.value?.undo?.();
pendingEdit.value = null;
}
@ -104,8 +105,13 @@ export const PaneFO = {
children.push(
h('h4', { style: 'margin:0 0 0.5rem' }, 'Variation'),
h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => {
if (key === 'depends_on') node.dependsOn = value;
onChange();
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 })

View File

@ -1,7 +1,8 @@
import { h } from 'vue';
// Renders a shape's coord table with cascade-shift and an SVG preview.
// `shape` is mutated in place; `onChange` called after each mutation.
// `shape` is mutated in place; `onChange` called after each mutation with
// { undo } so callers can revert if needed.
export const ShapeEditor = {
props: ['shape', 'onChange'],
@ -14,27 +15,31 @@ export const ShapeEditor = {
const delta = num - old;
coord[field] = num;
// cascade-shift: if x increased past next coord, shift all following
const shifted = [];
if (field === 'x' && delta > 0) {
for (let j = i + 1; j < props.shape.coords.length; j++) {
if (props.shape.coords[j].x <= num) {
shifted.push({ j, ox: props.shape.coords[j].x });
props.shape.coords[j].x += delta;
} else break;
}
}
props.onChange?.();
props.onChange?.({ undo: () => {
coord[field] = old;
for (const { j, ox } of shifted) props.shape.coords[j].x = ox;
}});
}
function addCoord() {
const coords = props.shape.coords;
const lastX = coords.length ? coords[coords.length - 1].x + 1 : 1;
coords.push({ x: lastX, y: 0, z: 1, isSharp: false });
props.onChange?.();
props.onChange?.({ undo: () => coords.pop() });
}
function removeCoord(i) {
props.shape.coords.splice(i, 1);
props.onChange?.();
const removed = props.shape.coords.splice(i, 1)[0];
props.onChange?.({ undo: () => props.shape.coords.splice(i, 0, removed) });
}
function renderSvg() {
@ -62,12 +67,7 @@ export const ShapeEditor = {
viewBox: `0 0 ${W} ${H}`,
preserveAspectRatio: 'none',
}, [
h('polyline', {
points,
fill: 'none',
stroke: '#2a6aaa',
'stroke-width': '1.5',
}),
h('polyline', { points, fill: 'none', stroke: '#2a6aaa', 'stroke-width': '1.5' }),
...coords.map(c => {
const [sx, sy] = toSvg(c);
return h('circle', { cx: sx, cy: sy, r: 2.5, fill: '#6aacff' });
@ -101,7 +101,11 @@ export const ShapeEditor = {
})),
h('td', null, h('input', {
type: 'checkbox', checked: !!coord.isSharp,
onChange: e => { coord.isSharp = e.target.checked; props.onChange?.(); },
onChange: e => {
const old = coord.isSharp;
coord.isSharp = e.target.checked;
props.onChange?.({ undo: () => { coord.isSharp = old; } });
},
})),
h('td', null, h('button', { onClick: () => removeCoord(i) }, '✕')),
])

View File

@ -1,35 +1,20 @@
import { h, ref, onMounted, onUnmounted } from 'vue';
import { h, onMounted, onUnmounted, ref } from 'vue';
import { fetchStatus } from '../api.js';
export const StatusPoller = {
props: ['store'],
setup(props) {
const timer = ref(null);
const eta = ref(null);
function nextInterval(status) {
if (!status) return 2000;
const pct = status.progress ?? 0;
// At 020%: poll at centile-of-ETA intervals
if (eta.value && pct > 0 && pct <= 20) {
return Math.max(500, (eta.value * 10) | 0);
}
return 2000;
}
async function poll() {
try {
const status = await fetchStatus(props.store.credentials);
if (status.eta) eta.value = status.eta;
props.store.synthesisStatus = status;
if (status.frozen) {
// done — stop polling
return;
}
if (status.frozen) return;
} catch (_) {
// transient error — keep polling
// transient — keep polling
}
timer.value = setTimeout(poll, nextInterval(props.store.synthesisStatus));
timer.value = setTimeout(poll, 2000);
}
onMounted(() => { poll(); });
@ -39,10 +24,16 @@ export const StatusPoller = {
const s = props.store.synthesisStatus;
if (!s) return h('div', { class: 'se-status' }, 'Polling…');
const pct = s.progress ?? 0;
const label = s.frozen
? (s.error ? `Error: ${s.error}` : 'Done')
: `${s.state ?? 'Running'} ${pct}%`;
const total = s.notes_in_total ?? 0;
const done = s.currently_rendered_notes ?? 0;
const pct = total > 0 ? Math.min(100, Math.round(done / total * 100)) : 0;
let label;
if (s.frozen) {
label = s.errors ? `Error: ${s.errors}` : 'Done';
} else {
label = s.remaining_time ?? `Synthesizing… ${pct}%`;
}
return h('div', { class: 'se-status' }, [
h('span', null, label),