Major changes: logic add to panel, local variables, panel histor, users management...
This commit is contained in:
+144
-3
@@ -1,4 +1,4 @@
|
||||
import type { Interface, Widget, SignalRef } from './types';
|
||||
import type { Interface, Widget, SignalRef, PlotLayout, StateVar, LogicGraph } from './types';
|
||||
|
||||
/** Escape a string for use as an XML attribute value. */
|
||||
function xmlEsc(s: string): string {
|
||||
@@ -9,14 +9,67 @@ function xmlEsc(s: string): string {
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/** Serialize a panel-logic flow graph to indented XML lines. */
|
||||
function serializeLogic(graph: LogicGraph, lines: string[]): void {
|
||||
lines.push(` <logic>`);
|
||||
for (const node of graph.nodes) {
|
||||
lines.push(
|
||||
` <node id="${xmlEsc(node.id)}" kind="${xmlEsc(node.kind)}" ` +
|
||||
`x="${node.x}" y="${node.y}">`
|
||||
);
|
||||
for (const [key, value] of Object.entries(node.params)) {
|
||||
lines.push(` <param key="${xmlEsc(key)}" value="${xmlEsc(value)}"/>`);
|
||||
}
|
||||
lines.push(` </node>`);
|
||||
}
|
||||
for (const wire of graph.wires) {
|
||||
const portAttr = wire.fromPort && wire.fromPort !== 'out'
|
||||
? ` port="${xmlEsc(wire.fromPort)}"` : '';
|
||||
lines.push(` <wire from="${xmlEsc(wire.from)}"${portAttr} to="${xmlEsc(wire.to)}"/>`);
|
||||
}
|
||||
lines.push(` </logic>`);
|
||||
}
|
||||
|
||||
/** Serialize a PlotLayout subtree to indented XML lines. */
|
||||
function serializeLayout(node: PlotLayout, indent: string, lines: string[]): void {
|
||||
if (node.type === 'leaf') {
|
||||
lines.push(`${indent}<leaf widget="${xmlEsc(node.widget)}"/>`);
|
||||
return;
|
||||
}
|
||||
lines.push(`${indent}<split dir="${node.dir}" ratio="${node.ratio}">`);
|
||||
serializeLayout(node.a, indent + ' ', lines);
|
||||
serializeLayout(node.b, indent + ' ', lines);
|
||||
lines.push(`${indent}</split>`);
|
||||
}
|
||||
|
||||
/** Serialize an Interface object to an XML string. */
|
||||
export function serializeInterface(iface: Interface): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
||||
const kindAttr = iface.kind === 'plot' ? ` kind="plot"` : '';
|
||||
lines.push(
|
||||
`<interface id="${xmlEsc(iface.id)}" name="${xmlEsc(iface.name)}" ` +
|
||||
`version="${iface.version}" width="${iface.w}" height="${iface.h}">`
|
||||
`version="${iface.version}" width="${iface.w}" height="${iface.h}"${kindAttr}>`
|
||||
);
|
||||
if (iface.kind === 'plot' && iface.layout) {
|
||||
lines.push(` <layout>`);
|
||||
serializeLayout(iface.layout, ' ', lines);
|
||||
lines.push(` </layout>`);
|
||||
}
|
||||
for (const sv of iface.statevars ?? []) {
|
||||
const attrs = [
|
||||
`name="${xmlEsc(sv.name)}"`,
|
||||
`type="${xmlEsc(sv.type ?? 'number')}"`,
|
||||
`initial="${xmlEsc(sv.initial ?? '')}"`,
|
||||
];
|
||||
if (sv.unit) attrs.push(`unit="${xmlEsc(sv.unit)}"`);
|
||||
if (sv.low !== undefined) attrs.push(`low="${sv.low}"`);
|
||||
if (sv.high !== undefined) attrs.push(`high="${sv.high}"`);
|
||||
lines.push(` <statevar ${attrs.join(' ')}/>`);
|
||||
}
|
||||
if (iface.logic && (iface.logic.nodes.length > 0 || iface.logic.wires.length > 0)) {
|
||||
serializeLogic(iface.logic, lines);
|
||||
}
|
||||
for (const w of iface.widgets) {
|
||||
lines.push(
|
||||
` <widget id="${xmlEsc(w.id)}" type="${xmlEsc(w.type)}" ` +
|
||||
@@ -46,6 +99,56 @@ export function serializeInterface(iface: Interface): string {
|
||||
* </widget>
|
||||
* </interface>
|
||||
*/
|
||||
/** Parse a <split>/<leaf> element into a PlotLayout node. */
|
||||
function parseLayout(el: Element): PlotLayout {
|
||||
if (el.tagName === 'leaf') {
|
||||
return { type: 'leaf', widget: el.getAttribute('widget') ?? '' };
|
||||
}
|
||||
// split
|
||||
const dir = el.getAttribute('dir') === 'v' ? 'v' : 'h';
|
||||
const ratio = parseFloat(el.getAttribute('ratio') ?? '0.5');
|
||||
const children = Array.from(el.children).filter(
|
||||
c => c.tagName === 'split' || c.tagName === 'leaf'
|
||||
);
|
||||
const a = children[0] ? parseLayout(children[0]) : { type: 'leaf', widget: '' } as PlotLayout;
|
||||
const b = children[1] ? parseLayout(children[1]) : { type: 'leaf', widget: '' } as PlotLayout;
|
||||
return { type: 'split', dir, ratio: isNaN(ratio) ? 0.5 : ratio, a, b };
|
||||
}
|
||||
|
||||
/** Parse a <logic> element into a LogicGraph (nodes + wires). */
|
||||
function parseLogic(logicEl: Element): LogicGraph {
|
||||
const nodes: LogicGraph['nodes'] = [];
|
||||
const wires: LogicGraph['wires'] = [];
|
||||
|
||||
for (const el of Array.from(logicEl.children)) {
|
||||
if (el.tagName === 'node') {
|
||||
const id = el.getAttribute('id') ?? '';
|
||||
if (!id) continue;
|
||||
const params: Record<string, string> = {};
|
||||
for (const child of Array.from(el.children)) {
|
||||
if (child.tagName !== 'param') continue;
|
||||
const key = child.getAttribute('key');
|
||||
const value = child.getAttribute('value');
|
||||
if (key !== null && value !== null) params[key] = value;
|
||||
}
|
||||
nodes.push({
|
||||
id,
|
||||
kind: (el.getAttribute('kind') ?? 'action.write') as LogicGraph['nodes'][number]['kind'],
|
||||
x: parseFloat(el.getAttribute('x') ?? '0'),
|
||||
y: parseFloat(el.getAttribute('y') ?? '0'),
|
||||
params,
|
||||
});
|
||||
} else if (el.tagName === 'wire') {
|
||||
const from = el.getAttribute('from');
|
||||
const to = el.getAttribute('to');
|
||||
const port = el.getAttribute('port');
|
||||
if (from && to) wires.push({ from, to, ...(port ? { fromPort: port } : {}) });
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, wires };
|
||||
}
|
||||
|
||||
export function parseInterface(xml: string): Interface {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, 'application/xml');
|
||||
@@ -66,6 +169,38 @@ export function parseInterface(xml: string): Interface {
|
||||
const version = parseInt(root.getAttribute('version') ?? '1', 10);
|
||||
const w = parseInt(root.getAttribute('width') ?? '1200', 10);
|
||||
const h = parseInt(root.getAttribute('height') ?? '800', 10);
|
||||
const kind = root.getAttribute('kind') === 'plot' ? 'plot' : undefined;
|
||||
|
||||
let layout: PlotLayout | undefined;
|
||||
if (kind === 'plot') {
|
||||
const layoutEl = Array.from(root.children).find(el => el.tagName === 'layout');
|
||||
const rootNode = layoutEl
|
||||
? Array.from(layoutEl.children).find(el => el.tagName === 'split' || el.tagName === 'leaf')
|
||||
: undefined;
|
||||
if (rootNode) layout = parseLayout(rootNode);
|
||||
}
|
||||
|
||||
const statevars: StateVar[] = [];
|
||||
for (const el of Array.from(root.children)) {
|
||||
if (el.tagName !== 'statevar') continue;
|
||||
const name = el.getAttribute('name') ?? '';
|
||||
if (!name) continue;
|
||||
const type = el.getAttribute('type');
|
||||
const low = el.getAttribute('low');
|
||||
const high = el.getAttribute('high');
|
||||
const unit = el.getAttribute('unit');
|
||||
statevars.push({
|
||||
name,
|
||||
type: type === 'bool' || type === 'string' ? type : 'number',
|
||||
initial: el.getAttribute('initial') ?? '',
|
||||
...(unit ? { unit } : {}),
|
||||
...(low !== null ? { low: parseFloat(low) } : {}),
|
||||
...(high !== null ? { high: parseFloat(high) } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
const logicEl = Array.from(root.children).find(el => el.tagName === 'logic');
|
||||
const logic = logicEl ? parseLogic(logicEl) : null;
|
||||
|
||||
const widgets: Widget[] = [];
|
||||
|
||||
@@ -100,5 +235,11 @@ export function parseInterface(xml: string): Interface {
|
||||
widgets.push({ id, type, x, y, w, h, signals, options });
|
||||
}
|
||||
|
||||
return { id: ifaceId, name, version, w, h, widgets };
|
||||
return {
|
||||
id: ifaceId, name, version, w, h, widgets,
|
||||
...(kind ? { kind } : {}),
|
||||
...(layout ? { layout } : {}),
|
||||
...(statevars.length > 0 ? { statevars } : {}),
|
||||
...(logic && (logic.nodes.length > 0 || logic.wires.length > 0) ? { logic } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user