244 lines
8.4 KiB
JavaScript
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();
|
|
}
|