Node Graph Editor
Qwen 3.6 27B MTP GGUF · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 49,868 bytes · SHA-256 c88cc8e337e4
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Logic Circuit Node-Graph Editor</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0d1117; font-family: 'Segoe UI', system-ui, sans-serif; color: #e0e0e0; }
#app { display: flex; flex-direction: column; width: 100%; height: 100%; }
.toolbar {
display: flex; align-items: center; gap: 5px; padding: 7px 12px;
background: #0d1117; border-bottom: 1px solid #21262d;
flex-wrap: wrap; z-index: 10; user-select: none; flex-shrink: 0;
}
.toolbar button {
padding: 5px 12px; border: 1px solid #30363d; border-radius: 6px;
background: #161b22; color: #c9d1d9; font-size: 12px; cursor: pointer;
transition: all 0.12s; font-weight: 500;
}
.toolbar button:hover { background: #21262d; border-color: #58a6ff; color: #fff; }
.toolbar-sep { width: 1px; height: 24px; background: #21262d; margin: 0 3px; }
#clear-btn { color: #f85149; }
#clear-btn:hover { background: #2d1215; border-color: #f85149; }
#canvas {
flex: 1; display: block;
width: 100%; height: 100%;
min-width: 0; min-height: 0;
cursor: default;
}
#hint {
position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%);
padding: 5px 18px; background: rgba(13, 17, 23, 0.92); border: 1px solid #21262d; border-radius: 20px;
font-size: 11px; color: #484f58; pointer-events: none; z-index: 10;
white-space: nowrap; letter-spacing: 0.02em;
}
</style>
</head>
<body>
<div id="toolbar" class="toolbar">
<button data-node-type="toggle">Toggle</button>
<button data-node-type="clock">Clock</button>
<button data-node-type="and">AND</button>
<button data-node-type="or">OR</button>
<button data-node-type="not">NOT</button>
<button data-node-type="xor">XOR</button>
<button data-node-type="nand">NAND</button>
<button data-node-type="nor">NOR</button>
<button data-node-type="lamp">Lamp</button>
<button data-node-type="hex">Hex Display</button>
<div class="toolbar-sep"></div>
<button id="clear-btn">Clear All</button>
</div>
<canvas id="canvas"></canvas>
<div id="hint">Scroll to zoom · Drag empty space to pan · Click ports to wire · Delete to remove</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./render":"src/render.ts","./interaction":"src/interaction.ts","./eval":"src/eval.ts","./config":"src/config.ts","./types":"src/types.ts"},"src/config.ts":{"./types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts","./config":"src/config.ts"},"src/eval.ts":{"./types":"src/types.ts","./config":"src/config.ts"},"src/render.ts":{"./types":"src/types.ts","./eval":"src/eval.ts","./config":"src/config.ts"},"src/interaction.ts":{"./types":"src/types.ts","./render":"src/render.ts","./config":"src/config.ts","./state":"src/state.ts","./eval":"src/eval.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// Main entry point
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const render_1 = require("./render");
const interaction_1 = require("./interaction");
const eval_1 = require("./eval");
// ── Clock system ──
let clockTimers = new Map();
let clockPulse = new Map();
function startClock(nodeId, rate) {
stopClock(nodeId);
const timer = setInterval(() => {
const scene = (0, state_1.getScene)();
const node = scene.nodes.find(n => n.id === nodeId);
if (!node) {
stopClock(nodeId);
return;
}
const currentVal = (0, eval_1.getEvalState)().values.get(nodeId);
const newVal = !(currentVal?.[0] ?? false);
(0, eval_1.getEvalState)().values.set(nodeId, [newVal]);
clockPulse.set(nodeId, 15);
(0, eval_1.evaluateAll)(scene);
}, rate);
clockTimers.set(nodeId, timer);
}
function stopClock(nodeId) {
const id = clockTimers.get(nodeId);
if (id !== undefined)
clearInterval(id);
clockTimers.delete(nodeId);
}
function syncClocks() {
for (const id of clockTimers.keys())
stopClock(id);
for (const node of (0, state_1.getScene)().nodes) {
if (node.type === 'clock')
startClock(node.id, node.clockRate ?? 1000);
}
}
// ── Toolbar ──
function initToolbar() {
const toolbar = document.getElementById('toolbar');
if (!toolbar)
return;
toolbar.addEventListener('click', (e) => {
const btn = e.target;
if (btn.tagName !== 'BUTTON')
return;
const nodeType = btn.getAttribute('data-node-type');
if (nodeType) {
const vp = (0, interaction_1.getViewport)();
const canvas = document.getElementById('canvas');
const rect = canvas.getBoundingClientRect();
const cx = (rect.width / 2 - vp.panX) / vp.zoom;
const cy = (rect.height / 2 - vp.panY) / vp.zoom;
const scene = (0, state_1.getScene)();
const angle = scene.nodes.length * 0.9;
const radius = scene.nodes.length * 70 + 150;
(0, state_1.createNode)(nodeType, cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius);
(0, eval_1.evaluateAll)((0, state_1.getScene)());
syncClocks();
return;
}
if (btn.id === 'clear-btn') {
(0, state_1.clearScene)();
(0, eval_1.evaluateAll)((0, state_1.getScene)());
syncClocks();
}
});
}
// ── Viewport centering ──
function centerViewport() {
const scene = (0, state_1.getScene)();
if (scene.nodes.length === 0)
return;
const canvas = document.getElementById('canvas');
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const n of scene.nodes) {
minX = Math.min(minX, n.x - 100);
minY = Math.min(minY, n.y - 60);
maxX = Math.max(maxX, n.x + 100);
maxY = Math.max(maxY, n.y + 60);
}
const cw = canvas.clientWidth || 1280;
const ch = canvas.clientHeight || 720;
const contentCX = (minX + maxX) / 2;
const contentCY = (minY + maxY) / 2;
const fitZoom = Math.min(cw / ((maxX - minX) + 160), ch / ((maxY - minY) + 120), 0.8);
const vp = (0, interaction_1.getViewport)();
vp.zoom = Math.max(fitZoom, 0.2);
vp.panX = cw / 2 - contentCX * vp.zoom;
vp.panY = ch / 2 - contentCY * vp.zoom;
}
// ── Demo circuit ──
function createDemoCircuit() {
const t1 = (0, state_1.createNode)('toggle', 150, 160);
const t2 = (0, state_1.createNode)('toggle', 150, 300);
const and = (0, state_1.createNode)('and', 380, 230);
const not = (0, state_1.createNode)('not', 380, 370);
const lamp1 = (0, state_1.createNode)('lamp', 620, 230);
const lamp2 = (0, state_1.createNode)('lamp', 620, 370);
const s = (0, state_1.getScene)();
s.nodes[0].toggleOn = true; // T1 ON
s.nodes[1].toggleOn = true; // T2 ON
(0, state_1.addWire)(t1, 0, and, 0);
(0, state_1.addWire)(t2, 0, and, 1);
(0, state_1.addWire)(t1, 0, not, 0);
(0, state_1.addWire)(and, 0, lamp1, 0);
(0, state_1.addWire)(not, 0, lamp2, 0);
}
// ── Render loop ──
function gameLoop() {
clockPulse.forEach((v, id) => {
if (v > 0)
clockPulse.set(id, v - 1);
});
(0, render_1.render)((0, state_1.getScene)(), (0, interaction_1.getViewport)(), (0, interaction_1.getHoveredNode)(), (0, interaction_1.getSelectedNode)(), (0, interaction_1.getHoveredPort)(), (0, interaction_1.getWireDragInfo)(), (0, interaction_1.isCycleReject)(), clockPulse);
requestAnimationFrame(gameLoop);
}
// ── Init ──
function init() {
const canvas = document.getElementById('canvas');
(0, state_1.initScene)();
(0, render_1.initRenderer)(canvas);
(0, interaction_1.initInteraction)(canvas);
initToolbar();
if ((0, state_1.getScene)().nodes.length === 0)
createDemoCircuit();
centerViewport();
(0, eval_1.evaluateAll)((0, state_1.getScene)());
syncClocks();
requestAnimationFrame(gameLoop);
}
init();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Types for the logic circuit node-graph editor
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
// Tunable constants for the logic circuit editor
Object.defineProperty(exports, "__esModule", { value: true });
exports.STORAGE_KEY = exports.NODE_TYPES = exports.DEFAULT_CLOCK_RATE = exports.CLOCK_RATES = exports.WIRE_BEND = exports.GRID_SIZE = exports.NODE_PADDING = exports.PORT_MARGIN_TOP = exports.PORT_SPACING = exports.LAMP_NODE_HEIGHT = exports.HEX_NODE_HEIGHT = exports.NODE_HEIGHT = exports.NODE_WIDTH = exports.PORT_SNAP_DISTANCE = exports.PORT_HIT_TOLERANCE = exports.PORT_RADIUS = exports.ZOOM_STEP = exports.ZOOM_MAX = exports.ZOOM_MIN = exports.COLORS = void 0;
exports.getNodeConfig = getNodeConfig;
exports.truthTable = truthTable;
exports.COLORS = {
bg: '#1a1a2e',
grid: '#222244',
gridDot: '#2a2a50',
nodeBg: '#16213e',
nodeBorder: '#0f3460',
nodeHover: '#1a2744',
nodeSelected: '#533483',
portInput: '#4fc3f7',
portOutput: '#ffb74d',
wireOn: '#66ff66',
wireOff: '#555577',
wirePreview: '#8888cc',
wireCycle: '#ff4444',
textPrimary: '#e0e0e0',
textSecondary: '#8b949e',
lampOff: '#333355',
lampOn: '#66ff66',
hexBg: '#0d0d1a',
hexOff: '#2a2a3a',
hexOn: '#00ff88',
toggleOff: '#444466',
toggleOn: '#66ff66',
clockIndicator: '#ffaa00',
};
exports.ZOOM_MIN = 0.5;
exports.ZOOM_MAX = 2.0;
exports.ZOOM_STEP = 0.1;
exports.PORT_RADIUS = 7;
exports.PORT_HIT_TOLERANCE = 14; // in world coords, adjusted by zoom
exports.PORT_SNAP_DISTANCE = 25;
exports.NODE_WIDTH = 140;
exports.NODE_HEIGHT = 50;
exports.HEX_NODE_HEIGHT = 80;
exports.LAMP_NODE_HEIGHT = 70;
exports.PORT_SPACING = 22;
exports.PORT_MARGIN_TOP = 12;
exports.NODE_PADDING = 12;
exports.GRID_SIZE = 30;
exports.WIRE_BEND = 60; // bezier control point distance
exports.CLOCK_RATES = [200, 500, 1000, 2000];
exports.DEFAULT_CLOCK_RATE = 1000;
exports.NODE_TYPES = [
{ type: 'toggle', label: 'Toggle', inputPorts: 0, outputPorts: 1, height: exports.NODE_HEIGHT },
{ type: 'clock', label: 'Clock', inputPorts: 0, outputPorts: 1, height: exports.NODE_HEIGHT },
{ type: 'and', label: 'AND', inputPorts: 2, outputPorts: 1, height: exports.NODE_HEIGHT },
{ type: 'or', label: 'OR', inputPorts: 2, outputPorts: 1, height: exports.NODE_HEIGHT },
{ type: 'not', label: 'NOT', inputPorts: 1, outputPorts: 1, height: exports.NODE_HEIGHT },
{ type: 'xor', label: 'XOR', inputPorts: 2, outputPorts: 1, height: exports.NODE_HEIGHT },
{ type: 'nand', label: 'NAND', inputPorts: 2, outputPorts: 1, height: exports.NODE_HEIGHT },
{ type: 'nor', label: 'NOR', inputPorts: 2, outputPorts: 1, height: exports.NODE_HEIGHT },
{ type: 'lamp', label: 'Lamp', inputPorts: 1, outputPorts: 0, height: exports.LAMP_NODE_HEIGHT },
{ type: 'hex', label: 'Hex Display', inputPorts: 4, outputPorts: 0, height: exports.HEX_NODE_HEIGHT },
];
function getNodeConfig(type) {
return exports.NODE_TYPES.find(n => n.type === type);
}
exports.STORAGE_KEY = 'logic-circuit-editor';
function truthTable(type, inputs) {
switch (type) {
case 'and': return inputs[0] && inputs[1];
case 'or': return inputs[0] || inputs[1];
case 'not': return !inputs[0];
case 'xor': return inputs[0] !== inputs[1];
case 'nand': return !(inputs[0] && inputs[1]);
case 'nor': return !(inputs[0] || inputs[1]);
default: return false;
}
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// Scene state management — nodes, wires, persistence
Object.defineProperty(exports, "__esModule", { value: true });
exports.initScene = initScene;
exports.getScene = getScene;
exports.createNode = createNode;
exports.removeNode = removeNode;
exports.addWire = addWire;
exports.removeWire = removeWire;
exports.hasWireTo = hasWireTo;
exports.toggleNode = toggleNode;
exports.cycleClockRate = cycleClockRate;
exports.saveScene = saveScene;
exports.loadScene = loadScene;
exports.clearScene = clearScene;
const config_1 = require("./config");
let scene = { nodes: [], wires: [] };
let idCounter = 0;
function initScene() {
loadScene();
}
function getScene() {
return scene;
}
function createNode(type, x, y) {
const id = `n_${++idCounter}`;
const node = {
id, type, x, y,
toggleOn: type === 'toggle' ? false : undefined,
clockRate: type === 'clock' ? config_1.DEFAULT_CLOCK_RATE : undefined,
};
scene.nodes.push(node);
saveScene();
return id;
}
function removeNode(id) {
scene.nodes = scene.nodes.filter(n => n.id !== id);
scene.wires = scene.wires.filter(w => w.fromId !== id && w.toId !== id);
saveScene();
}
function addWire(fromId, fromPort, toId, toPort) {
// Remove existing wire on this input port
scene.wires = scene.wires.filter(w => !(w.toId === toId && w.toPort === toPort));
// Add new wire
scene.wires.push({ fromId, fromPort, toId, toPort });
saveScene();
}
function removeWire(fromId, fromPort, toId, toPort) {
scene.wires = scene.wires.filter(w => !(w.fromId === fromId && w.fromPort === fromPort && w.toId === toId && w.toPort === toPort));
saveScene();
}
function hasWireTo(toId, toPort) {
return scene.wires.find(w => w.toId === toId && w.toPort === toPort);
}
function toggleNode(id, value) {
const node = scene.nodes.find(n => n.id === id);
if (node) {
if (value !== undefined) {
node.toggleOn = value;
}
else {
node.toggleOn = !node.toggleOn;
}
saveScene();
}
}
function cycleClockRate(id) {
const node = scene.nodes.find(n => n.id === id);
if (node?.type === 'clock') {
const rates = [200, 500, 1000, 2000];
const idx = rates.indexOf(node.clockRate ?? 1000);
node.clockRate = rates[(idx + 1) % rates.length];
saveScene();
}
}
function saveScene() {
try {
const data = JSON.stringify({ scene, idCounter });
localStorage.setItem(config_1.STORAGE_KEY, data);
}
catch { /* storage full or unavailable */ }
}
function loadScene() {
try {
const raw = localStorage.getItem(config_1.STORAGE_KEY);
if (raw) {
const data = JSON.parse(raw);
// Handle both formats: { scene: { nodes, wires }, idCounter } or { nodes, wires, idCounter }
if (data.scene && data.scene.nodes) {
scene = data.scene;
idCounter = data.idCounter ?? scene.nodes.length;
}
else if (data.nodes) {
scene = { nodes: data.nodes, wires: data.wires || [] };
idCounter = data.idCounter ?? scene.nodes.length;
}
else {
scene = { nodes: [], wires: [] };
idCounter = 0;
}
}
}
catch { /* corrupted data */ }
}
function clearScene() {
scene = { nodes: [], wires: [] };
saveScene();
}
};
// ── module: src/eval.ts ──
__mods["src/eval.ts"] = function (exports, require, module) {
"use strict";
// Reactive topological evaluation engine
Object.defineProperty(exports, "__esModule", { value: true });
exports.getEvalState = getEvalState;
exports.getOutput = getOutput;
exports.evaluateAll = evaluateAll;
exports.evaluateFrom = evaluateFrom;
const config_1 = require("./config");
let evalState = { values: new Map(), topoOrder: new Map() };
function getEvalState() {
return evalState;
}
function getOutput(nodeId, portIndex) {
const vals = evalState.values.get(nodeId);
if (!vals)
return false;
if (portIndex !== undefined && vals[portIndex] !== undefined)
return vals[portIndex];
return vals[0] ?? false;
}
function evaluateAll(scene) {
const nodes = scene.nodes;
const wires = scene.wires;
// Reset values
evalState.values = new Map();
evalState.topoOrder = new Map();
// Build adjacency: nodeId -> list of { nodeId, portIndex } (children)
// and parent map: nodeId -> list of { nodeId, portIndex } (parents)
const children = new Map();
const parents = new Map();
for (const n of nodes) {
children.set(n.id, []);
parents.set(n.id, []);
}
for (const w of wires) {
const ch = children.get(w.fromId) ?? [];
ch.push({ nodeId: w.toId, port: w.toPort });
children.set(w.fromId, ch);
const pa = parents.get(w.toId) ?? [];
pa.push({ nodeId: w.fromId, port: w.fromPort });
parents.set(w.toId, pa);
}
// Topological sort using Kahn's algorithm
const inDegree = new Map();
for (const n of nodes) {
inDegree.set(n.id, (parents.get(n.id) ?? []).length);
}
const queue = [];
for (const n of nodes) {
if (inDegree.get(n.id) === 0) {
queue.push(n.id);
}
}
const topoList = [];
while (queue.length > 0) {
const nodeId = queue.shift();
topoList.push(nodeId);
const ch = children.get(nodeId) ?? [];
for (const c of ch) {
const deg = inDegree.get(c.nodeId) - 1;
inDegree.set(c.nodeId, deg);
if (deg === 0) {
queue.push(c.nodeId);
}
}
}
// Assign topo order
topoList.forEach((id, idx) => {
evalState.topoOrder.set(id, idx);
});
// Nodes not in topoList have cycles — skip them (they won't evaluate)
// Evaluate in topo order
for (const nodeId of topoList) {
const node = nodes.find(n => n.id === nodeId);
if (!node)
continue;
const config = (0, config_1.getNodeConfig)(node.type);
const result = [];
if (node.type === 'toggle') {
result.push(node.toggleOn ?? false);
}
else if (node.type === 'clock') {
// Clock starts at false; it's toggled externally
result.push(false);
}
else {
// Gate or output: read inputs from wires
const nodeParents = parents.get(nodeId) ?? [];
const inputValues = [];
for (let i = 0; i < config.inputPorts; i++) {
const parent = nodeParents.find(p => p.port === i);
let val = false;
if (parent) {
val = getOutput(parent.nodeId, parent.port);
}
inputValues.push(val);
}
if (node.type === 'lamp' || node.type === 'hex') {
// Outputs just reflect their input
result.push(...inputValues);
}
else {
result.push((0, config_1.truthTable)(node.type, inputValues));
}
}
evalState.values.set(nodeId, result);
}
}
// Evaluate only downstream from a changed node (reactive)
function evaluateFrom(scene, sourceNodeId, sourceValue) {
// First do a full topo sort to get ordering
const nodes = scene.nodes;
const wires = scene.wires;
const children = new Map();
const parents = new Map();
for (const n of nodes) {
children.set(n.id, []);
parents.set(n.id, []);
}
for (const w of wires) {
const ch = children.get(w.fromId) ?? [];
ch.push({ nodeId: w.toId, port: w.toPort });
children.set(w.fromId, ch);
const pa = parents.get(w.toId) ?? [];
pa.push({ nodeId: w.fromId, port: w.fromPort });
parents.set(w.toId, pa);
}
// Compute topo order
const inDegree = new Map();
for (const n of nodes) {
inDegree.set(n.id, (parents.get(n.id) ?? []).length);
}
const queue = [];
for (const n of nodes) {
if (inDegree.get(n.id) === 0)
queue.push(n.id);
}
const topoList = [];
while (queue.length > 0) {
const nodeId = queue.shift();
topoList.push(nodeId);
const ch = children.get(nodeId) ?? [];
for (const c of ch) {
const deg = inDegree.get(c.nodeId) - 1;
inDegree.set(c.nodeId, deg);
if (deg === 0)
queue.push(c.nodeId);
}
}
const topoOrder = new Map();
topoList.forEach((id, idx) => topoOrder.set(id, idx));
// Set source value
evalState.values.set(sourceNodeId, [sourceValue]);
evalState.topoOrder = topoOrder;
// BFS from source, evaluate in topo order
const evaluated = new Set();
const frontier = [sourceNodeId];
evaluated.add(sourceNodeId);
while (frontier.length > 0) {
const current = frontier.shift();
const childrenList = children.get(current) ?? [];
for (const child of childrenList) {
if (evaluated.has(child.nodeId))
continue;
const parentNode = nodes.find(n => n.id === child.nodeId);
if (!parentNode)
continue;
const config = (0, config_1.getNodeConfig)(parentNode.type);
const nodeParents = parents.get(child.nodeId) ?? [];
const inputValues = [];
for (let i = 0; i < config.inputPorts; i++) {
const parent = nodeParents.find(p => p.port === i);
let val = false;
if (parent && evalState.values.has(parent.nodeId)) {
val = getOutput(parent.nodeId, parent.port);
}
inputValues.push(val);
}
const result = [];
if (parentNode.type === 'lamp' || parentNode.type === 'hex') {
result.push(...inputValues);
}
else {
result.push((0, config_1.truthTable)(parentNode.type, inputValues));
}
evalState.values.set(child.nodeId, result);
evaluated.add(child.nodeId);
frontier.push(child.nodeId);
}
}
}
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// Canvas rendering — nodes, wires, grid
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRenderer = initRenderer;
exports.render = render;
exports.portPos = portPos;
exports.nodeBounds = nodeBounds;
const eval_1 = require("./eval");
const config_1 = require("./config");
let canvas;
let ctx;
function initRenderer(c) {
canvas = c;
ctx = canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
}
function resizeCanvas() {
const dpr = window.devicePixelRatio || 1;
const displayWidth = canvas.clientWidth;
const displayHeight = canvas.clientHeight;
if (displayWidth === 0 || displayHeight === 0)
return;
canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function render(scene, viewport, hoveredNode, selectedNode, hoveredPort, wireDrag, cycleReject, clockPulse) {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
const evalState = (0, eval_1.getEvalState)();
ctx.save();
ctx.clearRect(0, 0, w, h);
ctx.translate(viewport.panX, viewport.panY);
ctx.scale(viewport.zoom, viewport.zoom);
drawGrid(viewport, w, h);
drawWires(scene, evalState, wireDrag, cycleReject);
// Draw nodes back-to-front
for (const node of scene.nodes) {
drawNode(node, evalState, node.id === hoveredNode, node.id === selectedNode, hoveredPort, clockPulse);
}
ctx.restore();
// Draw clock rate HUD
if (selectedNode) {
const selNode = scene.nodes.find(n => n.id === selectedNode);
if (selNode?.type === 'clock') {
ctx.save();
ctx.fillStyle = config_1.COLORS.clockIndicator;
ctx.font = 'bold 13px monospace';
ctx.fillText(`⏱ ${selNode.clockRate ?? 1000}ms`, 16, 26);
ctx.restore();
}
}
}
// ── Grid ──
function drawGrid(viewport, w, h) {
const { panX, panY, zoom } = viewport;
const step = config_1.GRID_SIZE * zoom;
if (step < 8)
return;
const originX = -panX % step;
const originY = -panY % step;
// Major grid
const majorStep = step * 5;
const majorOriginX = -panX % majorStep;
const majorOriginY = -panY % majorStep;
ctx.strokeStyle = 'rgba(255,255,255,0.02)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = majorOriginX; x <= w; x += majorStep) {
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
}
for (let y = majorOriginY; y <= h; y += majorStep) {
ctx.moveTo(0, y);
ctx.lineTo(w, y);
}
ctx.stroke();
// Dot grid
ctx.fillStyle = 'rgba(255,255,255,0.06)';
for (let x = originX; x < w; x += step) {
for (let y = originY; y < h; y += step) {
ctx.beginPath();
ctx.arc(x, y, Math.max(1, 1.2 * zoom), 0, Math.PI * 2);
ctx.fill();
}
}
}
// ── Wires ──
function drawWires(scene, evalState, wireDrag, cycleReject) {
const nodes = new Map(scene.nodes.map(n => [n.id, n]));
for (const wire of scene.wires) {
const fromNode = nodes.get(wire.fromId);
const toNode = nodes.get(wire.toId);
if (!fromNode || !toNode)
continue;
const fromP = portPos(fromNode, wire.fromPort, false);
const toP = portPos(toNode, wire.toPort, true);
const energized = (0, eval_1.getOutput)(wire.fromId, wire.fromPort);
drawCurve(fromP, toP, energized, false);
}
// Wire drag preview
if (wireDrag) {
if (wireDrag.fromId === '___cursor___') {
// Rewiring: cursor → target input
const tn = nodes.get(wireDrag.targetNodeId);
if (tn) {
const tp = portPos(tn, wireDrag.targetPort, true);
drawCurve({ x: wireDrag.worldX, y: wireDrag.worldY }, tp, true, cycleReject);
}
}
else {
const fn = nodes.get(wireDrag.fromId);
if (fn) {
const fp = portPos(fn, wireDrag.fromPort, false);
drawCurve(fp, { x: wireDrag.worldX, y: wireDrag.worldY }, true, cycleReject);
}
}
}
}
function drawCurve(from, to, on, reject) {
const dx = to.x - from.x;
const cx = Math.max(Math.abs(dx) * 0.45, config_1.WIRE_BEND);
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Glow layer
if (reject) {
ctx.strokeStyle = 'rgba(255, 60, 60, 0.3)';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.bezierCurveTo(from.x + cx, from.y, to.x - cx, to.y, to.x, to.y);
ctx.stroke();
ctx.strokeStyle = config_1.COLORS.wireCycle;
ctx.lineWidth = 2.5;
ctx.shadowColor = config_1.COLORS.wireCycle;
ctx.shadowBlur = 10;
}
else if (on) {
ctx.strokeStyle = 'rgba(102, 255, 102, 0.2)';
ctx.lineWidth = 7;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.bezierCurveTo(from.x + cx, from.y, to.x - cx, to.y, to.x, to.y);
ctx.stroke();
ctx.strokeStyle = config_1.COLORS.wireOn;
ctx.lineWidth = 2.5;
ctx.shadowColor = config_1.COLORS.wireOn;
ctx.shadowBlur = 6;
}
else {
ctx.strokeStyle = config_1.COLORS.wireOff;
ctx.lineWidth = 2;
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
}
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.bezierCurveTo(from.x + cx, from.y, to.x - cx, to.y, to.x, to.y);
ctx.stroke();
ctx.restore();
}
// ── Nodes ──
function drawNode(node, evalState, isHovered, isSelected, hoveredPort, clockPulse) {
const cfg = (0, config_1.getNodeConfig)(node.type);
const H = cfg.height;
const W = config_1.NODE_WIDTH;
const x = node.x - W / 2;
const y = node.y - H / 2;
const isOn = evalState.values.get(node.id)?.[0] ?? false;
ctx.save();
// Depth shadow
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = isHovered ? 18 : 10;
ctx.shadowOffsetY = isHovered ? 6 : 3;
// Card
const grad = ctx.createLinearGradient(x, y, x, y + H);
grad.addColorStop(0, '#1e2a4a');
grad.addColorStop(1, '#16213e');
ctx.fillStyle = grad;
roundRect(ctx, x, y, W, H, 10);
ctx.fill();
// Border
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
if (isSelected) {
ctx.strokeStyle = config_1.COLORS.nodeSelected;
ctx.lineWidth = 2.5;
ctx.shadowColor = config_1.COLORS.nodeSelected;
ctx.shadowBlur = 8;
}
else if (isHovered) {
ctx.strokeStyle = '#2a4a8a';
ctx.lineWidth = 1.5;
}
else {
ctx.strokeStyle = 'rgba(100, 140, 200, 0.2)';
ctx.lineWidth = 1;
}
roundRect(ctx, x, y, W, H, 10);
ctx.stroke();
ctx.shadowBlur = 0;
// Type-specific content
if (node.type === 'toggle')
drawToggleNode(ctx, node, x, y, W, H);
else if (node.type === 'clock')
drawClockNode(ctx, node, x, y, W, H, isOn, clockPulse);
else if (node.type === 'lamp')
drawLampNode(ctx, node, x, y, W, H, isOn);
else if (node.type === 'hex')
drawHexNode(ctx, node, x, y, W, H, evalState);
else {
// Gate: icon + label
const iconY = y + H / 2;
ctx.fillStyle = isOn ? 'rgba(102, 255, 102, 0.9)' : 'rgba(180, 200, 220, 0.8)';
ctx.font = `bold ${Math.min(20, H * 0.45)}px "Segoe UI", system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(cfg.label, node.x, iconY);
}
// Ports
drawPorts(ctx, node, x, y, W, H, hoveredPort);
ctx.restore();
}
function drawToggleNode(ctx, node, x, y, W, H) {
const on = node.toggleOn ?? false;
const cx = node.x;
const cy = node.y;
// Switch track
const tw = 36, th = 18;
const tx = cx - tw / 2;
const ty = cy - th / 2;
ctx.fillStyle = on ? 'rgba(102, 255, 102, 0.3)' : 'rgba(60, 60, 80, 0.5)';
roundRect(ctx, tx, ty, tw, th, th / 2);
ctx.fill();
// Knob
const knobX = on ? cx + tw / 2 - 10 : cx - tw / 2 + 10;
ctx.beginPath();
ctx.arc(knobX, cy, 7, 0, Math.PI * 2);
ctx.fillStyle = on ? config_1.COLORS.toggleOn : '#888';
ctx.fill();
if (on) {
ctx.shadowColor = config_1.COLORS.toggleOn;
ctx.shadowBlur = 10;
ctx.fill();
ctx.shadowBlur = 0;
}
// Label
ctx.fillStyle = config_1.COLORS.textPrimary;
ctx.font = 'bold 11px "Segoe UI", system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Toggle', cx, cy - H * 0.12);
ctx.fillStyle = on ? config_1.COLORS.toggleOn : config_1.COLORS.textSecondary;
ctx.font = '10px monospace';
ctx.fillText(on ? 'ON' : 'OFF', cx, cy + 20);
}
function drawClockNode(ctx, node, x, y, W, H, isOn, clockPulse) {
const cx = node.x;
const cy = node.y;
// Indicator
ctx.beginPath();
ctx.arc(cx, cy - 6, 6, 0, Math.PI * 2);
ctx.fillStyle = isOn ? config_1.COLORS.clockIndicator : '#555';
ctx.fill();
if (isOn) {
ctx.shadowColor = config_1.COLORS.clockIndicator;
ctx.shadowBlur = 12;
ctx.fill();
ctx.shadowBlur = 0;
}
// Label
ctx.fillStyle = config_1.COLORS.textPrimary;
ctx.font = 'bold 11px "Segoe UI", system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Clock', cx, cy + 10);
ctx.fillStyle = config_1.COLORS.textSecondary;
ctx.font = '9px monospace';
ctx.fillText(`${node.clockRate ?? 1000}ms`, cx, cy + 22);
}
function drawLampNode(ctx, node, x, y, W, H, isOn) {
const cx = node.x;
const cy = node.y - 5;
const r = 22;
// Outer glow ring
ctx.beginPath();
ctx.arc(cx, cy, r + 4, 0, Math.PI * 2);
ctx.fillStyle = isOn ? 'rgba(102, 255, 102, 0.08)' : 'rgba(40, 40, 60, 0.3)';
ctx.fill();
// Lamp body
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
if (isOn) {
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
g.addColorStop(0, '#aaffaa');
g.addColorStop(0.5, config_1.COLORS.lampOn);
g.addColorStop(1, 'rgba(102, 255, 102, 0.4)');
ctx.fillStyle = g;
ctx.fill();
ctx.shadowColor = config_1.COLORS.lampOn;
ctx.shadowBlur = 30;
ctx.fill();
ctx.shadowBlur = 0;
}
else {
ctx.fillStyle = '#2a2a44';
ctx.fill();
}
ctx.strokeStyle = isOn ? 'rgba(102, 255, 102, 0.6)' : '#3a3a5a';
ctx.lineWidth = 1.5;
ctx.stroke();
// Label
ctx.fillStyle = isOn ? config_1.COLORS.lampOn : config_1.COLORS.textSecondary;
ctx.font = '11px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(isOn ? '● ON' : '○ OFF', cx, cy + 30);
}
function drawHexNode(ctx, node, x, y, W, H, evalState) {
const vals = evalState.values.get(node.id);
const bits = [];
for (let i = 0; i < 4; i++)
bits.push(vals?.[i] ? 1 : 0);
const value = (bits[0] << 3) | (bits[1] << 2) | (bits[2] << 1) | bits[3];
const hexChar = value.toString(16).toUpperCase();
const cx = node.x;
const cy = node.y;
// Display background
const dw = 60, dh = 28;
const dx = cx - dw / 2;
const dy = cy - 10;
ctx.fillStyle = '#0a0a18';
roundRect(ctx, dx, dy, dw, dh, 4);
ctx.fill();
ctx.strokeStyle = 'rgba(100, 140, 200, 0.15)';
ctx.lineWidth = 1;
roundRect(ctx, dx, dy, dw, dh, 4);
ctx.stroke();
// Hex digit
ctx.fillStyle = config_1.COLORS.hexOn;
ctx.shadowColor = config_1.COLORS.hexOn;
ctx.shadowBlur = 6;
ctx.font = 'bold 18px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(hexChar, cx, cy - 2);
ctx.shadowBlur = 0;
// Bit LEDs
for (let i = 0; i < 4; i++) {
const lx = cx - 18 + i * 12;
const ly = cy + 20;
ctx.beginPath();
ctx.arc(lx, ly, 3, 0, Math.PI * 2);
ctx.fillStyle = bits[i] ? config_1.COLORS.hexOn : '#222244';
ctx.fill();
if (bits[i]) {
ctx.shadowColor = config_1.COLORS.hexOn;
ctx.shadowBlur = 4;
ctx.fill();
ctx.shadowBlur = 0;
}
}
ctx.fillStyle = config_1.COLORS.textPrimary;
ctx.font = 'bold 10px "Segoe UI", system-ui, sans-serif';
ctx.fillText('HEX', cx, cy - 28);
}
// ── Ports ──
function drawPorts(ctx, node, x, y, W, H, hoveredPort) {
const cfg = (0, config_1.getNodeConfig)(node.type);
// Input ports (left edge)
for (let i = 0; i < cfg.inputPorts; i++) {
const py = y + config_1.PORT_MARGIN_TOP + i * config_1.PORT_SPACING;
const px = x;
const hov = hoveredPort && hoveredPort.nodeId === node.id && hoveredPort.portIndex === i && hoveredPort.isInput;
// Connector line
ctx.strokeStyle = hov ? 'rgba(255,255,255,0.3)' : 'rgba(100,140,200,0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(px + 2, py);
ctx.lineTo(px + 16, py);
ctx.stroke();
// Port circle
ctx.beginPath();
ctx.arc(px, py, hov ? config_1.PORT_RADIUS + 2 : config_1.PORT_RADIUS, 0, Math.PI * 2);
ctx.fillStyle = hov ? '#2a4a8a' : config_1.COLORS.nodeBg;
ctx.fill();
ctx.strokeStyle = hov ? '#fff' : config_1.COLORS.portInput;
ctx.lineWidth = hov ? 2.5 : 1.5;
ctx.stroke();
if (hov) {
ctx.shadowColor = config_1.COLORS.portInput;
ctx.shadowBlur = 8;
ctx.stroke();
ctx.shadowBlur = 0;
}
// Label
ctx.fillStyle = hov ? '#fff' : config_1.COLORS.textSecondary;
ctx.font = '9px monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(`0`, px - 6, py);
}
// Output ports (right edge)
for (let i = 0; i < cfg.outputPorts; i++) {
const py = y + config_1.PORT_MARGIN_TOP + i * config_1.PORT_SPACING;
const px = x + W;
const hov = hoveredPort && hoveredPort.nodeId === node.id && hoveredPort.portIndex === i && !hoveredPort.isInput;
// Connector line
ctx.strokeStyle = hov ? 'rgba(255,255,255,0.3)' : 'rgba(255,183,77,0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(px - 16, py);
ctx.lineTo(px - 2, py);
ctx.stroke();
// Port circle
ctx.beginPath();
ctx.arc(px, py, hov ? config_1.PORT_RADIUS + 2 : config_1.PORT_RADIUS, 0, Math.PI * 2);
ctx.fillStyle = hov ? '#4a3a1a' : config_1.COLORS.nodeBg;
ctx.fill();
ctx.strokeStyle = hov ? '#fff' : config_1.COLORS.portOutput;
ctx.lineWidth = hov ? 2.5 : 1.5;
ctx.stroke();
if (hov) {
ctx.shadowColor = config_1.COLORS.portOutput;
ctx.shadowBlur = 8;
ctx.stroke();
ctx.shadowBlur = 0;
}
}
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
// Geometry helpers (used by both render and interaction)
function portPos(node, portIndex, isInput) {
const cfg = (0, config_1.getNodeConfig)(node.type);
const H = cfg.height;
const py = node.y - H / 2 + config_1.PORT_MARGIN_TOP + portIndex * config_1.PORT_SPACING;
const px = isInput ? node.x - config_1.NODE_WIDTH / 2 : node.x + config_1.NODE_WIDTH / 2;
return { x: px, y: py };
}
function nodeBounds(node) {
const cfg = (0, config_1.getNodeConfig)(node.type);
const H = cfg.height;
return { x: node.x - config_1.NODE_WIDTH / 2, y: node.y - H / 2, w: config_1.NODE_WIDTH, h: H };
}
};
// ── module: src/interaction.ts ──
__mods["src/interaction.ts"] = function (exports, require, module) {
"use strict";
// Interaction handler — panning, zooming, node drag, wire creation, hit testing
Object.defineProperty(exports, "__esModule", { value: true });
exports.getViewport = getViewport;
exports.initInteraction = initInteraction;
exports.getWireDragInfo = getWireDragInfo;
exports.isCycleReject = isCycleReject;
exports.getHoveredPort = getHoveredPort;
exports.getHoveredNode = getHoveredNode;
exports.getSelectedNode = getSelectedNode;
const render_1 = require("./render");
const config_1 = require("./config");
const state_1 = require("./state");
const eval_1 = require("./eval");
// ── Viewport ──
let viewport = { panX: 0, panY: 0, zoom: 1 };
function getViewport() { return viewport; }
// ── Mouse state ──
let mouseWorld = { x: 0, y: 0 };
let mode = 'idle';
let panStart = { sx: 0, sy: 0, px: 0, py: 0 };
let dragNodeData = { nodeId: '', ox: 0, oy: 0 };
let wireFromOutputData = { srcNodeId: '', srcPort: 0 };
let wireRewireData = { origInputNodeId: '', origInputPort: 0 };
let selectedNode = null;
let hoveredNode = null;
let hoveredPort = null;
let cycleReject = false;
let canvasRef;
function initInteraction(canvas) {
canvasRef = canvas;
canvas.addEventListener('mousedown', onMouseDown);
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
canvas.addEventListener('mouseleave', () => { hoveredNode = null; hoveredPort = null; });
canvas.addEventListener('wheel', onWheel, { passive: false });
canvas.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('keydown', onKeyDown);
}
function getCanvasPos(e) {
const rect = canvasRef.getBoundingClientRect();
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
function screenToWorld(sx, sy) {
return { x: (sx - viewport.panX) / viewport.zoom, y: (sy - viewport.panY) / viewport.zoom };
}
function hitTestPort(wx, wy, scene) {
const tol = config_1.PORT_HIT_TOLERANCE / viewport.zoom;
for (let ni = scene.nodes.length - 1; ni >= 0; ni--) {
const node = scene.nodes[ni];
const config = (0, config_1.getNodeConfig)(node.type);
for (let i = config.inputPorts - 1; i >= 0; i--) {
const pos = (0, render_1.portPos)(node, i, true);
const dx = wx - pos.x, dy = wy - pos.y;
if (dx * dx + dy * dy < tol * tol) {
return { nodeId: node.id, portIndex: i, isInput: true, label: '' };
}
}
for (let i = config.outputPorts - 1; i >= 0; i--) {
const pos = (0, render_1.portPos)(node, i, false);
const dx = wx - pos.x, dy = wy - pos.y;
if (dx * dx + dy * dy < tol * tol) {
return { nodeId: node.id, portIndex: i, isInput: false, label: '' };
}
}
}
return null;
}
function hitTestNode(wx, wy, scene) {
for (let i = scene.nodes.length - 1; i >= 0; i--) {
const node = scene.nodes[i];
const b = (0, render_1.nodeBounds)(node);
if (wx >= b.x && wx <= b.x + b.w && wy >= b.y && wy <= b.y + b.h) {
return node.id;
}
}
return null;
}
function wouldCreateCycle(fromNodeId, toNodeId, scene) {
const visited = new Set();
const queue = [toNodeId];
visited.add(toNodeId);
while (queue.length > 0) {
const cur = queue.shift();
if (cur === fromNodeId)
return true;
for (const wire of scene.wires) {
if (wire.fromId === cur && !visited.has(wire.toId)) {
visited.add(wire.toId);
queue.push(wire.toId);
}
}
}
return false;
}
// ── Mouse events ──
function onMouseDown(e) {
if (e.button !== 0)
return;
const pos = getCanvasPos(e);
const scene = (0, state_1.getScene)();
const world = screenToWorld(pos.x, pos.y);
mouseWorld = world;
const port = hitTestPort(world.x, world.y, scene);
if (port) {
if (!port.isInput) {
mode = 'wireFromOutput';
wireFromOutputData = { srcNodeId: port.nodeId, srcPort: port.portIndex };
}
else {
const existing = (0, state_1.hasWireTo)(port.nodeId, port.portIndex);
if (existing) {
(0, state_1.removeWire)(existing.fromId, existing.fromPort, existing.toId, existing.toPort);
mode = 'wireRewireInput';
wireRewireData = { origInputNodeId: port.nodeId, origInputPort: port.portIndex };
}
}
cycleReject = false;
return;
}
const nodeHit = hitTestNode(world.x, world.y, scene);
if (nodeHit) {
const node = scene.nodes.find(n => n.id === nodeHit);
if (node.type === 'toggle') {
(0, state_1.toggleNode)(node.id);
(0, eval_1.evaluateAll)((0, state_1.getScene)());
selectedNode = node.id;
return;
}
if (node.type === 'clock') {
(0, state_1.cycleClockRate)(node.id);
(0, eval_1.evaluateAll)((0, state_1.getScene)());
selectedNode = node.id;
return;
}
mode = 'dragNode';
dragNodeData = { nodeId: nodeHit, ox: world.x - node.x, oy: world.y - node.y };
selectedNode = nodeHit;
return;
}
mode = 'panning';
panStart = { sx: pos.x, sy: pos.y, px: viewport.panX, py: viewport.panY };
selectedNode = null;
}
function onMouseMove(e) {
const pos = getCanvasPos(e);
const scene = (0, state_1.getScene)();
const world = screenToWorld(pos.x, pos.y);
mouseWorld = world;
if (mode === 'panning') {
viewport = {
panX: panStart.px + (pos.x - panStart.sx),
panY: panStart.py + (pos.y - panStart.sy),
zoom: viewport.zoom,
};
return;
}
if (mode === 'dragNode') {
const node = scene.nodes.find(n => n.id === dragNodeData.nodeId);
if (node) {
node.x = world.x - dragNodeData.ox;
node.y = world.y - dragNodeData.oy;
}
return;
}
if (mode === 'wireFromOutput' || mode === 'wireRewireInput') {
const port = hitTestPort(world.x, world.y, scene);
hoveredPort = port ? { nodeId: port.nodeId, portIndex: port.portIndex, isInput: port.isInput } : null;
if (port) {
if (mode === 'wireFromOutput') {
if (port.isInput && port.nodeId !== wireFromOutputData.srcNodeId) {
cycleReject = wouldCreateCycle(wireFromOutputData.srcNodeId, port.nodeId, scene);
}
else {
cycleReject = false;
}
}
else {
if (!port.isInput && port.nodeId !== wireRewireData.origInputNodeId) {
cycleReject = wouldCreateCycle(port.nodeId, wireRewireData.origInputNodeId, scene);
}
else {
cycleReject = false;
}
}
}
else {
cycleReject = false;
}
return;
}
// Idle — update hover
const port = hitTestPort(world.x, world.y, scene);
if (port) {
hoveredPort = { nodeId: port.nodeId, portIndex: port.portIndex, isInput: port.isInput };
hoveredNode = port.nodeId;
}
else {
const nodeHit = hitTestNode(world.x, world.y, scene);
hoveredNode = nodeHit;
hoveredPort = null;
}
}
function onMouseUp(e) {
if (mode !== 'wireFromOutput' && mode !== 'wireRewireInput') {
mode = 'idle';
hoveredPort = null;
return;
}
const pos = getCanvasPos(e);
const scene = (0, state_1.getScene)();
const world = screenToWorld(pos.x, pos.y);
const port = hitTestPort(world.x, world.y, scene);
let connected = false;
if (port && !cycleReject) {
if (mode === 'wireFromOutput') {
if (port.isInput) {
(0, state_1.addWire)(wireFromOutputData.srcNodeId, wireFromOutputData.srcPort, port.nodeId, port.portIndex);
connected = true;
}
}
else if (mode === 'wireRewireInput') {
if (!port.isInput) {
(0, state_1.addWire)(port.nodeId, port.portIndex, wireRewireData.origInputNodeId, wireRewireData.origInputPort);
connected = true;
}
}
}
if (connected)
(0, eval_1.evaluateAll)((0, state_1.getScene)());
mode = 'idle';
cycleReject = false;
hoveredPort = null;
}
function onWheel(e) {
e.preventDefault();
const pos = getCanvasPos(e);
const factor = e.deltaY < 0 ? (1 + config_1.ZOOM_STEP) : (1 / (1 + config_1.ZOOM_STEP));
const newZoom = Math.min(config_1.ZOOM_MAX, Math.max(config_1.ZOOM_MIN, viewport.zoom * factor));
const wx = (pos.x - viewport.panX) / viewport.zoom;
const wy = (pos.y - viewport.panY) / viewport.zoom;
viewport = {
panX: pos.x - wx * newZoom,
panY: pos.y - wy * newZoom,
zoom: newZoom,
};
}
function onKeyDown(e) {
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedNode) {
if (document.activeElement === document.body || document.activeElement === canvasRef) {
(0, state_1.removeNode)(selectedNode);
selectedNode = null;
(0, eval_1.evaluateAll)((0, state_1.getScene)());
}
}
}
function getWireDragInfo() {
if (mode === 'wireFromOutput') {
return {
fromId: wireFromOutputData.srcNodeId,
fromPort: wireFromOutputData.srcPort,
worldX: mouseWorld.x,
worldY: mouseWorld.y,
targetNodeId: null,
targetPort: 0,
};
}
if (mode === 'wireRewireInput') {
return {
fromId: '___cursor___',
fromPort: 0,
worldX: mouseWorld.x,
worldY: mouseWorld.y,
targetNodeId: wireRewireData.origInputNodeId,
targetPort: wireRewireData.origInputPort,
};
}
return null;
}
function isCycleReject() { return cycleReject; }
function getHoveredPort() { return hoveredPort; }
function getHoveredNode() { return hoveredNode; }
function getSelectedNode() { return selectedNode; }
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"e1447e4d-569f-4a3d-8d85-08e864aff681","tokensIn":6651090,"tokensOut":70303,"tokensTotal":6721393,"cost":0,"turns":123,"toolCalls":122,"failedToolCalls":3,"timestamp":"2026-08-02T11:04:49.737Z"} -->