Files
avcf/persist.js
T

244 lines
8.4 KiB
JavaScript

// ── Workspace persistence (localStorage) ──────────────────────────────────────
//
// Serialises and deserialises the full workspace state — all blocks in all
// three tab panes, their positions and data, plus SVG connection topology.
//
// Depends on: dom.js ($, $id, svgNamespace, STORAGE_KEY)
// helpers.js (refreshAll, splitValueAndUnit)
// draw.js (drawpath)
//
/**
* Reads the position and type-specific data from a single block element.
* Delegates to the block's saveData() method for type-specific fields.
* @param {HTMLElement} block - a WCBlock or WCProgram child element
* @returns {{ x: number, y: number, ... }}
*/
function saveBlockData(block) {
const data = {
x: parseInt(block.style.left) || 0,
y: parseInt(block.style.top) || 0
};
Object.assign(data, block.saveData());
return data;
}
/**
* Serialises the entire workspace to localStorage under STORAGE_KEY.
* Captures all blocks from algorithm, measurements, and data panes,
* plus every SVG connection path.
*/
function saveWorkspace() {
const prog = $id("main-program");
const state = {
version: 1,
counter: prog.counter,
blocks: { algorithm: [], measurements: [], data: [], procedures: [] },
connections: []
};
for (const block of prog.children) {
state.blocks.algorithm.push({
id: block.id, tag: block.tagName.toLowerCase(),
data: saveBlockData(block)
});
}
for (const block of $id("data-area").children) {
state.blocks.measurements.push({
id: block.id, tag: block.tagName.toLowerCase(),
data: saveBlockData(block)
});
}
for (const block of $id("variable-area").children) {
state.blocks.data.push({
id: block.id, tag: block.tagName.toLowerCase(),
data: saveBlockData(block)
});
}
for (const block of document.querySelectorAll(".lib-pane .procedures-area > [data-block]")) {
state.blocks.procedures.push({
id: block.id, tag: block.tagName.toLowerCase(),
data: saveBlockData(block)
});
}
for (const path of $id("connections").querySelectorAll("path.flow, path.flow-data")) {
state.connections.push({
fromBlock: path.fromElement.id,
fromHook: path.fromHook ? (path.fromHook.id || null) : null,
toBlock: path.toElement.id,
toHook: path.toHook ? (path.toHook.id || null) : null
});
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
/**
* Deep-clones a block <template> by tag name and restores position + data onto it.
* Delegates to the element's restoreData() method for type-specific fields.
* @param {{ id: string, tag: string, data: object }} info - saved block descriptor
* @returns {HTMLElement|null} the restored element, or null for unknown tags
*/
function restoreBlock(info) {
const reg = BLOCK_REGISTRY[info.tag];
if (!reg) return null;
const frag = document.importNode(document.querySelector(reg.template).content, true);
const el = frag.firstElementChild;
el.id = info.id;
el.style.left = info.data.x + "px";
el.style.top = info.data.y + "px";
return el;
}
/**
* Restores a group of block descriptors into a container in a single pass.
* Creates each block, appends it, then calls restoreData on all of them.
* @param {{ id: string, tag: string, data: object }[]} infos - saved block descriptors
* @param {HTMLElement} container - parent element to append blocks into
*/
function restoreGroup(infos, container) {
const els = [];
for (const info of infos) {
const el = restoreBlock(info);
if (el) { container.appendChild(el); els.push({ el, data: info.data || {} }); }
}
for (const { el, data } of els) el.restoreData(data);
}
/**
* Deserialises the workspace from localStorage and reconstructs all blocks,
* connections, and internal state. Existing user blocks are removed first.
*/
function loadWorkspace() {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
let state;
try { state = JSON.parse(raw); } catch { return; }
const prog = $id("main-program");
const dataArea = $id("data-area");
const varArea = $id("variable-area");
const connectionsSvg = $id("connections");
/* ── 1. Tear down existing state ── */
for (const p of connectionsSvg.querySelectorAll("path.flow, path.flow-data")) p.remove();
for (const block of document.querySelectorAll("[data-block]")) {
block.wcForeFlow = [];
block.wcNextFlow = [];
}
for (const block of [...prog.children]) {
if (block.id !== "main-start" && block.id !== "main-end") block.remove();
}
dataArea.innerHTML = "";
varArea.innerHTML = "";
for (const pane of document.querySelectorAll(".lib-pane")) {
const pa = pane.querySelector(".procedures-area");
if (pa) pa.innerHTML = "";
}
document.querySelector(".lib-tab-bar").innerHTML = "";
prog.counter = state.counter;
/* ── 2. Restore measurements (types + units) ── */
restoreGroup(state.blocks.measurements, dataArea);
/* ── 3. Restore procedures — BEFORE algorithm (WCProcCall depends on them) ── */
const libMap = {};
const procEls = [];
for (const info of state.blocks.procedures) {
const lib = (info.data && info.data.library) || "unknown";
if (!libMap[lib]) {
const tab = document.createElement("button");
tab.className = "lib-tab";
tab.dataset.libName = lib;
tab.textContent = lib;
tab.addEventListener("click", () => switchLibTab(lib));
document.querySelector(".lib-tab-bar").appendChild(tab);
const pane = document.createElement("div");
pane.className = "lib-pane";
pane.dataset.libName = lib;
const pa = document.createElement("div");
pa.className = "procedures-area";
pane.appendChild(pa);
$id("pane-procedures").appendChild(pane);
libMap[lib] = pa;
}
const el = restoreBlock(info);
if (el) { libMap[lib].appendChild(el); procEls.push({ el, data: info.data || {} }); }
}
for (const { el, data } of procEls) el.restoreData(data);
/* Activate first library tab */
const firstTab = document.querySelector(".lib-tab");
if (firstTab) switchLibTab(firstTab.dataset.libName);
/* ── 4. Restore algorithm blocks (reposition start/end, recreate others) ── */
const algoEls = [];
for (const info of state.blocks.algorithm) {
if (info.id === "main-start" || info.id === "main-end") {
const el = $id(info.id);
if (el) {
el.style.left = info.data.x + "px";
el.style.top = info.data.y + "px";
}
} else {
const el = restoreBlock(info);
if (el) { prog.appendChild(el); algoEls.push({ el, data: info.data || {} }); }
}
}
for (const { el, data } of algoEls) el.restoreData(data);
/* ── 5. Restore data (datum / variable blocks) ── */
restoreGroup(state.blocks.data, varArea);
/* ── 6. Restore SVG connections ── */
for (const conn of state.connections) {
const fromBlock = $id(conn.fromBlock);
const toBlock = $id(conn.toBlock);
if (!fromBlock || !toBlock) continue;
const fromHook = conn.fromHook
? fromBlock.querySelector("#" + conn.fromHook)
: (fromBlock.querySelector('[data-direction="out"]') || fromBlock.querySelector(".hook"));
const toHook = conn.toHook
? toBlock.querySelector("#" + conn.toHook)
: (toBlock.querySelector('[data-direction="in"]') || toBlock.querySelector(".hook"));
if (!fromHook || !toHook) continue;
const linkType = fromHook.dataset.linkType || "logic";
const path = document.createElementNS(svgNamespace, "path");
path.dataset.linkType = linkType;
path.classList.add(linkType === "data" ? "flow-data" : "flow");
path.toElement = toBlock;
path.fromElement = fromBlock;
path.fromHook = fromHook;
path.toHook = toHook;
toBlock.wcForeFlow.push(path);
fromBlock.wcNextFlow.push(path);
connectionsSvg.appendChild(path);
drawpath(path);
}
/* ── 7. Refresh dropdowns & SI displays (after all blocks + connections exist) ── */
refreshAll();
for (const u of dataArea.querySelectorAll("wc-unit")) u.updateSiDisplay();
for (const d of varArea.querySelectorAll("wc-scalar")) d.updateSiDisplay();
}
/**
* Clears the saved workspace (removes localStorage entry) and reloads the page,
* returning the app to its pristine default state.
*/
function newWorkspace() {
if (!confirm("Clear the entire workspace and start fresh?")) return;
localStorage.removeItem(STORAGE_KEY);
location.reload();
}