diff --git a/web/src/HelpModal.tsx b/web/src/HelpModal.tsx index 5967085..de4bcb3 100644 --- a/web/src/HelpModal.tsx +++ b/web/src/HelpModal.tsx @@ -657,7 +657,7 @@ function SectionLogic() { {[ { type: 'Triggers', desc: 'Button press, threshold crossing, value change, timer/interval, panel loop, and On-open / On-close lifecycle.' }, { type: 'Logic', desc: 'AND gate, If (then/else branches), and Loop (count or while).' }, - { type: 'Actions', desc: 'Write a value to a signal/variable, Delay, Log (debug), and Accumulate / Export-CSV / Clear for in-memory data arrays.' }, + { type: 'Actions', desc: 'Write a value to a signal/variable, Delay, Log (debug), Array push/set/remove/pop/clear on an array local variable, and Export-CSV.' }, { type: 'Dialogs', desc: 'Info and Error pop-ups, and a Set-point prompt that asks the user for a number and writes it to a target.' }, ].map(w => (
diff --git a/web/src/LogicEditor.tsx b/web/src/LogicEditor.tsx index 36b8c42..19c3573 100644 --- a/web/src/LogicEditor.tsx +++ b/web/src/LogicEditor.tsx @@ -73,9 +73,7 @@ const PALETTE: PaletteEntry[] = [ { kind: 'flow.loop', label: 'Loop', params: { mode: 'count', count: '3', cond: '' } }, { kind: 'action.write', label: 'Write', params: { target: '', expr: '' } }, { kind: 'action.delay', label: 'Delay', params: { ms: '500' } }, - { kind: 'action.accumulate', label: 'Accumulate', params: { array: 'data', expr: '' } }, - { kind: 'action.export', label: 'Export CSV', params: { columns: '[{"array":"data","label":""}]', align: 'common', filename: '' } }, - { kind: 'action.clear', label: 'Clear array', params: { array: 'data' } }, + { kind: 'action.export', label: 'Export CSV', params: { columns: '[{"array":"","label":""}]', align: 'common', filename: '' } }, { kind: 'action.log', label: 'Log', params: { expr: '', label: '' } }, { kind: 'action.widget', label: 'Widget control', params: { widget: '', op: 'disable' } }, { kind: 'action.dialog.info', label: 'Info dialog', params: { title: 'Info', message: '' } }, @@ -106,9 +104,7 @@ const KIND_LABEL: Record = { 'flow.loop': 'Loop', 'action.write': 'Write', 'action.delay': 'Delay', - 'action.accumulate': 'Accumulate', 'action.export': 'Export CSV', - 'action.clear': 'Clear array', 'action.log': 'Log', 'action.widget': 'Widget control', 'action.dialog.info': 'Info dialog', @@ -575,16 +571,6 @@ export default function LogicEditor({ graph, onChange, widgets = [], statevars, const byId = new Map(nodes.map(n => [n.id, n])); - // Array names already referenced by accumulate/export/clear nodes, offered as - // autocomplete suggestions so the same array is reused across nodes. - // Suggestions for the array-name autocomplete (export columns, accumulate, - // clear): declared array locals first, then any names already referenced by - // nodes (covers legacy/in-flight names not yet declared). - const arrayNames = Array.from(new Set([ - ...(statevars ?? []).filter(v => v.type === 'array').map(v => v.name), - ...nodes.map(n => n.params.array).filter((a): a is string => !!a), - ])); - // Append a {ds:name} reference into a node's expression field. function insertRef(id: string, field: string, ds: string, sig: string) { const cur = (byId.get(id)?.params[field]) ?? ''; @@ -684,9 +670,6 @@ export default function LogicEditor({ graph, onChange, widgets = [], statevars, {onStateVarsChange && ( )} - - {arrayNames.map(a =>
)} - {selected.kind === 'action.accumulate' && ( - -
- - patchParams(selected.id, { array: (e.target as HTMLInputElement).value })} /> -
- patchParams(selected.id, { expr: v })} - onInsert={(ds2, sig) => insertRef(selected.id, 'expr', ds2, sig)} - allSignals={allSignalOptions()} onOpenSignals={openAllSignals} - hint="Appends this value (with a timestamp) to the array. e.g. {stub:level}, or {sys:time} for the clock." /> -
- )} - {selected.kind === 'action.export' && ( v.type === 'array').map(v => v.name)} align={selected.params.align ?? 'common'} filename={selected.params.filename ?? ''} onColumns={(cols) => patchParams(selected.id, { columns: JSON.stringify(cols) })} @@ -1059,16 +1027,6 @@ export default function LogicEditor({ graph, onChange, widgets = [], statevars, onFilename={(f) => patchParams(selected.id, { filename: f })} /> )} - {selected.kind === 'action.clear' && ( -
- - patchParams(selected.id, { array: (e.target as HTMLInputElement).value })} /> -

Empties the named array (e.g. to start a fresh capture).

-
- )} - {(selected.kind === 'action.array.push' || selected.kind === 'action.array.set' || selected.kind === 'action.array.remove' || selected.kind === 'action.array.pop' || selected.kind === 'action.array.clear') && (() => { @@ -1268,9 +1226,9 @@ function ExprField({ label, value, onChange, onInsert, allSignals, onOpenSignals interface ExportColumn { array: string; label: string; } -// The columns configured on an action.export node. Reads the JSON `columns` -// param; falls back to the legacy single `array` param (so old panels migrate -// transparently the first time the node is edited). +// The columns configured on an action.export node, read from the JSON `columns` +// param. Legacy single-`array` nodes are migrated to this form on load (see +// xml.ts migrateLegacyArrayNode), so only the JSON form is read here. function parseExportColumns(n: LogicNode): ExportColumn[] { const raw = (n.params.columns ?? '').trim(); if (raw) { @@ -1281,8 +1239,7 @@ function parseExportColumns(n: LogicNode): ExportColumn[] { } } catch {} } - const a = (n.params.array ?? '').trim(); - return [{ array: a, label: '' }]; + return [{ array: '', label: '' }]; } const ALIGN_OPTS: Array<{ value: string; label: string; hint: string }> = [ @@ -1291,8 +1248,9 @@ const ALIGN_OPTS: Array<{ value: string; label: string; hint: string }> = [ { value: 'interpolate', label: 'Interpolate', hint: 'Union of all timestamps; missing cells linearly interpolated.' }, ]; -function ExportEditor({ columns, align, filename, onColumns, onAlign, onFilename }: { +function ExportEditor({ columns, arrayLocals, align, filename, onColumns, onAlign, onFilename }: { columns: ExportColumn[]; + arrayLocals: string[]; align: string; filename: string; onColumns: (cols: ExportColumn[]) => void; @@ -1306,12 +1264,15 @@ function ExportEditor({ columns, align, filename, onColumns, onAlign, onFilename return (
- + {columns.map((c, i) => (
- patch(i, { array: (e.target as HTMLInputElement).value })} /> + patch(i, { label: (e.target as HTMLInputElement).value })} /> @@ -1321,6 +1282,9 @@ function ExportEditor({ columns, align, filename, onColumns, onAlign, onFilename
))} + {arrayLocals.length === 0 && ( +

No array locals declared yet — add one in the Local vars panel.

+ )}
{columns.length > 1 && (
@@ -1455,13 +1419,11 @@ function nodeSummary(n: LogicNode): string { : `while ${n.params.cond || '?'}`; case 'action.write': return `${n.params.target || '?'} = ${n.params.expr || ''}`; case 'action.delay': return `wait ${n.params.ms || '0'} ms`; - case 'action.accumulate': return `${n.params.array || '?'} ← ${n.params.expr || ''}`; case 'action.export': { const cols = parseExportColumns(n).filter(c => c.array); const names = cols.map(c => c.label || c.array).join(', '); return `export ${names || '?'} → csv`; } - case 'action.clear': return `clear ${n.params.array || '?'}`; case 'action.array.push': return `push ${n.params.expr || ''} → ${n.params.array || '?'}`; case 'action.array.set': return `${n.params.array || '?'}[${n.params.index || '?'}] = ${n.params.expr || ''}`; case 'action.array.remove': return `remove ${n.params.array || '?'}[${n.params.index || '?'}]`; diff --git a/web/src/lib/logic.ts b/web/src/lib/logic.ts index 52ddd63..844af50 100644 --- a/web/src/lib/logic.ts +++ b/web/src/lib/logic.ts @@ -12,8 +12,8 @@ // flow.loop — repeats the 'body' port (count / while, capped), then 'done'. // action.write— evaluates `expr` and writes the result to `target`. // action.delay— awaits `ms` before continuing. -// action.accumulate / export / clear — collect expression values into a named -// in-memory array and download it as CSV. +// action.array.* — push/set/remove/pop/clear on a named array local variable. +// action.export— download named array local variables as CSV (one col each). // action.log — logs an expression value to the console. // // Expressions reference signals inline as {ds:name} and panel-local vars as @@ -398,7 +398,6 @@ export class LogicEngine { case 'flow.if': collectRefs(node.params.cond ?? '').forEach(want); break; case 'flow.loop': if ((node.params.mode ?? 'count') === 'while') collectRefs(node.params.cond ?? '').forEach(want); break; case 'action.write': - case 'action.accumulate': case 'action.array.push': case 'action.log': collectRefs(node.params.expr ?? '').forEach(want); break; case 'action.array.set': @@ -686,35 +685,12 @@ export class LogicEngine { return; } - // ── Legacy array nodes (aliased to new implementations) ───────────────── - - case 'action.accumulate': { - // Legacy alias → action.array.push (backs into array locals). - const name = (node.params.array ?? '').trim(); - const val = evalValue(node.params.expr ?? '', ctx.resolve); - this.markActive(node.id, val); - if (name) writeLocalState(name, [...curArray(name), val]); - await this.follow(node.id, 'out', ctx); - return; - } - case 'action.export': { if (!this.dryRun) this.exportArrays(this.exportColumns(node), node.params.filename ?? ''); await this.follow(node.id, 'out', ctx); return; } - case 'action.clear': { - // Legacy alias → action.array.clear (backed by array locals). - const name = (node.params.array ?? '').trim(); - if (name) { - const sv = declaredVar(name); - writeLocalState(name, sv ? applySizing([], sv) : []); - } - await this.follow(node.id, 'out', ctx); - return; - } - case 'action.log': { const val = evalExpr(node.params.expr ?? '', ctx.resolve); const label = (node.params.label ?? '').trim(); @@ -783,8 +759,9 @@ export class LogicEngine { } } - // The columns an action.export node should emit. Reads the JSON `columns` - // param (a list of { array, label }); falls back to the legacy single `array`. + // The columns an action.export node should emit, read from the JSON `columns` + // param (a list of { array, label }). Legacy single-`array` nodes are migrated + // to this form on load (see xml.ts migrateLegacyArrayNode). private exportColumns(node: LogicNode): Array<{ array: string; label: string }> { const raw = (node.params.columns ?? '').trim(); if (raw) { @@ -797,8 +774,7 @@ export class LogicEngine { } } catch {} } - const a = (node.params.array ?? '').trim(); - return a ? [{ array: a, label: a }] : []; + return []; } // Dump one or more named array locals to a CSV the browser downloads. @@ -928,20 +904,13 @@ export function ensureArrayDecls(graph: LogicGraph | undefined, vars: StateVar[] } }; for (const n of graph.nodes) { - if ( - n.kind === 'action.accumulate' || - n.kind === 'action.clear' || - n.kind.startsWith('action.array.') - ) { + if (n.kind.startsWith('action.array.')) { need((n.params.array ?? '').trim()); } if (n.kind === 'action.export') { try { (JSON.parse(n.params.columns || '[]') as any[]).forEach(c => need(String(c?.array ?? '').trim())); } catch {} - // Also handle legacy single `array` param. - const a = (n.params.array ?? '').trim(); - if (a) need(a); } } return out; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 8d33932..3197b60 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -115,13 +115,14 @@ export interface StateVar { // action.write — evaluate `expr` and write the result to `target` // ("ds:name" or a bare local var) (params: target, expr). // action.delay — pause `ms` before continuing downstream (params: ms). -// action.accumulate — evaluate `expr` and append the value (with a timestamp) -// to the named in-memory data array (params: array, expr). -// action.export — download one or more named arrays as a CSV file, one -// column per array (params: columns = JSON list of -// {array,label}, align 'common'|'any'|'interpolate', +// action.export — download one or more named array local variables as a +// CSV file, one column per array (params: columns = JSON +// list of {array,label}, align 'common'|'any'|'interpolate', // filename). Legacy single-array nodes use `array`. -// action.clear — empty the named array (params: array). +// action.array.* — push/set/remove/pop/clear on a named array local variable +// (params: array, plus index/expr where relevant). The old +// action.accumulate / action.clear kinds are migrated to +// action.array.push / action.array.clear on load. // action.log — evaluate `expr` and log it to the browser console for // debugging a flow (params: expr, label). // action.widget — drive a panel widget by id (params: widget, op). `op` is @@ -156,9 +157,7 @@ export type LogicNodeKind = | 'flow.loop' | 'action.write' | 'action.delay' - | 'action.accumulate' | 'action.export' - | 'action.clear' | 'action.log' | 'action.widget' | 'action.dialog.info' diff --git a/web/src/lib/xml.ts b/web/src/lib/xml.ts index d655f99..f6e0bc4 100644 --- a/web/src/lib/xml.ts +++ b/web/src/lib/xml.ts @@ -128,6 +128,31 @@ function parseLayout(el: Element): PlotLayout { return { type: 'split', dir, ratio: isNaN(ratio) ? 0.5 : ratio, a, b }; } +// Rewrite a legacy array node kind to its current equivalent, mutating `params` +// in place. The old action.accumulate / action.clear kinds (and the legacy +// single-`array` form of action.export) predate the typed array local-variable +// nodes; old panels are migrated transparently on load so the editor only ever +// sees the current kinds. +function migrateLegacyArrayNode( + kind: string, + params: Record, + setKind: (k: LogicGraph['nodes'][number]['kind']) => void, +): void { + if (kind === 'action.accumulate') { + setKind('action.array.push'); // same params: { array, expr } + } else if (kind === 'action.clear') { + setKind('action.array.clear'); // same params: { array } + } else if (kind === 'action.export') { + // Legacy single-array export had only an `array` param; fold it into the + // current JSON `columns` form so the editor never sees the legacy field. + const a = (params.array ?? '').trim(); + if (a && !(params.columns ?? '').trim()) { + params.columns = JSON.stringify([{ array: a, label: '' }]); + } + delete params.array; + } +} + /** Parse a element into a LogicGraph (nodes + wires). */ function parseLogic(logicEl: Element): LogicGraph { const nodes: LogicGraph['nodes'] = []; @@ -160,9 +185,11 @@ function parseLogic(logicEl: Element): LogicGraph { const value = child.getAttribute('value'); if (key !== null && value !== null) params[key] = value; } + let kind = (el.getAttribute('kind') ?? 'action.write') as LogicGraph['nodes'][number]['kind']; + migrateLegacyArrayNode(kind, params, (k) => { kind = k; }); nodes.push({ id, - kind: (el.getAttribute('kind') ?? 'action.write') as LogicGraph['nodes'][number]['kind'], + kind, x: parseFloat(el.getAttribute('x') ?? '0'), y: parseFloat(el.getAttribute('y') ?? '0'), params,