← Node Graph Editor results

Node Graph Editor

thinkingcap qwen3.6 27b · 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 47,246 bytes · SHA-256 32d617815e72
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Logic Circuit Editor</title>
<style>
/* ── css: layout ── */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #1a1d23;
  font-family: 'Segoe UI', system-ui, sans-serif;
}

canvas {
  display: block;
  cursor: default;
}

#palette {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 10;
  background: #1e2128ee;
  border: 1px solid #3a3f4b;
  border-radius: 10px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  backdrop-filter: blur(8px);
  box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}

.palette-title {
  color: #8a90a0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1.5px;
  text-align: center;
  padding: 6px 0 4px;
}

.palette-btn {
  background: #262a33;
  color: #e0e4ea;
  border: 1px solid #3a3f4b;
  border-radius: 6px;
  padding: 7px 14px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s ease;
  text-align: center;
  min-width: 80px;
}

.palette-btn:hover {
  background: #303540;
  border-color: #4a9eff;
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(74, 158, 255, 0.2);
}

.palette-btn:active {
  transform: translateY(0);
  background: #3a3f4b;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./rendering":"src/rendering.ts","./persistence":"src/persistence.ts","./input":"src/input.ts","./types":"src/types.ts","./evaluation":"src/evaluation.ts"},"src/state.ts":{"./constants":"src/constants.ts","./types":"src/types.ts"},"src/evaluation.ts":{"./state":"src/state.ts"},"src/rendering.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./types":"src/types.ts"},"src/persistence.ts":{"./state":"src/state.ts"},"src/input.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./evaluation":"src/evaluation.ts","./types":"src/types.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";
// ─── Entry Point ────────────────────────────────────────────────────
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("./constants");
const state_1 = require("./state");
const rendering = __importStar(require("./rendering"));
const persistence = __importStar(require("./persistence"));
const input = __importStar(require("./input"));
const evaluation_1 = require("./evaluation");
// ─── Setup DOM ──────────────────────────────────────────────────────
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
rendering.initRendering(canvas);
// Camera state
const cam = { worldX: -300, worldY: -100, zoom: 1 };
// ─── Resize handling ──────────────────────────────────────────────
function resize() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// ─── Clock ticker ─────────────────────────────────────────────────
const clockTimers = new Map();
function startClocks() {
    for (const [, timerId] of clockTimers)
        clearInterval(timerId);
    clockTimers.clear();
    for (const node of state_1.scene.nodes) {
        if (node.type === 'clock' && node.clockRate) {
            const interval = Math.round(1000 / node.clockRate);
            const timerId = setInterval(() => {
                node.outputState = !node.outputState;
                (0, evaluation_1.evaluateDownstream)(node.id);
                persistence.save();
            }, interval);
            clockTimers.set(node.id, timerId);
        }
    }
}
// ─── Palette sidebar ──────────────────────────────────────────────
const palette = document.createElement('div');
palette.id = 'palette';
document.body.appendChild(palette);
function buildPalette() {
    palette.innerHTML = '';
    const title = document.createElement('div');
    title.className = 'palette-title';
    title.textContent = 'NODES';
    palette.appendChild(title);
    for (const [type, info] of Object.entries(constants_1.NODE_TYPES)) {
        const btn = document.createElement('button');
        btn.className = 'palette-btn';
        btn.textContent = info.label;
        btn.addEventListener('click', () => {
            const cx = cam.worldX + canvas.width / (2 * cam.zoom);
            const cy = cam.worldY + canvas.height / (2 * cam.zoom);
            input.createNodeAt(type, cx, cy);
            startClocks();
        });
        palette.appendChild(btn);
    }
}
buildPalette();
// ─── Wire up modules for circular dep resolution ──────────────
input.setRenderingModule(rendering);
input.setStateModule({ createNode: state_1.createNode });
input.setSaveFn(() => persistence.save());
// Also expose on window as fallback
window.__renderingModule = rendering;
window.__stateModule = { createNode: state_1.createNode };
// ─── Setup input handlers ────────────────────────────────────────
input.setupInput(canvas, cam);
// ─── Load saved scene or create demo ────────────────────────────
if (!persistence.load()) {
    // Create a small demo circuit
    const t1 = (0, state_1.createNode)('toggle', 150, -80);
    const t2 = (0, state_1.createNode)('toggle', 150, 80);
    const and = (0, state_1.createNode)('and', 350, 0);
    const lamp = (0, state_1.createNode)('lamp', 550, 0);
    state_1.scene.wires.push({ fromId: t1.id, fromPort: 0, toId: and.id, toPort: 0 });
    state_1.scene.wires.push({ fromId: t2.id, fromPort: 0, toId: and.id, toPort: 1 });
    state_1.scene.wires.push({ fromId: and.id, fromPort: 0, toId: lamp.id, toPort: 0 });
    (0, evaluation_1.evaluateAll)();
}
startClocks();
// ─── Main render loop ──────────────────────────────────────────
let cycleRejectTimer = 0;
function frame() {
    // Handle cycle reject flash timer
    if (input.isCycleReject()) {
        cycleRejectTimer++;
        if (cycleRejectTimer > 15) {
            input.clearCycleReject();
            cycleRejectTimer = 0;
        }
    }
    rendering.render(cam);
    // Draw wire preview during drag
    const mode = input.getMode();
    if (mode === 'creatingWire') {
        const wireSrc = input.getWireSource();
        if (wireSrc) {
            const fromPos = getPortWorldPos(wireSrc.nodeId, wireSrc.portIndex);
            if (fromPos) {
                rendering.drawWirePreview(fromPos, input.lastCursorWorld, cam, input.isCycleReject());
            }
        }
    }
    // Draw selection highlight
    if (input.selectedNodeId) {
        rendering.drawSelection(input.selectedNodeId, cam);
    }
    requestAnimationFrame(frame);
}
// Helper to get port world position for wire preview (mirrors rendering.ts logic)
function getPortWorldPos(nodeId, portIndex) {
    const node = state_1.scene.nodes.find(n => n.id === nodeId);
    if (!node)
        return null;
    const info = constants_1.NODE_TYPES[node.type];
    // Try output ports first (wire source is always an output port)
    if (portIndex < info.outputs) {
        const totalPorts = Math.max(info.inputs, info.outputs);
        const totalHeight = Math.max(totalPorts, 1) * 28 - 28;
        const startY = node.y - totalHeight / 2 + 14;
        return { x: node.x + 60, y: startY + portIndex * 28 };
    }
    // Try input ports
    if (portIndex < info.inputs) {
        const totalPorts = Math.max(info.inputs, info.outputs);
        const totalHeight = Math.max(totalPorts, 1) * 28 - 28;
        const startY = node.y - totalHeight / 2 + 14;
        return { x: node.x - 60, y: startY + portIndex * 28 };
    }
    return null;
}
requestAnimationFrame(frame);
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ─── Tunable Constants ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.NODE_TYPES = exports.DEFAULT_CLOCK_RATE = exports.CLOCK_RATES = exports.GRID_SIZE = exports.PORT_SPACING = exports.NODE_HEADER_HEIGHT = exports.NODE_WIDTH = exports.PORT_HIT_TOLERANCE = exports.PORT_RADIUS = exports.ZOOM_MAX = exports.ZOOM_MIN = exports.COLORS = void 0;
exports.COLORS = {
    bg: '#1a1d23',
    gridDot: '#2a2e35',
    nodeBg: '#262a33',
    nodeBorder: '#3a3f4b',
    nodeHover: '#303540',
    nodeSelected: '#4a9eff',
    portInput: '#5b8def', // cool blue
    portOutput: '#ef7c5b', // warm orange
    wireOff: '#5a6070',
    wireOn: '#4aff80',
    wireDragging: '#ffcc33',
    wireCycleReject: '#ff3344',
    textPrimary: '#e0e4ea',
    textSecondary: '#8a90a0',
    paletteBg: '#1e2128',
    paletteBorder: '#3a3f4b',
    lampOff: '#555566',
    lampOn: '#4aff80',
    clockPulse: '#ffaa33',
};
exports.ZOOM_MIN = 0.5;
exports.ZOOM_MAX = 2.0;
exports.PORT_RADIUS = 7;
exports.PORT_HIT_TOLERANCE = 12; // world coords, expanded at low zoom
exports.NODE_WIDTH = 120;
exports.NODE_HEADER_HEIGHT = 30;
exports.PORT_SPACING = 28;
exports.GRID_SIZE = 24;
exports.CLOCK_RATES = [0.5, 1, 2, 4]; // Hz
exports.DEFAULT_CLOCK_RATE = 1;
exports.NODE_TYPES = {
    toggle: { label: 'Toggle', inputs: 0, outputs: 1 },
    clock: { label: 'Clock', inputs: 0, outputs: 1 },
    and: { label: 'AND', inputs: 2, outputs: 1 },
    or: { label: 'OR', inputs: 2, outputs: 1 },
    not: { label: 'NOT', inputs: 1, outputs: 1 },
    xor: { label: 'XOR', inputs: 2, outputs: 1 },
    nand: { label: 'NAND', inputs: 2, outputs: 1 },
    nor: { label: 'NOR', inputs: 2, outputs: 1 },
    lamp: { label: 'Lamp', inputs: 1, outputs: 0 },
    hex: { label: 'Hex', inputs: 4, outputs: 0 },
};
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─── Data Model Types ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ─── Scene State Management ──────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.scene = void 0;
exports.createNode = createNode;
exports.removeNode = removeNode;
exports.getNode = getNode;
exports.addWire = addWire;
exports.removeWiresForNode = removeWiresForNode;
exports.getInputCount = getInputCount;
exports.getOutputCount = getOutputCount;
exports.wouldCreateCycle = wouldCreateCycle;
exports.topologicalOrder = topologicalOrder;
exports.serializeScene = serializeScene;
exports.deserializeScene = deserializeScene;
const constants_1 = require("./constants");
let nextId = 1;
function genId() {
    return `n${nextId++}`;
}
exports.scene = { nodes: [], wires: [] };
// ─── Node operations ────────────────────────────────────────────────
function createNode(type, x, y) {
    const node = {
        id: genId(),
        type,
        x,
        y,
        outputState: false,
        toggleOn: type === 'toggle' ? false : undefined,
        clockRate: type === 'clock' ? constants_1.DEFAULT_CLOCK_RATE : undefined,
    };
    exports.scene.nodes.push(node);
    return node;
}
function removeNode(id) {
    exports.scene.nodes = exports.scene.nodes.filter(n => n.id !== id);
    exports.scene.wires = exports.scene.wires.filter(w => w.fromId !== id && w.toId !== id);
}
function getNode(id) {
    return exports.scene.nodes.find(n => n.id === id);
}
// ─── Wire operations ────────────────────────────────────────────────
function addWire(fromId, fromPort, toId, toPort) {
    // Remove existing wire on this input port (one wire per input)
    exports.scene.wires = exports.scene.wires.filter(w => !(w.toId === toId && w.toPort === toPort));
    // Check for duplicate
    if (exports.scene.wires.some(w => w.fromId === fromId && w.fromPort === fromPort && w.toId === toId && w.toPort === toPort)) {
        return false;
    }
    exports.scene.wires.push({ fromId, fromPort, toId, toPort });
    return true;
}
function removeWiresForNode(id) {
    exports.scene.wires = exports.scene.wires.filter(w => w.fromId !== id && w.toId !== id);
}
// ─── Port info helpers ──────────────────────────────────────────────
function getInputCount(type) {
    return constants_1.NODE_TYPES[type]?.inputs ?? 0;
}
function getOutputCount(type) {
    return constants_1.NODE_TYPES[type]?.outputs ?? 0;
}
// ─── Cycle detection (DFS on wire graph) ────────────────────────────
function wouldCreateCycle(fromId, fromPort, toId, toPort) {
    // Adding a wire from `fromId` output to `toId` input creates a cycle if
    // there's already a path from `toId` back to `fromId`.
    const visited = new Set();
    const stack = [toId];
    while (stack.length > 0) {
        const current = stack.pop();
        if (current === fromId)
            return true;
        if (visited.has(current))
            continue;
        visited.add(current);
        // Follow wires downstream from `current`
        for (const w of exports.scene.wires) {
            if (w.fromId === current) {
                stack.push(w.toId);
            }
        }
    }
    return false;
}
// ─── Topological sort for evaluation ────────────────────────────────
function topologicalOrder() {
    // Kahn's algorithm on the wire graph
    const inDegree = {};
    const adjList = {};
    for (const node of exports.scene.nodes) {
        inDegree[node.id] = 0;
        adjList[node.id] = [];
    }
    for (const w of exports.scene.wires) {
        if (inDegree[w.toId] !== undefined && inDegree[w.fromId] !== undefined) {
            adjList[w.fromId].push(w.toId);
            inDegree[w.toId]++;
        }
    }
    const queue = [];
    for (const id of Object.keys(inDegree)) {
        if (inDegree[id] === 0)
            queue.push(id);
    }
    const result = [];
    while (queue.length > 0) {
        const node = queue.shift();
        result.push(node);
        for (const neighbor of adjList[node]) {
            inDegree[neighbor]--;
            if (inDegree[neighbor] === 0)
                queue.push(neighbor);
        }
    }
    // If not all nodes are in the result, there's a cycle
    return result.length === exports.scene.nodes.length ? result : null;
}
// ─── Serialization ──────────────────────────────────────────────────
function serializeScene() {
    return JSON.stringify(exports.scene);
}
function deserializeScene(json) {
    const data = JSON.parse(json);
    exports.scene.nodes = data.nodes;
    exports.scene.wires = data.wires;
    // Restore nextId to avoid conflicts
    let maxNum = 0;
    for (const n of exports.scene.nodes) {
        const num = parseInt(n.id.slice(1), 10);
        if (!isNaN(num) && num > maxNum)
            maxNum = num;
    }
    nextId = maxNum + 1;
}
};

