← Node Graph Editor results

Node Graph Editor

Qwen 3.6 27B MTP GGUF · typescript

Initial view of Node Graph Editor

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"} -->