Wiring view: every machine and what connects it (Companion, MIDI, Dante, watchdog, web, Art-Net), hide types or single links, scripts per computer
RESI LIVE NOT REC follows the Resi auto-record service (custom autorec_status, not during its REC hold; stale heartbeat = no verdict); INGEST watches AUTO REC (running, heartbeat, Resi reachable), hold restarts it. Probes list each computer's scripts by file name only. HyperDeck addresses read from bonjourHost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+233
-2
@@ -233,7 +233,7 @@
|
||||
search.onblur = () => setTimeout(() => { results.hidden = true; }, 200);
|
||||
|
||||
// ---- routing ---------------------------------------------------------------
|
||||
const screens = { overview, keys, computers, settings, logs, history, help, review };
|
||||
const screens = { overview, keys, computers, wiring, settings, logs, history, help, review };
|
||||
let timer = null;
|
||||
async function route() {
|
||||
let [name, ...rest] = (location.hash || "#overview").slice(1).split("/");
|
||||
@@ -247,7 +247,7 @@
|
||||
await fn(rest);
|
||||
wireInputs(main);
|
||||
wireOnOff(main);
|
||||
const refresh = name === "overview" || (name === "computers" && !["gear", "setup"].includes(rest[0]));
|
||||
const refresh = name === "overview" || name === "wiring" || (name === "computers" && !["gear", "setup"].includes(rest[0]));
|
||||
if (refresh) timer = setInterval(() => fn(rest, true).then(() => wireInputs(main)).catch(() => {}), 5000);
|
||||
} catch (e) { main.innerHTML = `<p class="notice">Could not load this screen: ${esc(e.message)}</p>`; }
|
||||
}
|
||||
@@ -630,6 +630,237 @@
|
||||
${canRunNow(r.fix) && editing() ? `<button class="small" data-run="${esc(r.id)}|${esc(r.fix)}|0">Run the fix</button>` : ""}<details><summary>more</summary><dl class="kv"><dt>id</dt><dd class="mono">${esc(r.id)}</dd>${r.watch && r.watch.length ? `<dt>flashes while down</dt><dd>${esc(r.watch.join(", "))}</dd>` : ""}</dl></details>${editing() ? `<p style="margin-top:12px"><button class="small danger" id="delrkey">Delete key</button></p>` : ""}</div>`;
|
||||
}
|
||||
|
||||
// ---- Wiring ------------------------------------------------------------------
|
||||
// Every computer and device the panel knows and what connects them, like a wiring diagram (2026-10-27). Drag a
|
||||
// machine to arrange it; click one to see its scripts and connections; hide connection types or single
|
||||
// connections. Positions and what is hidden are kept in this browser only.
|
||||
const W_W = 2000, W_H = 1300;
|
||||
const wire = { data: null, pos: null, hidden: null, types: null, sel: null, focus: false, problems: false, drag: null, view: null };
|
||||
const wireRead = (k, d) => { try { return JSON.parse(store.get(k) || "null") || d; } catch (e) { return d; } };
|
||||
const wireSave = () => { store.set("sp-wire-pos", JSON.stringify(wire.pos)); store.set("sp-wire-hidden", JSON.stringify([...wire.hidden])); store.set("sp-wire-types", JSON.stringify([...wire.types])); };
|
||||
const wireType = (id) => (wire.data.types.find((t) => t.id === id) || { label: id, color: "#888" });
|
||||
const wireNode = (id) => wire.data.nodes.find((n) => n.id === id);
|
||||
const wireName = (id) => (wireNode(id) || { label: id }).label;
|
||||
async function wiring(rest, quiet) {
|
||||
if (quiet && (wire.drag || document.getElementById("dialog").hidden === false)) return;
|
||||
wire.data = await api("/api/wiring");
|
||||
if (!wire.pos) { wire.pos = wireRead("sp-wire-pos", {}); wire.hidden = new Set(wireRead("sp-wire-hidden", [])); wire.types = new Set(wireRead("sp-wire-types", ["watch"])); } // the watchdog's own lines: hidden at first
|
||||
if (wire.sel && !(wire.sel.edge ? wire.data.edges.some((e) => e.id === wire.sel.id) : wireNode(wire.sel.id))) wire.sel = null;
|
||||
wireLayout();
|
||||
if (!(quiet && document.getElementById("wiresvg"))) {
|
||||
main.innerHTML = `<h1>Wiring${hint("What is connected to what. Drag a machine to move it, click it for its scripts and connections. Colours: the kind of connection; red = that connection has a problem; dashed = can't see it from here.")}</h1>
|
||||
<div class="tabs" id="wiretools"></div><div class="wire"><div class="wirecanvas"><div class="wzoom"><button class="small" id="wzin" aria-label="zoom in">+</button><button class="small" id="wzout" aria-label="zoom out">−</button><button class="small" id="wfit" data-hint="Show everything that is shown">Fit</button></div><svg id="wiresvg" class="canvas" viewBox="0 0 ${W_W} ${W_H}" role="img" aria-label="wiring diagram"></svg></div><aside class="card wireside" id="wireside"></aside></div>`;
|
||||
wireEvents();
|
||||
}
|
||||
wireTools(); wireDraw(); wireSide();
|
||||
}
|
||||
|
||||
function wireLayout(all) {
|
||||
const d = wire.data, cx = W_W / 2, cy = W_H / 2;
|
||||
const fresh = d.nodes.filter((n) => all || !wire.pos[n.id]).map((n) => n.id);
|
||||
if (!fresh.length) return;
|
||||
const nb = {};
|
||||
for (const e of d.edges) if (e.type !== "watch") { (nb[e.from] = nb[e.from] || []).push(e.to); (nb[e.to] = nb[e.to] || []).push(e.from); }
|
||||
const move = new Set(fresh), hubs = d.nodes.filter((n) => (n.kind === "computer" || n.kind === "switch") && n.id !== d.me);
|
||||
hubs.sort((a, b) => (a.kind === "switch") - (b.kind === "switch") || a.label.localeCompare(b.label));
|
||||
if (d.me && move.has(d.me)) wire.pos[d.me] = { x: cx, y: cy };
|
||||
const angle = {};
|
||||
hubs.forEach((n, i) => { angle[n.id] = 2 * Math.PI * i / hubs.length - Math.PI / 2; if (move.has(n.id)) wire.pos[n.id] = { x: cx + W_W * 0.24 * Math.cos(angle[n.id]), y: cy + W_H * 0.26 * Math.sin(angle[n.id]) }; });
|
||||
// Each device beside the machine it belongs to: its switch or computer with the most links, else Companion's Mac.
|
||||
const anchor = {}, groups = {};
|
||||
for (const n of d.nodes) {
|
||||
if (n.kind === "computer" || n.kind === "switch") continue;
|
||||
const count = {};
|
||||
for (const o of nb[n.id] || []) if (angle[o] !== undefined) count[o] = (count[o] || 0) + 1;
|
||||
const best = Object.keys(count).sort((a, b) => count[b] - count[a])[0] || d.me;
|
||||
anchor[n.id] = best; (groups[best] = groups[best] || []).push(n);
|
||||
}
|
||||
for (const [hub, list] of Object.entries(groups)) {
|
||||
list.sort((a, b) => String(a.kind).localeCompare(String(b.kind)) || String(a.label).localeCompare(String(b.label)));
|
||||
list.forEach((n, i) => {
|
||||
if (!move.has(n.id)) return;
|
||||
if (hub === d.me || angle[hub] === undefined) { // the outer ring, kinds together
|
||||
const a = 2 * Math.PI * i / list.length - Math.PI / 2 + 0.2;
|
||||
wire.pos[n.id] = { x: cx + W_W * 0.43 * Math.cos(a), y: cy + W_H * 0.44 * Math.sin(a) };
|
||||
} else { // a fan outward from its machine
|
||||
const a = angle[hub] + (i - (list.length - 1) / 2) * 0.12, r = 1.0 + 0.16 * (1 + (i % 2));
|
||||
wire.pos[n.id] = { x: cx + W_W * 0.24 * r * Math.cos(a), y: cy + W_H * 0.26 * r * Math.sin(a) };
|
||||
}
|
||||
});
|
||||
}
|
||||
// Push apart whatever overlaps (boxes, with a margin), each device held near its machine.
|
||||
const ids = d.nodes.map((n) => n.id), size = {};
|
||||
for (const n of d.nodes) size[n.id] = nodeSize(n);
|
||||
for (let it = 0; it < 260; it++) {
|
||||
for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
|
||||
const a = ids[i], b = ids[j];
|
||||
if (!move.has(a) && !move.has(b)) continue;
|
||||
const pa = wire.pos[a], pb = wire.pos[b];
|
||||
const ox = (size[a].w + size[b].w) / 2 + 14 - Math.abs(pa.x - pb.x), oy = (size[a].h + size[b].h) / 2 + 12 - Math.abs(pa.y - pb.y);
|
||||
if (ox <= 0 || oy <= 0) continue;
|
||||
const ma = move.has(a) ? (move.has(b) ? 0.5 : 1) : 0, mb = 1 - ma;
|
||||
if (ox < oy) { const sx = (pa.x < pb.x ? -1 : 1) * ox; pa.x += sx * ma; pb.x -= sx * mb; }
|
||||
else { const sy = (pa.y < pb.y ? -1 : 1) * oy; pa.y += sy * ma; pb.y -= sy * mb; }
|
||||
}
|
||||
for (const id of move) {
|
||||
const h = anchor[id], p = wire.pos[id];
|
||||
if (h && h !== d.me && wire.pos[h]) { const q = wire.pos[h], dx = q.x - p.x, dy = q.y - p.y, dd = Math.hypot(dx, dy) || 1; if (dd > 230) { p.x += dx * 0.02; p.y += dy * 0.02; } }
|
||||
p.x = Math.max(size[id].w / 2 + 6, Math.min(W_W - size[id].w / 2 - 6, p.x)); p.y = Math.max(20, Math.min(W_H - 20, p.y));
|
||||
}
|
||||
}
|
||||
wireSave();
|
||||
}
|
||||
|
||||
function wireFit() {
|
||||
const { nodes } = wireShown();
|
||||
if (!nodes.length) { wire.view = { x: 0, y: 0, w: W_W, h: W_H }; return; }
|
||||
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
||||
for (const n of nodes) { const p = wire.pos[n.id], { w, h } = nodeSize(n); x0 = Math.min(x0, p.x - w / 2); x1 = Math.max(x1, p.x + w / 2); y0 = Math.min(y0, p.y - h / 2); y1 = Math.max(y1, p.y + h / 2); }
|
||||
const svg = document.getElementById("wiresvg"), box = svg && svg.getBoundingClientRect();
|
||||
const pad = 30, w = x1 - x0 + 2 * pad, h = y1 - y0 + 2 * pad, ratio = box && box.height ? box.width / box.height : W_W / W_H;
|
||||
const vw = Math.max(w, h * ratio), vh = vw / ratio;
|
||||
wire.view = { x: (x0 + x1) / 2 - vw / 2, y: (y0 + y1) / 2 - vh / 2, w: vw, h: vh };
|
||||
}
|
||||
function wireZoom(f, px, py) {
|
||||
const v = wire.view, nw = Math.max(250, Math.min(W_W * 2.5, v.w * f)), k = nw / v.w;
|
||||
px = px == null ? v.x + v.w / 2 : px; py = py == null ? v.y + v.h / 2 : py;
|
||||
wire.view = { x: px - (px - v.x) * k, y: py - (py - v.y) * k, w: nw, h: v.h * k };
|
||||
const svg = document.getElementById("wiresvg"); if (svg) svg.setAttribute("viewBox", `${wire.view.x} ${wire.view.y} ${wire.view.w} ${wire.view.h}`);
|
||||
}
|
||||
|
||||
function wireShown() {
|
||||
const d = wire.data, sel = wire.sel && !wire.sel.edge ? wire.sel.id : null;
|
||||
const edges = d.edges.filter((e) => !wire.types.has(e.type) && !wire.hidden.has(e.id) && (!wire.problems || e.light === "problem")
|
||||
&& (!wire.focus || !sel || e.from === sel || e.to === sel));
|
||||
const ends = new Set(); for (const e of edges) { ends.add(e.from); ends.add(e.to); }
|
||||
const nodes = d.nodes.filter((n) => ends.has(n.id) || (n.kind === "computer" && !wire.focus && !wire.problems) || n.id === sel);
|
||||
return { edges, nodes };
|
||||
}
|
||||
const nodeSize = (n) => n.kind === "computer" ? { w: 168, h: 46 } : { w: Math.min(190, 26 + 6.6 * String(n.label).length), h: 26 };
|
||||
|
||||
function wireDraw() {
|
||||
const svg = document.getElementById("wiresvg");
|
||||
if (!svg) return;
|
||||
if (!wire.view) wireFit();
|
||||
svg.setAttribute("viewBox", `${wire.view.x} ${wire.view.y} ${wire.view.w} ${wire.view.h}`);
|
||||
const { edges, nodes } = wireShown(), sel = wire.sel;
|
||||
const near = new Set();
|
||||
if (sel && !sel.edge) { near.add(sel.id); for (const e of edges) if (e.from === sel.id || e.to === sel.id) { near.add(e.from); near.add(e.to); } }
|
||||
if (sel && sel.edge) { const e = wire.data.edges.find((x) => x.id === sel.id); if (e) { near.add(e.from); near.add(e.to); } }
|
||||
const pairs = {};
|
||||
for (const e of edges) { const k = [e.from, e.to].sort().join("|"); (pairs[k] = pairs[k] || []).push(e); }
|
||||
let out = "";
|
||||
for (const list of Object.values(pairs)) list.forEach((e, i) => {
|
||||
const a = wire.pos[e.from], b = wire.pos[e.to];
|
||||
if (!a || !b) return;
|
||||
const off = (i - (list.length - 1) / 2) * 16, dx = b.x - a.x, dy = b.y - a.y, len = Math.hypot(dx, dy) || 1;
|
||||
const mx = (a.x + b.x) / 2 - dy / len * off, my = (a.y + b.y) / 2 + dx / len * off;
|
||||
const t = wireType(e.type), on = sel ? (sel.edge ? sel.id === e.id : e.from === sel.id || e.to === sel.id) : true;
|
||||
out += `<path class="wedge ${esc(e.light)}${on ? "" : " dim"}${sel && sel.edge && sel.id === e.id ? " sel" : ""}" data-edge="${esc(e.id)}" d="M${a.x},${a.y} Q${mx},${my} ${b.x},${b.y}" style="stroke:${e.light === "problem" ? "var(--problem)" : t.color}"><title>${esc(t.label)}: ${esc(wireName(e.from))} ↔ ${esc(wireName(e.to))} · ${esc(e.label)} · ${esc(lightWord[e.light] || e.light)}</title></path>`;
|
||||
});
|
||||
for (const n of nodes) {
|
||||
const p = wire.pos[n.id], { w, h } = nodeSize(n), dim = sel && !near.has(n.id);
|
||||
const scripts = n.scripts ? n.scripts.filter((x) => x.running).length : null;
|
||||
const shape = n.kind === "computer" ? `<rect x="${-w / 2}" y="${-h / 2}" width="${w}" height="${h}" rx="9"/>`
|
||||
: n.kind === "switch" ? `<rect x="${-w / 2}" y="${-h / 2}" width="${w}" height="${h}" rx="2"/>`
|
||||
: `<rect x="${-w / 2}" y="${-h / 2}" width="${w}" height="${h}" rx="${h / 2}"/>`;
|
||||
const name = String(n.label).length > 26 ? String(n.label).slice(0, 25) + "…" : n.label;
|
||||
out += `<g class="wnode ${esc(n.kind)} ${esc(n.light)}${dim ? " dim" : ""}${sel && !sel.edge && sel.id === n.id ? " sel" : ""}" data-node="${esc(n.id)}" transform="translate(${p.x},${p.y})"><title>${esc(n.label)}${n.address ? " · " + esc(n.address) : ""}${n.reason ? " · " + esc(n.reason) : ""}</title>${shape}
|
||||
<circle class="wdot" cx="${-w / 2 + 12}" cy="0" r="5"/><text x="${-w / 2 + 22}" y="${n.kind === "computer" ? -3 : 4}">${esc(name)}</text>
|
||||
${n.kind === "computer" ? `<text class="wsub" x="${-w / 2 + 22}" y="13">${scripts == null ? "scripts: not looked yet" : scripts + " script" + (scripts === 1 ? "" : "s") + " running"}${n.self ? " · this Mac" : ""}</text>` : ""}</g>`;
|
||||
}
|
||||
svg.innerHTML = out;
|
||||
}
|
||||
|
||||
function wireTools() {
|
||||
const el = document.getElementById("wiretools");
|
||||
if (!el) return;
|
||||
const d = wire.data, count = {};
|
||||
for (const e of d.edges) count[e.type] = (count[e.type] || 0) + 1;
|
||||
const bad = d.edges.filter((e) => e.light === "problem").length;
|
||||
el.innerHTML = d.types.filter((t) => count[t.id]).map((t) => `<button class="small wtype${wire.types.has(t.id) ? " off" : " on"}" data-wtype="${esc(t.id)}" data-hint="${esc(t.hint)} · click to ${wire.types.has(t.id) ? "show" : "hide"}"><i style="background:${t.color}"></i>${esc(t.label)} <span class="muted">${count[t.id]}</span></button>`).join("")
|
||||
+ `<button class="small${wire.problems ? " on" : ""}" id="wprob" data-hint="Only the connections with a problem">Problems only <span class="muted">${bad}</span></button>`
|
||||
+ `<button class="small" id="wall" data-hint="Show every connection again">Show all</button>`
|
||||
+ `<button class="small" id="wre" data-hint="Forget where machines were dragged and lay them out again">Re-arrange</button>`
|
||||
+ `<button class="small" id="wprobe" style="margin-left:auto" ${d.probing ? "disabled" : ""} data-hint="Log in to every computer now and list its scripts (read-only)">${d.probing ? "Looking…" : "Check scripts now"}</button>`;
|
||||
for (const b of el.querySelectorAll("[data-wtype]")) b.onclick = () => { const t = b.dataset.wtype; wire.types.has(t) ? wire.types.delete(t) : wire.types.add(t); wireSave(); wireTools(); wireDraw(); wireSide(); };
|
||||
el.querySelector("#wprob").onclick = () => { wire.problems = !wire.problems; wireTools(); wireDraw(); wireSide(); };
|
||||
el.querySelector("#wall").onclick = () => { wire.types.clear(); wire.hidden.clear(); wire.focus = false; wire.problems = false; wireSave(); wireTools(); wireDraw(); wireSide(); };
|
||||
el.querySelector("#wre").onclick = () => { wireLayout(true); wireFit(); wireDraw(); };
|
||||
el.querySelector("#wprobe").onclick = async (ev) => { ev.target.disabled = true; ev.target.textContent = "Looking…"; try { await post("/api/machines/probe"); } catch (e) { alertBox(e.message); } setTimeout(() => wiring([], true), 6000); };
|
||||
}
|
||||
|
||||
function wireSide() {
|
||||
const el = document.getElementById("wireside");
|
||||
if (!el) return;
|
||||
const d = wire.data, sel = wire.sel;
|
||||
const row = (e, other) => `<li><label class="wrow"><input type="checkbox" data-whide="${esc(e.id)}" ${wire.hidden.has(e.id) ? "" : "checked"}>${lightDot(e.light, lightWord[e.light])}<span><b>${esc(wireName(other))}</b> <span class="muted small">${esc(e.label)}</span></span></label></li>`;
|
||||
let html;
|
||||
if (sel && sel.edge) {
|
||||
const e = d.edges.find((x) => x.id === sel.id), t = wireType(e.type);
|
||||
html = `<h2><i class="wsw" style="background:${t.color}"></i>${esc(t.label)}</h2><p>${esc(wireName(e.from))} ↔ ${esc(wireName(e.to))}</p><p class="small">${esc(e.label)} · ${lightDot(e.light)} ${esc(lightWord[e.light] || e.light)}</p>
|
||||
<p><button class="small" data-whide-one="${esc(e.id)}">${wire.hidden.has(e.id) ? "Show" : "Hide"} this connection</button> <button class="small" data-wsel="${esc(e.from)}">${esc(wireName(e.from))}</button> <button class="small" data-wsel="${esc(e.to)}">${esc(wireName(e.to))}</button></p>`;
|
||||
} else if (sel) {
|
||||
const n = wireNode(sel.id), mine = d.edges.filter((e) => e.from === n.id || e.to === n.id);
|
||||
const byType = {};
|
||||
for (const e of mine) (byType[e.type] = byType[e.type] || []).push(e);
|
||||
const scripts = n.kind !== "computer" ? "" : n.scripts == null
|
||||
? `<h3>Scripts</h3><p class="small muted">Not looked yet: press Check scripts now.</p>`
|
||||
: `<h3>Scripts <span class="muted small">${n.scripts.filter((x) => x.running).length} running${n.scripts.some((x) => !x.running) ? `, ${n.scripts.filter((x) => !x.running).length} stopped` : ""}</span></h3>${n.scripts.length ? `<ul class="wlist">${n.scripts.map((x) => `<li>${lightDot(x.running ? "ok" : "not_needed", x.running ? "running" : (x.state || "not running"))}<span class="mono small">${esc(x.name)}</span>${x.by && x.by !== x.name ? ` <span class="muted small">${esc(x.by.replace(/^org\.waterschurch\./, ""))}</span>` : ""}</li>`).join("")}</ul>` : `<p class="small muted">None.</p>`}${n.looked ? `<p class="small muted">looked ${esc(ago(Date.now() / 1000 - n.looked))}</p>` : ""}`;
|
||||
html = `<h2>${lightDot(n.light, lightWord[n.light])} ${esc(n.label)}</h2><p class="small muted">${esc(n.kind === "computer" ? (n.os === "windows" ? "Windows PC" : "Mac") + (n.self ? " · this Mac (the panel)" : "") : n.kind)}${n.address ? " · " + esc(n.address) : ""}${n.role ? " · " + esc(n.role) : ""}</p>
|
||||
${n.reason ? `<p class="small">${esc(n.reason)}</p>` : ""}${n.names && n.names.length > 1 ? `<p class="small muted">also called ${esc(n.names.filter((x) => x !== n.label).join(", "))}</p>` : ""}
|
||||
<p><button class="small${wire.focus ? " on" : ""}" id="wfocus">${wire.focus ? "Show everything" : "Only this one's connections"}</button></p>${scripts}
|
||||
<h3>Connections <span class="muted small">${mine.length}</span></h3>${Object.entries(byType).map(([t, list]) => `<p class="small wtyp"><i class="wsw" style="background:${wireType(t).color}"></i>${esc(wireType(t).label)}</p><ul class="wlist">${list.map((e) => row(e, e.from === n.id ? e.to : e.from)).join("")}</ul>`).join("") || `<p class="small muted">None.</p>`}
|
||||
${(n.local || []).length ? `<p class="small muted">On this machine itself: ${esc(n.local.map((x) => x.label).join(", "))}</p>` : ""}`;
|
||||
} else {
|
||||
const comps = d.nodes.filter((n) => n.kind === "computer");
|
||||
const bad = d.edges.filter((e) => e.light === "problem");
|
||||
html = `<h2>${comps.length} computers, ${d.nodes.length - comps.length} devices</h2><p class="small">${d.edges.length} connections. Click a machine for its scripts and connections; untick a connection to hide it.</p>
|
||||
<h3>Scripts per computer</h3><ul class="wlist">${comps.map((n) => `<li><a href="#" data-wsel="${esc(n.id)}">${esc(n.label)}</a> <span class="muted small">${n.scripts == null ? "not looked yet" : n.scripts.filter((x) => x.running).length + " running"}</span></li>`).join("")}</ul>
|
||||
${bad.length ? `<h3>Problems <span class="muted small">${bad.length}</span></h3><ul class="wlist">${bad.map((e) => `<li><a href="#" data-wedge="${esc(e.id)}">${esc(wireName(e.from))} ↔ ${esc(wireName(e.to))}</a> <span class="muted small">${esc(e.label)}</span></li>`).join("")}</ul>` : ""}
|
||||
${wire.hidden.size ? `<h3>Hidden <span class="muted small">${wire.hidden.size}</span></h3><ul class="wlist">${[...wire.hidden].map((id) => d.edges.find((e) => e.id === id)).filter(Boolean).map((e) => `<li><label class="wrow"><input type="checkbox" data-whide="${esc(e.id)}"><span>${esc(wireName(e.from))} ↔ ${esc(wireName(e.to))} <span class="muted small">${esc(e.label)}</span></span></label></li>`).join("")}</ul>` : ""}`;
|
||||
}
|
||||
el.innerHTML = html;
|
||||
for (const c of el.querySelectorAll("[data-whide]")) c.onchange = () => { c.checked ? wire.hidden.delete(c.dataset.whide) : wire.hidden.add(c.dataset.whide); wireSave(); wireDraw(); wireSide(); };
|
||||
for (const b of el.querySelectorAll("[data-whide-one]")) b.onclick = () => { const id = b.dataset.whideOne; wire.hidden.has(id) ? wire.hidden.delete(id) : wire.hidden.add(id); wireSave(); wireDraw(); wireSide(); };
|
||||
for (const a of el.querySelectorAll("[data-wsel]")) a.onclick = (ev) => { ev.preventDefault(); wire.sel = { id: a.dataset.wsel }; wireDraw(); wireSide(); };
|
||||
for (const a of el.querySelectorAll("[data-wedge]")) a.onclick = (ev) => { ev.preventDefault(); wire.sel = { id: a.dataset.wedge, edge: true }; wireDraw(); wireSide(); };
|
||||
const f = el.querySelector("#wfocus"); if (f) f.onclick = () => { wire.focus = !wire.focus; wireDraw(); wireSide(); };
|
||||
}
|
||||
|
||||
function wireEvents() {
|
||||
const svg = document.getElementById("wiresvg");
|
||||
const at = (ev) => { const pt = svg.createSVGPoint(); pt.x = ev.clientX; pt.y = ev.clientY; return pt.matrixTransform(svg.getScreenCTM().inverse()); };
|
||||
document.getElementById("wzin").onclick = () => wireZoom(0.8);
|
||||
document.getElementById("wzout").onclick = () => wireZoom(1.25);
|
||||
document.getElementById("wfit").onclick = () => { wireFit(); wireDraw(); };
|
||||
svg.addEventListener("wheel", (ev) => { ev.preventDefault(); const p = at(ev); wireZoom(ev.deltaY > 0 ? 1.12 : 1 / 1.12, p.x, p.y); }, { passive: false });
|
||||
svg.addEventListener("pointerdown", (ev) => {
|
||||
const g = ev.target.closest("[data-node]"), path = ev.target.closest("[data-edge]");
|
||||
const p = at(ev);
|
||||
if (g) { const id = g.dataset.node; wire.drag = { id, dx: wire.pos[id].x - p.x, dy: wire.pos[id].y - p.y, x0: ev.clientX, y0: ev.clientY, moved: false }; }
|
||||
else if (path) { wire.sel = { id: path.dataset.edge, edge: true }; wireDraw(); wireSide(); return; }
|
||||
else wire.drag = { pan: true, x0: ev.clientX, y0: ev.clientY, vx: wire.view.x, vy: wire.view.y, k: wire.view.w / svg.getBoundingClientRect().width, moved: false };
|
||||
svg.setPointerCapture(ev.pointerId); ev.preventDefault();
|
||||
});
|
||||
svg.addEventListener("pointermove", (ev) => {
|
||||
const dr = wire.drag; if (!dr) return;
|
||||
if (!dr.moved && Math.hypot(ev.clientX - dr.x0, ev.clientY - dr.y0) < 4) return;
|
||||
dr.moved = true;
|
||||
if (dr.pan) { wire.view.x = dr.vx - (ev.clientX - dr.x0) * dr.k; wire.view.y = dr.vy - (ev.clientY - dr.y0) * dr.k; svg.setAttribute("viewBox", `${wire.view.x} ${wire.view.y} ${wire.view.w} ${wire.view.h}`); return; }
|
||||
const p = at(ev);
|
||||
wire.pos[dr.id] = { x: p.x + dr.dx, y: p.y + dr.dy };
|
||||
wireDraw();
|
||||
});
|
||||
const end = () => {
|
||||
const dr = wire.drag; wire.drag = null; if (!dr) return;
|
||||
if (dr.pan) { if (!dr.moved) { wire.sel = null; wire.focus = false; wireDraw(); wireSide(); } return; }
|
||||
if (dr.moved) wireSave(); else { wire.sel = wire.sel && !wire.sel.edge && wire.sel.id === dr.id ? null : { id: dr.id }; if (!wire.sel) wire.focus = false; }
|
||||
wireDraw(); wireSide();
|
||||
};
|
||||
svg.addEventListener("pointerup", end); svg.addEventListener("pointercancel", end);
|
||||
}
|
||||
|
||||
// ---- Computers ---------------------------------------------------------------
|
||||
let compTab = "map";
|
||||
async function computers(rest, quiet) {
|
||||
|
||||
Reference in New Issue
Block a user