// ── module: src/evaluation.ts ──
__mods["src/evaluation.ts"] = function (exports, require, module) {
"use strict";
// ─── Reactive Evaluation Engine ──────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluateAll = evaluateAll;
exports.evaluateDownstream = evaluateDownstream;
exports.toggleInput = toggleInput;
const state_1 = require("./state");
// Gate truth tables
const GATES = {
    and: (a, b) => a && !!b,
    or: (a, b) => a || !!b,
    not: (a) => !a,
    xor: (a, b) => a !== !!b,
    nand: (a, b) => !(a && !!b),
    nor: (a, b) => !(a || !!b),
};
// Get input values for a node from its connected wires
function getInputValues(nodeId) {
    const inputs = [];
    for (const w of state_1.scene.wires) {
        if (w.toId === nodeId) {
            const srcNode = (0, state_1.getNode)(w.fromId);
            inputs[w.toPort] = srcNode?.outputState ?? false;
        }
    }
    return inputs;
}
// Evaluate a single node based on its type and input values
function evaluateNode(node) {
    switch (node.type) {
        case 'toggle':
            node.outputState = !!node.toggleOn;
            break;
        case 'clock':
            // Clock state is set externally by the clock ticker
            break;
        case 'lamp':
        case 'hex':
            {
                const vals = getInputValues(node.id);
                node.outputState = vals[0] ?? false;
            }
            break;
        default: {
            const gateFn = GATES[node.type];
            if (gateFn) {
                const vals = getInputValues(node.id);
                node.outputState = gateFn(vals[0] ?? false, vals[1]);
            }
        }
    }
}
// Full topological evaluation of the entire graph
function evaluateAll() {
    // Simple approach: iterate until stable (safe for combinational circuits)
    let changed = true;
    let iterations = 0;
    const maxIterations = state_1.scene.nodes.length + 2;
    while (changed && iterations < maxIterations) {
        changed = false;
        iterations++;
        for (const node of state_1.scene.nodes) {
            const oldState = node.outputState;
            evaluateNode(node);
            if (node.outputState !== oldState)
                changed = true;
        }
    }
}
// Find all downstream nodes from a source using BFS on wire graph
function findDownstreamNodes(sourceId) {
    const result = new Set();
    const queue = [sourceId];
    while (queue.length > 0) {
        const current = queue.shift();
        for (const w of state_1.scene.wires) {
            if (w.fromId === current && !result.has(w.toId)) {
                result.add(w.toId);
                queue.push(w.toId);
            }
        }
    }
    return result;
}
// Evaluate only downstream nodes from a source node
function evaluateDownstream(sourceId) {
    const downstream = findDownstreamNodes(sourceId);
    // Also include the source itself (for toggle/clock state update)
    downstream.add(sourceId);
    for (const id of downstream) {
        const node = (0, state_1.getNode)(id);
        if (node)
            evaluateNode(node);
    }
}
// Toggle a toggle node and re-evaluate downstream
function toggleInput(id) {
    const node = (0, state_1.getNode)(id);
    if (!node || node.type !== 'toggle')
        return;
    node.toggleOn = !node.toggleOn;
    evaluateDownstream(id);
}
};

