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) { function buildVariation(node) {
const v = { const v = {
type: 'variation', type: 'variation',
dependsOn: node.props.depends_on ?? null, dependsOn: node.props.depends_on ?? node.props.for_value ?? null,
basicProperties: null, basicProperties: null,
labelSpecs: [], labelSpecs: [],
subvariations: [], subvariations: [],

View File

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

View File

@ -39,11 +39,11 @@ export const PaneFO = {
// Returns a change handler that intercepts the first edit to a linked // Returns a change handler that intercepts the first edit to a linked
// instrument and shows the embed-or-discard modal before applying it. // instrument and shows the embed-or-discard modal before applying it.
// `info` may carry { undo } forwarded from ShapeEditor / EnvelopeEditor.
function makeChangeHandler(instr, apply) { function makeChangeHandler(instr, apply) {
return () => { return (info) => {
if (instr.isLinked && !instr.isDirty) { if (instr.isLinked && !instr.isDirty) {
// Stash the apply callback and show the modal. pendingEdit.value = { instr, apply, undo: info?.undo };
pendingEdit.value = { instr, apply };
} else { } else {
apply(); apply();
instr.isDirty = true; instr.isDirty = true;
@ -62,6 +62,7 @@ export const PaneFO = {
} }
function discardEdit() { function discardEdit() {
pendingEdit.value?.undo?.();
pendingEdit.value = null; pendingEdit.value = null;
} }
@ -104,8 +105,13 @@ export const PaneFO = {
children.push( children.push(
h('h4', { style: 'margin:0 0 0.5rem' }, 'Variation'), h('h4', { style: 'margin:0 0 0.5rem' }, 'Variation'),
h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => { h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => {
if (key === 'depends_on') node.dependsOn = value; if (key === 'depends_on') {
onChange(); const old = node.dependsOn;
node.dependsOn = value;
onChange({ undo: () => { node.dependsOn = old; } });
} else {
onChange({});
}
}}), }}),
node.basicProperties node.basicProperties
? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange }) ? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange })

View File

@ -1,7 +1,8 @@
import { h } from 'vue'; import { h } from 'vue';
// Renders a shape's coord table with cascade-shift and an SVG preview. // 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 = { export const ShapeEditor = {
props: ['shape', 'onChange'], props: ['shape', 'onChange'],
@ -14,27 +15,31 @@ export const ShapeEditor = {
const delta = num - old; const delta = num - old;
coord[field] = num; coord[field] = num;
// cascade-shift: if x increased past next coord, shift all following const shifted = [];
if (field === 'x' && delta > 0) { if (field === 'x' && delta > 0) {
for (let j = i + 1; j < props.shape.coords.length; j++) { for (let j = i + 1; j < props.shape.coords.length; j++) {
if (props.shape.coords[j].x <= num) { if (props.shape.coords[j].x <= num) {
shifted.push({ j, ox: props.shape.coords[j].x });
props.shape.coords[j].x += delta; props.shape.coords[j].x += delta;
} else break; } else break;
} }
} }
props.onChange?.(); props.onChange?.({ undo: () => {
coord[field] = old;
for (const { j, ox } of shifted) props.shape.coords[j].x = ox;
}});
} }
function addCoord() { function addCoord() {
const coords = props.shape.coords; const coords = props.shape.coords;
const lastX = coords.length ? coords[coords.length - 1].x + 1 : 1; const lastX = coords.length ? coords[coords.length - 1].x + 1 : 1;
coords.push({ x: lastX, y: 0, z: 1, isSharp: false }); coords.push({ x: lastX, y: 0, z: 1, isSharp: false });
props.onChange?.(); props.onChange?.({ undo: () => coords.pop() });
} }
function removeCoord(i) { function removeCoord(i) {
props.shape.coords.splice(i, 1); const removed = props.shape.coords.splice(i, 1)[0];
props.onChange?.(); props.onChange?.({ undo: () => props.shape.coords.splice(i, 0, removed) });
} }
function renderSvg() { function renderSvg() {
@ -62,12 +67,7 @@ export const ShapeEditor = {
viewBox: `0 0 ${W} ${H}`, viewBox: `0 0 ${W} ${H}`,
preserveAspectRatio: 'none', preserveAspectRatio: 'none',
}, [ }, [
h('polyline', { h('polyline', { points, fill: 'none', stroke: '#2a6aaa', 'stroke-width': '1.5' }),
points,
fill: 'none',
stroke: '#2a6aaa',
'stroke-width': '1.5',
}),
...coords.map(c => { ...coords.map(c => {
const [sx, sy] = toSvg(c); const [sx, sy] = toSvg(c);
return h('circle', { cx: sx, cy: sy, r: 2.5, fill: '#6aacff' }); return h('circle', { cx: sx, cy: sy, r: 2.5, fill: '#6aacff' });
@ -101,7 +101,11 @@ export const ShapeEditor = {
})), })),
h('td', null, h('input', { h('td', null, h('input', {
type: 'checkbox', checked: !!coord.isSharp, 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) }, '✕')), 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'; import { fetchStatus } from '../api.js';
export const StatusPoller = { export const StatusPoller = {
props: ['store'], props: ['store'],
setup(props) { setup(props) {
const timer = ref(null); 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() { async function poll() {
try { try {
const status = await fetchStatus(props.store.credentials); const status = await fetchStatus(props.store.credentials);
if (status.eta) eta.value = status.eta;
props.store.synthesisStatus = status; props.store.synthesisStatus = status;
if (status.frozen) { if (status.frozen) return;
// done — stop polling
return;
}
} catch (_) { } catch (_) {
// transient error — keep polling // transient — keep polling
} }
timer.value = setTimeout(poll, nextInterval(props.store.synthesisStatus)); timer.value = setTimeout(poll, 2000);
} }
onMounted(() => { poll(); }); onMounted(() => { poll(); });
@ -39,10 +24,16 @@ export const StatusPoller = {
const s = props.store.synthesisStatus; const s = props.store.synthesisStatus;
if (!s) return h('div', { class: 'se-status' }, 'Polling…'); if (!s) return h('div', { class: 'se-status' }, 'Polling…');
const pct = s.progress ?? 0; const total = s.notes_in_total ?? 0;
const label = s.frozen const done = s.currently_rendered_notes ?? 0;
? (s.error ? `Error: ${s.error}` : 'Done') const pct = total > 0 ? Math.min(100, Math.round(done / total * 100)) : 0;
: `${s.state ?? 'Running'} ${pct}%`;
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' }, [ return h('div', { class: 'se-status' }, [
h('span', null, label), h('span', null, label),