// ── module: src/rendering.ts ──
__mods["src/rendering.ts"] = function (exports, require, module) {
"use strict";
// ─── Canvas Rendering ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRendering = initRendering;
exports.worldToScreen = worldToScreen;
exports.screenToWorld = screenToWorld;
exports.render = render;
exports.drawWirePreview = drawWirePreview;
exports.hitTestPort = hitTestPort;
exports.hitTestNode = hitTestNode;
exports.drawSelection = drawSelection;
const constants_1 = require("./constants");
const state_1 = require("./state");
let canvas;
let ctx;
function initRendering(cvs) {
    canvas = cvs;
    ctx = cvs.getContext('2d');
}
// ─── Coordinate transforms ──────────────────────────────────────────
function worldToScreen(wp, cam) {
    return { x: (wp.x - cam.worldX) * cam.zoom, y: (wp.y - cam.worldY) * cam.zoom };
}
function screenToWorld(sp, cam) {
    return { x: sp.x / cam.zoom + cam.worldX, y: sp.y / cam.zoom + cam.worldY };
}
// ─── Main render loop ──────────────────────────────────────────────
function render(cam) {
    const w = canvas.width;
    const h = canvas.height;
    ctx.clearRect(0, 0, w, h);
    ctx.fillStyle = constants_1.COLORS.bg;
    ctx.fillRect(0, 0, w, h);
    drawGrid(cam);
    drawWires(cam);
    drawNodes(cam);
}
// ─── Grid ──────────────────────────────────────────────────────────
function drawGrid(cam) {
    const w = canvas.width;
    const h = canvas.height;
    // Visible world bounds
    const left = cam.worldX;
    const top = cam.worldY;
    const right = (w / cam.zoom) + cam.worldX;
    const bottom = (h / cam.zoom) + cam.worldY;
    ctx.fillStyle = constants_1.COLORS.gridDot;
    // Snap to grid
    const startX = Math.floor(left / constants_1.GRID_SIZE) * constants_1.GRID_SIZE;
    const startY = Math.floor(top / constants_1.GRID_SIZE) * constants_1.GRID_SIZE;
    for (let wx = startX; wx < right; wx += constants_1.GRID_SIZE) {
        for (let wy = startY; wy < bottom; wy += constants_1.GRID_SIZE) {
            const sp = worldToScreen({ x: wx, y: wy }, cam);
            ctx.beginPath();
            ctx.arc(sp.x, sp.y, 1.2 * cam.zoom, 0, Math.PI * 2);
            ctx.fill();
        }
    }
}
// ─── Wires ─────────────────────────────────────────────────────────
function getPortWorldPos(nodeId, portIndex, isInput) {
    const node = (0, state_1.getNode)(nodeId);
    if (!node)
        return null;
    const count = isInput ? (0, state_1.getInputCount)(node.type) : (0, state_1.getOutputCount)(node.type);
    if (portIndex < 0 || portIndex >= count)
        return null;
    // Calculate total height of ports area
    const totalHeight = Math.max(count, 1) * constants_1.PORT_SPACING - constants_1.PORT_SPACING;
    const startY = node.y - totalHeight / 2 + constants_1.PORT_SPACING / 2;
    const x = isInput ? node.x - constants_1.NODE_WIDTH / 2 : node.x + constants_1.NODE_WIDTH / 2;
    const y = startY + portIndex * constants_1.PORT_SPACING;
    return { x, y };
}
function drawWire(fromPos, toPos, cam, energized, highlightColor) {
    const sp1 = worldToScreen(fromPos, cam);
    const sp2 = worldToScreen(toPos, cam);
    // Bezier control points (horizontal curve)
    const dx = Math.abs(sp2.x - sp1.x);
    const cpOffset = Math.max(dx * 0.4, 30);
    ctx.beginPath();
    ctx.moveTo(sp1.x, sp1.y);
    ctx.bezierCurveTo(sp1.x + cpOffset, sp1.y, sp2.x - cpOffset, sp2.y, sp2.x, sp2.y);
    const color = highlightColor ?? (energized ? constants_1.COLORS.wireOn : constants_1.COLORS.wireOff);
    ctx.strokeStyle = color;
    ctx.lineWidth = energized ? 3 : 2.5;
    if (energized) {
        ctx.shadowColor = constants_1.COLORS.wireOn;
        ctx.shadowBlur = 8 * cam.zoom;
    }
    else {
        ctx.shadowColor = 'transparent';
        ctx.shadowBlur = 0;
    }
    ctx.stroke();
    ctx.shadowBlur = 0; // reset
}
function drawWires(cam) {
    for (const wire of state_1.scene.wires) {
        const fromPos = getPortWorldPos(wire.fromId, wire.fromPort, false);
        const toPos = getPortWorldPos(wire.toId, wire.toPort, true);
        if (!fromPos || !toPos)
            continue;
        const srcNode = (0, state_1.getNode)(wire.fromId);
        const energized = !!srcNode?.outputState;
        drawWire(fromPos, toPos, cam, energized);
    }
}
// ─── Nodes ─────────────────────────────────────────────────────────
function drawNode(node, cam) {
    const hw = constants_1.NODE_WIDTH / 2 * cam.zoom;
    const hh = (constants_1.NODE_HEADER_HEIGHT + Math.max((0, state_1.getInputCount)(node.type), (0, state_1.getOutputCount)(node.type)) * constants_1.PORT_SPACING + 10) / 2 * cam.zoom;
    const sp = worldToScreen({ x: node.x, y: node.y }, cam);
    const rx = sp.x - hw;
    const ry = sp.y - hh;
    const rw = hw * 2;
    const rh = hh * 2;
    const r = 8 * cam.zoom; // corner radius
    // Node body
    ctx.beginPath();
    ctx.roundRect(rx, ry, rw, rh, r);
    // Tint based on output state
    if (node.outputState) {
        ctx.fillStyle = '#2a3540';
    }
    else {
        ctx.fillStyle = constants_1.COLORS.nodeBg;
    }
    ctx.fill();
    ctx.strokeStyle = constants_1.COLORS.nodeBorder;
    ctx.lineWidth = 1.5 * cam.zoom;
    ctx.stroke();
    // Header bar with node name
    const headerH = constants_1.NODE_HEADER_HEIGHT * cam.zoom;
    ctx.beginPath();
    ctx.roundRect(rx, ry, rw, headerH, [r, r, 0, 0]);
    ctx.fillStyle = '#2e3340';
    ctx.fill();
    // Node label
    const label = node.type.toUpperCase();
    ctx.fillStyle = constants_1.COLORS.textPrimary;
    ctx.font = `bold ${12 * cam.zoom}px monospace`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(label, sp.x, ry + headerH / 2);
    // Draw ports
    drawPorts(node, cam);
    // Special rendering for lamp and hex display
    if (node.type === 'lamp') {
        drawLamp(node, cam);
    }
    else if (node.type === 'hex') {
        drawHexDisplay(node, cam);
    }
    else if (node.type === 'toggle') {
        drawToggleState(node, cam);
    }
    else if (node.type === 'clock') {
        drawClockRate(node, cam);
    }
}
function drawPorts(node, cam) {
    const inputCount = (0, state_1.getInputCount)(node.type);
    const outputCount = (0, state_1.getOutputCount)(node.type);
    // Input ports (left side)
    for (let i = 0; i < inputCount; i++) {
        const pos = getPortWorldPos(node.id, i, true);
        const sp = worldToScreen(pos, cam);
        ctx.beginPath();
        ctx.arc(sp.x, sp.y, constants_1.PORT_RADIUS * cam.zoom, 0, Math.PI * 2);
        ctx.fillStyle = constants_1.COLORS.portInput;
        ctx.fill();
        ctx.strokeStyle = '#1a1d23';
        ctx.lineWidth = 1.5 * cam.zoom;
        ctx.stroke();
    }
    // Output ports (right side)
    for (let i = 0; i < outputCount; i++) {
        const pos = getPortWorldPos(node.id, i, false);
        const sp = worldToScreen(pos, cam);
        ctx.beginPath();
        ctx.arc(sp.x, sp.y, constants_1.PORT_RADIUS * cam.zoom, 0, Math.PI * 2);
        ctx.fillStyle = constants_1.COLORS.portOutput;
        ctx.fill();
        ctx.strokeStyle = '#1a1d23';
        ctx.lineWidth = 1.5 * cam.zoom;
        ctx.stroke();
    }
}
function drawLamp(node, cam) {
    const sp = worldToScreen({ x: node.x, y: node.y + 8 }, cam);
    const radius = 14 * cam.zoom;
    // Lamp glow
    if (node.outputState) {
        ctx.beginPath();
        ctx.arc(sp.x, sp.y, radius * 2.5, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(74, 255, 128, 0.15)';
        ctx.fill();
        ctx.beginPath();
        ctx.arc(sp.x, sp.y, radius * 1.6, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(74, 255, 128, 0.25)';
        ctx.fill();
    }
    // Lamp body
    ctx.beginPath();
    ctx.arc(sp.x, sp.y, radius, 0, Math.PI * 2);
    ctx.fillStyle = node.outputState ? constants_1.COLORS.lampOn : constants_1.COLORS.lampOff;
    ctx.fill();
    ctx.strokeStyle = '#1a1d23';
    ctx.lineWidth = 2 * cam.zoom;
    ctx.stroke();
    if (node.outputState) {
        ctx.shadowColor = constants_1.COLORS.wireOn;
        ctx.shadowBlur = 15 * cam.zoom;
        ctx.fill();
        ctx.shadowBlur = 0;
    }
}
function drawHexDisplay(node, cam) {
    // Get the 4 input bits (LSB at port 0, MSB at port 3)
    const bits = [];
    for (const w of state_1.scene.wires) {
        if (w.toId === node.id && w.toPort >= 0 && w.toPort <= 3) {
            const srcNode = (0, state_1.getNode)(w.fromId);
            bits[w.toPort] = srcNode?.outputState ?? false;
        }
    }
    // Calculate hex value: port 0 = LSB, port 3 = MSB
    let val = 0;
    for (let i = 0; i < 4; i++) {
        if (bits[i])
            val |= (1 << i);
    }
    const hexChars = '0123456789ABCDEF';
    const char = hexChars[val] || '?';
    const sp = worldToScreen({ x: node.x, y: node.y + 5 }, cam);
    ctx.fillStyle = constants_1.COLORS.wireOn;
    ctx.font = `bold ${24 * cam.zoom}px monospace`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    if (val > 0) {
        ctx.shadowColor = constants_1.COLORS.wireOn;
        ctx.shadowBlur = 10 * cam.zoom;
    }
    ctx.fillText(char, sp.x, sp.y);
    ctx.shadowBlur = 0;
}
function drawToggleState(node, cam) {
    const sp = worldToScreen({ x: node.x, y: node.y + 5 }, cam);
    ctx.fillStyle = constants_1.COLORS.textSecondary;
    ctx.font = `${10 * cam.zoom}px monospace`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(node.toggleOn ? 'ON' : 'OFF', sp.x, sp.y);
}
function drawClockRate(node, cam) {
    const sp = worldToScreen({ x: node.x, y: node.y + 5 }, cam);
    ctx.fillStyle = constants_1.COLORS.textSecondary;
    ctx.font = `${10 * cam.zoom}px monospace`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(`${node.clockRate ?? 1}Hz`, sp.x, sp.y);
}
function drawNodes(cam) {
    for (const node of state_1.scene.nodes) {
        drawNode(node, cam);
    }
}
// ─── Wire preview during drag ──────────────────────────────────────
function drawWirePreview(fromPos, cursorWorld, cam, cycleReject) {
    const sp1 = worldToScreen(fromPos, cam);
    const sp2 = worldToScreen(cursorWorld, cam);
    const dx = Math.abs(sp2.x - sp1.x);
    const cpOffset = Math.max(dx * 0.4, 30);
    ctx.beginPath();
    ctx.moveTo(sp1.x, sp1.y);
    ctx.bezierCurveTo(sp1.x + cpOffset, sp1.y, sp2.x - cpOffset, sp2.y, sp2.x, sp2.y);
    ctx.strokeStyle = cycleReject ? constants_1.COLORS.wireCycleReject : constants_1.COLORS.wireDragging;
    ctx.lineWidth = 3 * cam.zoom;
    ctx.setLineDash([6 * cam.zoom, 4 * cam.zoom]);
    ctx.stroke();
    ctx.setLineDash([]);
}
// ─── Hit testing ──────────────────────────────────────────────────
function hitTestPort(worldPos, cam) {
    const tolerance = Math.max(constants_1.PORT_HIT_TOLERANCE / cam.zoom, constants_1.PORT_RADIUS);
    for (const node of state_1.scene.nodes) {
        // Check input ports
        const inputCount = (0, state_1.getInputCount)(node.type);
        for (let i = 0; i < inputCount; i++) {
            const pos = getPortWorldPos(node.id, i, true);
            if (!pos)
                continue;
            const dx = worldPos.x - pos.x;
            const dy = worldPos.y - pos.y;
            if (dx * dx + dy * dy < tolerance * tolerance) {
                return { nodeId: node.id, portIndex: i, isInput: true };
            }
        }
        // Check output ports
        const outputCount = (0, state_1.getOutputCount)(node.type);
        for (let i = 0; i < outputCount; i++) {
            const pos = getPortWorldPos(node.id, i, false);
            if (!pos)
                continue;
            const dx = worldPos.x - pos.x;
            const dy = worldPos.y - pos.y;
            if (dx * dx + dy * dy < tolerance * tolerance) {
                return { nodeId: node.id, portIndex: i, isInput: false };
            }
        }
    }
    return null;
}
function hitTestNode(worldPos, cam) {
    for (let i = state_1.scene.nodes.length - 1; i >= 0; i--) {
        const node = state_1.scene.nodes[i];
        const hw = constants_1.NODE_WIDTH / 2;
        const portCount = Math.max((0, state_1.getInputCount)(node.type), (0, state_1.getOutputCount)(node.type));
        const hh = (constants_1.NODE_HEADER_HEIGHT + portCount * constants_1.PORT_SPACING + 10) / 2;
        if (worldPos.x >= node.x - hw && worldPos.x <= node.x + hw &&
            worldPos.y >= node.y - hh && worldPos.y <= node.y + hh) {
            return node.id;
        }
    }
    return null;
}
// ─── Selection highlight ──────────────────────────────────────────
function drawSelection(nodeId, cam) {
    const node = (0, state_1.getNode)(nodeId);
    if (!node)
        return;
    const hw = constants_1.NODE_WIDTH / 2 * cam.zoom + 3 * cam.zoom;
    const portCount = Math.max((0, state_1.getInputCount)(node.type), (0, state_1.getOutputCount)(node.type));
    const hh = (constants_1.NODE_HEADER_HEIGHT + portCount * constants_1.PORT_SPACING + 10) / 2 * cam.zoom + 3 * cam.zoom;
    const sp = worldToScreen({ x: node.x, y: node.y }, cam);
    ctx.strokeStyle = constants_1.COLORS.nodeSelected;
    ctx.lineWidth = 2 * cam.zoom;
    ctx.setLineDash([4 * cam.zoom, 4 * cam.zoom]);
    ctx.strokeRect(sp.x - hw, sp.y - hh, hw * 2, hh * 2);
    ctx.setLineDash([]);
}
};

// ── module: src/persistence.ts ──
__mods["src/persistence.ts"] = function (exports, require, module) {
"use strict";
// ─── localStorage Persistence ──────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.save = save;
exports.load = load;
const state_1 = require("./state");
const STORAGE_KEY = 'logic-circuit-editor-scene';
function save() {
    try {
        const data = (0, state_1.serializeScene)();
        localStorage.setItem(STORAGE_KEY, data);
    }
    catch (e) {
        // Storage full or unavailable — silently fail
    }
}
function load() {
    try {
        const data = localStorage.getItem(STORAGE_KEY);
        if (data) {
            (0, state_1.deserializeScene)(data);
            return true;
        }
    }
    catch (e) {
        // Corrupted data — ignore
    }
    return false;
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ─── Input Handling ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.lastCursorWorld = exports.selectedNodeId = void 0;
exports.getMode = getMode;
exports.getWireSource = getWireSource;
exports.setupInput = setupInput;
exports.setCycleReject = setCycleReject;
exports.isCycleReject = isCycleReject;
exports.clearCycleReject = clearCycleReject;
exports.createNodeAt = createNodeAt;
exports.setRenderingModule = setRenderingModule;
exports.setStateModule = setStateModule;
exports.setSaveFn = setSaveFn;
const constants_1 = require("./constants");
const state_1 = require("./state");
const evaluation_1 = require("./evaluation");
let state = { mode: 'idle' };
exports.selectedNodeId = null;
// Track cursor world position for wire preview
exports.lastCursorWorld = { x: 0, y: 0 };
// ─── Public API for the main loop to query ────────────────────────
function getMode() { return state.mode; }
function getWireSource() {
    if (state.mode === 'creatingWire' && state.wireFromId !== undefined) {
        return { nodeId: state.wireFromId, portIndex: state.wireFromPort };
    }
    return null;
}
// ─── Canvas event handlers ────────────────────────────────────────
function setupInput(canvas, cam) {
    canvas.addEventListener('mousedown', (e) => onMouseDown(e, cam));
    canvas.addEventListener('mousemove', (e) => onMouseMove(e, cam));
    canvas.addEventListener('mouseup', (e) => onMouseUp(e, cam));
    canvas.addEventListener('wheel', (e) => onWheel(e, cam), { passive: false });
    canvas.addEventListener('contextmenu', (e) => e.preventDefault());
    // Keyboard
    window.addEventListener('keydown', onKeyDown);
}
function getMouseWorldPos(e, cam) {
    const rect = e.target.getBoundingClientRect();
    const screenX = e.clientX - rect.left;
    const screenY = e.clientY - rect.top;
    return {
        x: screenX / cam.zoom + cam.worldX,
        y: screenY / cam.zoom + cam.worldY,
    };
}
function onMouseDown(e, cam) {
    if (e.button !== 0)
        return; // only left click
    const worldPos = getMouseWorldPos(e, cam);
    // Check port hit first (for wire creation)
    const portHit = importRendering().hitTestPort(worldPos, cam);
    if (portHit) {
        state.mode = 'creatingWire';
        state.wireFromId = portHit.nodeId;
        state.wireFromPort = portHit.portIndex;
        exports.selectedNodeId = null; // deselect when starting wire drag
        return;
    }
    // Check node hit (for dragging)
    const nodeId = importRendering().hitTestNode(worldPos, cam);
    if (nodeId) {
        state.mode = 'draggingNode';
        state.nodeId = nodeId;
        const node = (0, state_1.getNode)(nodeId);
        state.dragOffsetX = worldPos.x - node.x;
        state.dragOffsetY = worldPos.y - node.y;
        exports.selectedNodeId = nodeId;
        return;
    }
    // Empty space — start panning
    state.mode = 'panning';
    const rect = e.target.getBoundingClientRect();
    state.panStartScreenX = e.clientX - rect.left;
    state.panStartScreenY = e.clientY - rect.top;
    state.panStartWorldX = cam.worldX;
    state.panStartWorldY = cam.worldY;
    exports.selectedNodeId = null; // deselect on empty space click
}
function onMouseMove(e, cam) {
    const worldPos = getMouseWorldPos(e, cam);
    exports.lastCursorWorld = worldPos;
    switch (state.mode) {
        case 'panning': {
            const psx = state.panStartScreenX ?? 0;
            const psy = state.panStartScreenY ?? 0;
            const pwx = state.panStartWorldX ?? cam.worldX;
            const pwy = state.panStartWorldY ?? cam.worldY;
            const rect = e.target.getBoundingClientRect();
            const screenX = e.clientX - rect.left;
            const screenY = e.clientY - rect.top;
            cam.worldX = pwx - (screenX - psx) / cam.zoom;
            cam.worldY = pwy - (screenY - psy) / cam.zoom;
            break;
        }
        case 'draggingNode': {
            const dofx = state.dragOffsetX ?? 0;
            const dofy = state.dragOffsetY ?? 0;
            if (state.nodeId) {
                const node = (0, state_1.getNode)(state.nodeId);
                if (node) {
                    node.x = worldPos.x - dofx;
                    node.y = worldPos.y - dofy;
                }
            }
            break;
        }
        case 'creatingWire':
            // Wire preview is drawn in the main render loop
            break;
    }
}
function onMouseUp(e, cam) {
    const prevMode = state.mode;
    if (prevMode === 'draggingNode' && state.nodeId) {
        // Check for zero-distance click on toggle node
        const worldPos = getMouseWorldPos(e, cam);
        const nodeId = importRendering().hitTestNode(worldPos, cam);
        console.log('mouseup: prevMode=draggingNode, state.nodeId=' + state.nodeId + ', hitTestNode=' + nodeId + ', worldPos=' + JSON.stringify(worldPos));
        if (nodeId === state.nodeId) {
            const node = (0, state_1.getNode)(nodeId);
            console.log('same node, type=' + node?.type);
            if (node?.type === 'toggle') {
                (0, evaluation_1.toggleInput)(nodeId);
                saveNow();
                console.log('toggled!');
            }
        }
    }
    else if (prevMode === 'creatingWire' && state.wireFromId !== undefined) {
        // Try to connect to a port under cursor
        const worldPos = getMouseWorldPos(e, cam);
        const portHit = importRendering().hitTestPort(worldPos, cam);
        if (portHit) {
            const fromNode = (0, state_1.getNode)(state.wireFromId);
            const toNode = (0, state_1.getNode)(portHit.nodeId);
            // Don't connect a node to itself
            if (fromNode && toNode && fromNode.id !== toNode.id) {
                // Determine direction: output port → input port
                const fromIsOutput = state.wireFromPort < (0, state_1.getOutputCount)(fromNode.type);
                const toIsInput = portHit.portIndex < (0, state_1.getInputCount)(toNode.type);
                let validDir = false;
                let fnId = '';
                let fpIdx = -1;
                let tnId = '';
                let tpIdx = -1;
                if (fromIsOutput && toIsInput) {
                    fnId = state.wireFromId;
                    fpIdx = state.wireFromPort;
                    tnId = portHit.nodeId;
                    tpIdx = portHit.portIndex;
                    validDir = true;
                }
                else if (!fromIsOutput && !toIsInput) {
                    // Both are output ports — invalid, do nothing
                }
                else {
                    // Reverse: the "to" is actually the source
                    fnId = portHit.nodeId;
                    fpIdx = portHit.portIndex;
                    tnId = state.wireFromId;
                    tpIdx = state.wireFromPort;
                    validDir = true;
                }
                if (validDir) {
                    // Check for cycles
                    if ((0, state_1.wouldCreateCycle)(fnId, fpIdx, tnId, tpIdx)) {
                        setCycleReject(true);
                    }
                    else {
                        (0, state_1.addWire)(fnId, fpIdx, tnId, tpIdx);
                        (0, evaluation_1.evaluateDownstream)(fnId);
                        saveNow();
                    }
                }
            }
        }
    }
    state = { mode: 'idle' };
}
function onWheel(e, cam) {
    e.preventDefault();
    const rect = e.target.getBoundingClientRect();
    const screenX = e.clientX - rect.left;
    const screenY = e.clientY - rect.top;
    // World point under cursor before zoom
    const worldBeforeX = screenX / cam.zoom + cam.worldX;
    const worldBeforeY = screenY / cam.zoom + cam.worldY;
    // Apply zoom
    const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
    cam.zoom = Math.max(constants_1.ZOOM_MIN, Math.min(constants_1.ZOOM_MAX, cam.zoom * zoomFactor));
    // Adjust world origin so the point under cursor stays fixed
    cam.worldX = worldBeforeX - screenX / cam.zoom;
    cam.worldY = worldBeforeY - screenY / cam.zoom;
}
function onKeyDown(e) {
    if (e.key === 'Delete' || e.key === 'Backspace') {
        if (exports.selectedNodeId) {
            (0, state_1.removeNode)(exports.selectedNodeId);
            exports.selectedNodeId = null;
            saveNow();
        }
    }
}
// ─── Cycle reject visual feedback ────────────────────────────────
let cycleRejectActive = false;
function setCycleReject(v) { cycleRejectActive = v; }
function isCycleReject() { return cycleRejectActive; }
function clearCycleReject() { cycleRejectActive = false; }
// ─── Palette / node creation ──────────────────────────────────────
function createNodeAt(type, worldX, worldY) {
    const node = importState().createNode(type, worldX, worldY);
    (0, evaluation_1.evaluateDownstream)(node.id);
    saveNow();
}
// ─── Lazy imports to avoid circular deps ────────────────────────
let _renderingModule;
function importRendering() {
    if (!_renderingModule) {
        return window.__renderingModule;
    }
    return _renderingModule;
}
function setRenderingModule(mod) {
    _renderingModule = mod;
}
let _stateModule;
function importState() {
    if (!_stateModule) {
        return window.__stateModule;
    }
    return _stateModule;
}
function setStateModule(mod) {
    _stateModule = mod;
}
let _saveFn = null;
function saveNow() {
    if (_saveFn)
        _saveFn();
}
function setSaveFn(fn) {
    _saveFn = fn;
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>