← Node Graph Editor results

Node Graph Editor

Qwen 3.6 27b MTP · 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 71,377 bytes · SHA-256 fd0f2718bcdb
<!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;
  font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  background: #1a1d23;
  color: #d0d4dc;
}

body {
  display: flex;
}

/* ─── Sidebar ─────────────────────────────────────────────── */
#sidebar {
  width: 200px;
  min-width: 200px;
  background: #1e2128;
  border-right: 1px solid #2a2d35;
  display: flex;
  flex-direction: column;
  padding: 12px;
  overflow-y: auto;
  z-index: 10;
}

#sidebar-title {
  font-size: 14px;
  font-weight: 700;
  color: #8a9ab5;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  margin-bottom: 16px;
  padding-bottom: 8px;
  border-bottom: 1px solid #2a2d35;
}

.section-label {
  font-size: 10px;
  font-weight: 600;
  color: #5b6a80;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  margin-top: 14px;
  margin-bottom: 6px;
}

.node-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  margin-bottom: 3px;
  background: #252830;
  border: 1px solid #3a3d47;
  border-radius: 6px;
  color: #c0c4cc;
  font-size: 12px;
  cursor: grab;
  transition: all 0.15s ease;
  user-select: none;
}

.node-btn:hover {
  background: #2e3140;
  border-color: #4a9eff;
  color: #fff;
  transform: translateX(2px);
}

.node-btn:active {
  cursor: grabbing;
  transform: scale(0.97);
}

.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  background: #1a1d23;
  border-radius: 4px;
  font-size: 10px;
  color: #6b7080;
}

#sidebar-help {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid #2a2d35;
  font-size: 10px;
  color: #4a5060;
  line-height: 1.6;
}

#sidebar-help p {
  margin-bottom: 4px;
  color: #5b6a80;
}

#sidebar-help ul {
  list-style: none;
  padding-left: 0;
}

#sidebar-help li::before {
  content: '› ';
  color: #3a3d47;
}

/* ─── Canvas Container ────────────────────────────────────── */
#canvas-container {
  flex: 1;
  position: relative;
  overflow: hidden;
  min-width: 0;
}

#graph-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* ─── Zoom Indicator ──────────────────────────────────────── */
#zoom-indicator {
  position: absolute;
  bottom: 12px;
  right: 16px;
  background: rgba(30, 33, 40, 0.85);
  border: 1px solid #2a2d35;
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 11px;
  color: #5b6a80;
  pointer-events: none;
  transition: opacity 0.3s ease;
}

/* ─── Scrollbar Styling ───────────────────────────────────── */
#sidebar::-webkit-scrollbar {
  width: 4px;
}

#sidebar::-webkit-scrollbar-track {
  background: transparent;
}

#sidebar::-webkit-scrollbar-thumb {
  background: #3a3d47;
  border-radius: 2px;
}
</style>
</head>
<body>
<div id="sidebar">
  <div id="sidebar-title">Logic Circuit Editor</div>
  <div class="section-label">Inputs</div>
  <button class="node-btn" data-type="toggle"><span class="btn-icon">⏻</span> Toggle Switch</button>
  <button class="node-btn" data-type="clock"><span class="btn-icon">◷</span> Clock</button>
  <div class="section-label">Gates</div>
  <button class="node-btn" data-type="and"><span class="btn-icon">&amp;</span> AND</button>
  <button class="node-btn" data-type="or"><span class="btn-icon">≥1</span> OR</button>
  <button class="node-btn" data-type="not"><span class="btn-icon">!</span> NOT</button>
  <button class="node-btn" data-type="xor"><span class="btn-icon">=1</span> XOR</button>
  <button class="node-btn" data-type="nand"><span class="btn-icon">&amp;!</span> NAND</button>
  <button class="node-btn" data-type="nor"><span class="btn-icon">≥! </span> NOR</button>
  <div class="section-label">Outputs</div>
  <button class="node-btn" data-type="lamp"><span class="btn-icon">💡</span> Lamp</button>
  <button class="node-btn" data-type="hex"><span class="btn-icon">HEX</span> Hex Display</button>
  <div id="sidebar-help">
  <p><strong>Controls:</strong></p>
  <ul>
    <li>Click sidebar items to add nodes</li>
    <li>Drag sidebar items onto canvas</li>
    <li>Click toggle nodes to flip state</li>
    <li>Drag ports to create wires</li>
    <li>Delete key removes selected node</li>
    <li>Arrow keys move selected node</li>
    <li>Scroll to zoom, drag bg to pan</li>
  </ul>
</div>
</div>
<div id="canvas-container">
  <canvas id="graph-canvas"></canvas>
  <div id="zoom-indicator">100%</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./state":"src/state.ts","./rendering":"src/rendering.ts","./interaction":"src/interaction.ts","./persistence":"src/persistence.ts","./evaluation":"src/evaluation.ts"},"src/state.ts":{"./types":"src/types.ts"},"src/evaluation.ts":{"./types":"src/types.ts","./state":"src/state.ts"},"src/rendering.ts":{"./types":"src/types.ts","./state":"src/state.ts"},"src/interaction.ts":{"./types":"src/types.ts","./state":"src/state.ts","./rendering":"src/rendering.ts","./evaluation":"src/evaluation.ts"},"src/persistence.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";
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const rendering_1 = require("./rendering");
const interaction_1 = require("./interaction");
const persistence_1 = require("./persistence");
const evaluation_1 = require("./evaluation");
// ─── Initialization ──────────────────────────────────────────
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d', { alpha: false });
const zoomIndicator = document.getElementById('zoom-indicator');
// Load or create scene
let scene;
const saved = (0, persistence_1.loadScene)();
if (saved) {
    scene = saved;
}
else {
    scene = (0, state_1.createScene)();
}
// View transform
const view = { panX: 0, panY: 0, zoom: 1 };
// Create interaction manager
const interaction = new interaction_1.InteractionManager(canvas, scene, view);
// ─── Canvas Sizing ───────────────────────────────────────────
function resizeCanvas() {
    const container = canvas.parentElement;
    const dpr = window.devicePixelRatio || 1;
    canvas.width = container.clientWidth * dpr;
    canvas.height = container.clientHeight * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    // Store logical size for rendering
    canvas._logicalWidth = container.clientWidth;
    canvas._logicalHeight = container.clientHeight;
}
window.addEventListener('resize', () => {
    resizeCanvas();
    render();
});
// ─── Render Loop ─────────────────────────────────────────────
function render() {
    const w = canvas._logicalWidth || canvas.parentElement.clientWidth;
    const h = canvas._logicalHeight || canvas.parentElement.clientHeight;
    // Update hex displays before rendering
    interaction.updateHexDisplays();
    (0, rendering_1.renderScene)(ctx, scene, view, interaction.getSelectedNodeId(), interaction.getHoveredNodeId());
    // Draw wire preview if dragging
    interaction.drawWirePreview(ctx);
    // Update zoom indicator
    const pct = Math.round(view.zoom * 100);
    zoomIndicator.textContent = `${pct}%`;
}
// Set up render callback
interaction.onRender = () => requestAnimationFrame(render);
// ─── Clock Ticking System ────────────────────────────────────
const clockTimers = new Map();
function startClockTicking() {
    // Clear existing timers
    for (const [, timerId] of clockTimers) {
        clearInterval(timerId);
    }
    clockTimers.clear();
    // Set up individual timers per clock node
    for (const node of scene.nodes) {
        if (node.type === 'clock') {
            const rateIdx = node.clockRateIdx ?? 1;
            const rates = [200, 500, 1000, 2000];
            const rate = rates[rateIdx] || 500;
            const timerId = setInterval(() => {
                // Toggle clock state and re-evaluate downstream
                node.state = !node.state;
                interaction.updateHexDisplays();
                render();
                autoSave();
            }, rate);
            clockTimers.set(node.id, timerId);
        }
    }
}
// ─── Auto-Save ───────────────────────────────────────────────
let saveTimeout = null;
function autoSave() {
    if (saveTimeout)
        clearTimeout(saveTimeout);
    saveTimeout = window.setTimeout(() => {
        (0, persistence_1.saveScene)(scene);
    }, 500);
}
// ─── Sidebar Drag-and-Drop ──────────────────────────────────
const nodeButtons = document.querySelectorAll('.node-btn');
let dragNodeType = null;
for (const btn of nodeButtons) {
    const type = btn.getAttribute('data-type');
    // Click to add at center of viewport
    btn.addEventListener('click', () => {
        const container = canvas.parentElement;
        const cx = container.clientWidth / 2;
        const cy = container.clientHeight / 2;
        // Convert screen center to world coords, offset by pan/zoom
        const [wx, wy] = [(cx - view.panX) / view.zoom, (cy - view.panY) / view.zoom];
        // Center the node on the cursor position
        const nw = 130;
        const nh = 50;
        interaction.addNode(type, wx - nw / 2, wy - nh / 2);
        autoSave();
    });
    // Drag from sidebar to canvas
    btn.addEventListener('mousedown', (e) => {
        dragNodeType = type;
        e.preventDefault();
    });
}
// Handle drop on canvas from sidebar
canvas.addEventListener('mouseup', (e) => {
    if (!dragNodeType)
        return;
    const rect = canvas.getBoundingClientRect();
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;
    // Convert to world coords
    const [wx, wy] = [(sx - view.panX) / view.zoom, (sy - view.panY) / view.zoom];
    // Center the node on the drop position
    const nw = 130;
    const nh = 50;
    interaction.addNode(dragNodeType, wx - nw / 2, wy - nh / 2);
    dragNodeType = null;
    autoSave();
});
// Also handle mouseup on document to catch drops outside canvas
document.addEventListener('mouseup', () => {
    dragNodeType = null;
});
// ─── Toggle Node Click Handling (double-click on header) ─────
canvas.addEventListener('dblclick', (e) => {
    const rect = canvas.getBoundingClientRect();
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;
    // Check if double-clicking on a toggle node's body
    const [wx, wy] = [(sx - view.panX) / view.zoom, (sy - view.panY) / view.zoom];
    for (let i = scene.nodes.length - 1; i >= 0; i--) {
        const node = scene.nodes[i];
        if (node.type !== 'toggle')
            continue;
        // Check bounds (entire node body, not just header)
        const h = (0, state_1.getNodeHeight)(node.type);
        if (wx >= node.x && wx <= node.x + 130 && wy >= node.y && wy <= node.y + h) {
            // Double-clicked toggle - flip state
            node.state = !node.state;
            interaction.updateHexDisplays();
            render();
            autoSave();
            return;
        }
    }
    // Check clock double-click to cycle rate
    for (let i = scene.nodes.length - 1; i >= 0; i--) {
        const node = scene.nodes[i];
        if (node.type !== 'clock')
            continue;
        const h = (0, state_1.getNodeHeight)(node.type);
        if (wx >= node.x && wx <= node.x + 130 && wy >= node.y && wy <= node.y + h) {
            // Double-clicked clock - cycle rate
            const rates = [200, 500, 1000, 2000];
            node.clockRateIdx = ((node.clockRateIdx || 0) + 1) % rates.length;
            // Restart clock timers since rate changed
            startClockTicking();
            render();
            autoSave();
            return;
        }
    }
});
// ─── Initial Setup ──────────────────────────────────────────
resizeCanvas();
// Evaluate the scene on load (in case it was restored)
(0, evaluation_1.evaluateScene)(scene);
interaction.updateHexDisplays();
autoSave(); // Save evaluated states back to localStorage
// Start clock ticking for any loaded clocks
startClockTicking();
// Initial render
render();
// Save initial state if new
if (!saved) {
    autoSave();
}
console.log('Logic Circuit Editor initialized');
console.log(`Loaded ${scene.nodes.length} nodes, ${scene.wires.length} wires`);
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HEX_SEGMENTS = exports.GATE_FNS = exports.NODE_DEFS = exports.CONSTANTS = void 0;
// ─── Tunable Constants ────────────────────────────────────────────────
exports.CONSTANTS = {
    // Zoom
    ZOOM_MIN: 0.5,
    ZOOM_MAX: 2.0,
    ZOOM_STEP: 1.08,
    // Ports
    PORT_RADIUS: 7,
    PORT_HIT_TOLERANCE: 14, // in world coords; scaled by zoom for screen hit-testing
    // Nodes
    NODE_WIDTH: 130,
    NODE_HEIGHT: 50,
    NODE_HEADER_HEIGHT: 26,
    NODE_CORNER_RADIUS: 8,
    PORT_SPACING: 24,
    // Colors
    COLOR_BG: '#1a1d23',
    COLOR_GRID_DOT: 'rgba(255,255,255,0.06)',
    COLOR_NODE_BG: '#252830',
    COLOR_NODE_BORDER: '#3a3d47',
    COLOR_NODE_HEADER: '#2e3140',
    COLOR_NODE_SELECTED: '#4a9eff',
    COLOR_PORT_INPUT: '#5b8def',
    COLOR_PORT_OUTPUT: '#ef6c5b',
    COLOR_WIRE_OFF: 'rgba(100,120,160,0.35)',
    COLOR_WIRE_ON: '#4aeaff',
    COLOR_WIRE_GLOW: 'rgba(74,234,255,0.35)',
    COLOR_TEXT: '#d0d4dc',
    COLOR_TEXT_DIM: '#6b7080',
    COLOR_LAMP_OFF: '#3a3d47',
    COLOR_LAMP_ON: '#ffcc33',
    COLOR_LAMP_GLOW: 'rgba(255,204,51,0.4)',
    COLOR_CLOCK_PULSE: '#aa66ff',
    // Wire bezier control arm length (world coords)
    WIRE_ARM_LENGTH: 40,
    // Clock rates (ms per tick)
    CLOCK_RATES: [200, 500, 1000, 2000],
    // Animation
    ANIM_DURATION: 150, // ms for wire glow transitions
};
// ─── Node Definitions (ports, labels) ────────────────────────────────
exports.NODE_DEFS = {
    toggle: {
        label: 'Toggle',
        ports: [{ name: 'out', direction: 'out' }],
    },
    clock: {
        label: 'Clock',
        ports: [{ name: 'clk', direction: 'out' }],
    },
    and: {
        label: 'AND',
        ports: [
            { name: 'a', direction: 'in' },
            { name: 'b', direction: 'in' },
            { name: 'out', direction: 'out' },
        ],
    },
    or: {
        label: 'OR',
        ports: [
            { name: 'a', direction: 'in' },
            { name: 'b', direction: 'in' },
            { name: 'out', direction: 'out' },
        ],
    },
    not: {
        label: 'NOT',
        ports: [
            { name: 'a', direction: 'in' },
            { name: 'out', direction: 'out' },
        ],
    },
    xor: {
        label: 'XOR',
        ports: [
            { name: 'a', direction: 'in' },
            { name: 'b', direction: 'in' },
            { name: 'out', direction: 'out' },
        ],
    },
    nand: {
        label: 'NAND',
        ports: [
            { name: 'a', direction: 'in' },
            { name: 'b', direction: 'in' },
            { name: 'out', direction: 'out' },
        ],
    },
    nor: {
        label: 'NOR',
        ports: [
            { name: 'a', direction: 'in' },
            { name: 'b', direction: 'in' },
            { name: 'out', direction: 'out' },
        ],
    },
    lamp: {
        label: 'Lamp',
        ports: [{ name: 'in', direction: 'in' }],
    },
    hex: {
        label: 'Hex Display',
        ports: [
            { name: 'd0', direction: 'in' }, // LSB
            { name: 'd1', direction: 'in' },
            { name: 'd2', direction: 'in' },
            { name: 'd3', direction: 'in' }, // MSB
        ],
    },
};
exports.GATE_FNS = {
    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),
};
// ─── Hex Segment Map ────────────────────────────────────────────────
// Each hex digit maps to which segments are on (7-segment display)
// Segments: a(top), b(top-right), c(bot-right), d(bot), e(bot-left), f(top-left), g(mid)
exports.HEX_SEGMENTS = {
    0: [1, 1, 1, 1, 1, 1, 0],
    1: [0, 1, 1, 0, 0, 0, 0],
    2: [1, 1, 0, 1, 1, 0, 1],
    3: [1, 1, 1, 1, 0, 0, 1],
    4: [0, 1, 1, 0, 0, 1, 1],
    5: [1, 0, 1, 1, 0, 1, 1],
    6: [1, 0, 1, 1, 1, 1, 1],
    7: [1, 1, 1, 0, 0, 0, 0],
    8: [1, 1, 1, 1, 1, 1, 1],
    9: [1, 1, 1, 1, 0, 1, 1],
    10: [1, 1, 1, 0, 1, 1, 1], // A
    11: [0, 0, 1, 1, 1, 1, 1], // b
    12: [1, 0, 0, 1, 1, 1, 0], // C
    13: [0, 1, 1, 1, 1, 0, 1], // d
    14: [1, 0, 0, 1, 1, 1, 1], // E
    15: [1, 0, 0, 0, 1, 1, 1], // F
};
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createScene = createScene;
exports.newNode = newNode;
exports.addNode = addNode;
exports.removeNode = removeNode;
exports.removeWire = removeWire;
exports.addWire = addWire;
exports.getNodeHeight = getNodeHeight;
exports.getPortPosition = getPortPosition;
exports.getAllPortPositions = getAllPortPositions;
exports.isGate = isGate;
exports.isInputNode = isInputNode;
exports.getInputWire = getInputWire;
exports.getOutputWires = getOutputWires;
const types_1 = require("./types");
let idCounter = 1;
function createScene() {
    return { nodes: [], wires: [], nextId: idCounter };
}
function newNode(type, x, y) {
    const node = {
        id: `n${idCounter++}`,
        type,
        x,
        y,
        state: false,
    };
    if (type === 'clock') {
        node.clockRateIdx = 1; // default 500ms
    }
    return node;
}
function addNode(scene, type, x, y) {
    const node = newNode(type, x, y);
    scene.nodes.push(node);
    return node;
}
function removeNode(scene, nodeId) {
    scene.nodes = scene.nodes.filter(n => n.id !== nodeId);
    scene.wires = scene.wires.filter(w => w.fromNodeId !== nodeId && w.toNodeId !== nodeId);
}
function removeWire(scene, wireId) {
    scene.wires = scene.wires.filter(w => w.id !== wireId);
}
function addWire(scene, fromNodeId, fromPortName, toNodeId, toPortName) {
    // Remove existing wire on this input port (one wire per input)
    scene.wires = scene.wires.filter(w => !(w.toNodeId === toNodeId && w.toPortName === toPortName));
    const wire = {
        id: `w${idCounter++}`,
        fromNodeId,
        fromPortName,
        toNodeId,
        toPortName,
    };
    scene.wires.push(wire);
    return wire;
}
/** Get the world height of a node based on its port count */
function getNodeHeight(nodeType) {
    const def = types_1.NODE_DEFS[nodeType];
    if (!def)
        return types_1.CONSTANTS.NODE_HEIGHT;
    const inputPorts = def.ports.filter(p => p.direction === 'in');
    const outputPorts = def.ports.filter(p => p.direction === 'out');
    const maxPorts = Math.max(inputPorts.length, outputPorts.length);
    // Minimum height for header + at least one port row
    return types_1.CONSTANTS.NODE_HEADER_HEIGHT + Math.max(1, maxPorts) * types_1.CONSTANTS.PORT_SPACING - 4;
}
/** Get the world position of a port on a node */
function getPortPosition(node, portName) {
    const def = types_1.NODE_DEFS[node.type];
    if (!def)
        return null;
    const idx = def.ports.findIndex(p => p.name === portName);
    if (idx < 0)
        return null;
    const isInput = def.ports[idx].direction === 'in';
    // Group ports by direction and compute positions
    const sameDirPorts = def.ports.filter(p => p.direction === def.ports[idx].direction);
    const localIdx = sameDirPorts.findIndex(p => p.name === portName);
    const nodeH = getNodeHeight(node.type);
    const bodyH = nodeH - types_1.CONSTANTS.NODE_HEADER_HEIGHT;
    if (isInput) {
        // Input ports on left side, evenly distributed in body area
        const totalInputs = sameDirPorts.length;
        const spacing = totalInputs > 1 ? bodyH / (totalInputs + 0.5) : bodyH / 2;
        return {
            x: node.x,
            y: node.y + types_1.CONSTANTS.NODE_HEADER_HEIGHT + spacing * (localIdx + 0.5),
        };
    }
    else {
        // Output ports on right side, evenly distributed in body area
        const totalOutputs = sameDirPorts.length;
        const spacing = totalOutputs > 1 ? bodyH / (totalOutputs + 0.5) : bodyH / 2;
        return {
            x: node.x + types_1.CONSTANTS.NODE_WIDTH,
            y: node.y + types_1.CONSTANTS.NODE_HEADER_HEIGHT + spacing * (localIdx + 0.5),
        };
    }
}
/** Get all port positions for a node */
function getAllPortPositions(node) {
    const def = types_1.NODE_DEFS[node.type];
    if (!def)
        return new Map();
    const result = new Map();
    for (const port of def.ports) {
        const pos = getPortPosition(node, port.name);
        if (pos)
            result.set(port.name, pos);
    }
    return result;
}
/** Check if a node is a gate type */
function isGate(type) {
    return ['and', 'or', 'not', 'xor', 'nand', 'nor'].includes(type);
}
/** Check if a node is an input type (toggle, clock) */
function isInputNode(type) {
    return type === 'toggle' || type === 'clock';
}
/** Get the connected wire for an input port */
function getInputWire(scene, nodeId, portName) {
    return scene.wires.find(w => w.toNodeId === nodeId && w.toPortName === portName) || null;
}
/** Get all output wires from a node's output port */
function getOutputWires(scene, nodeId, portName) {
    return scene.wires.filter(w => w.fromNodeId === nodeId && w.fromPortName === portName);
}
};

// ── module: src/evaluation.ts ──
__mods["src/evaluation.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.topologicalSort = topologicalSort;
exports.wouldCreateCycle = wouldCreateCycle;
exports.evaluateScene = evaluateScene;
exports.evaluateDownstream = evaluateDownstream;
const types_1 = require("./types");
const state_1 = require("./state");
/**
 * Build a dependency graph and compute topological order.
 * Returns nodes in evaluation order (inputs first, then gates downstream).
 * Throws if a cycle is detected.
 */
function topologicalSort(scene) {
    const nodeMap = new Map();
    for (const n of scene.nodes)
        nodeMap.set(n.id, n);
    // Build adjacency: from -> [to] and in-degree count
    const adj = new Map();
    const inDegree = new Map();
    for (const n of scene.nodes) {
        adj.set(n.id, new Set());
        inDegree.set(n.id, 0);
    }
    for (const w of scene.wires) {
        if (!adj.has(w.fromNodeId) || !adj.has(w.toNodeId))
            continue;
        const target = w.toNodeId;
        adj.get(w.fromNodeId).add(target);
        inDegree.set(target, (inDegree.get(target) || 0) + 1);
    }
    // Kahn's algorithm
    const queue = [];
    for (const [id, deg] of inDegree) {
        if (deg === 0)
            queue.push(id);
    }
    const result = [];
    while (queue.length > 0) {
        const id = queue.shift();
        result.push(nodeMap.get(id));
        for (const neighbor of adj.get(id)) {
            inDegree.set(neighbor, inDegree.get(neighbor) - 1);
            if (inDegree.get(neighbor) === 0) {
                queue.push(neighbor);
            }
        }
    }
    // If not all nodes are in result, there's a cycle
    if (result.length !== scene.nodes.length) {
        throw new Error('Cycle detected in circuit');
    }
    return result;
}
/**
 * Check if adding a wire from `fromNodeId` to `toNodeId` would create a cycle.
 * Uses DFS from the target node to see if we can reach the source node.
 */
function wouldCreateCycle(scene, fromNodeId, toNodeId) {
    // Same node = self-loop
    if (fromNodeId === toNodeId)
        return true;
    // Build adjacency list including the proposed wire
    const adj = new Map();
    for (const n of scene.nodes) {
        adj.set(n.id, new Set());
    }
    for (const w of scene.wires) {
        if (adj.has(w.fromNodeId) && adj.has(w.toNodeId)) {
            adj.get(w.fromNodeId).add(w.toNodeId);
        }
    }
    // Add proposed wire
    if (adj.has(fromNodeId) && adj.has(toNodeId)) {
        adj.get(fromNodeId).add(toNodeId);
    }
    // DFS from toNodeId: can we reach fromNodeId?
    const visited = new Set();
    const stack = [toNodeId];
    while (stack.length > 0) {
        const current = stack.pop();
        if (current === fromNodeId)
            return true;
        if (visited.has(current))
            continue;
        visited.add(current);
        for (const neighbor of adj.get(current) || []) {
            if (!visited.has(neighbor)) {
                stack.push(neighbor);
            }
        }
    }
    return false;
}
/**
 * Evaluate the entire circuit in topological order.
 * Returns true if evaluation succeeded, false if there's a cycle.
 */
function evaluateScene(scene) {
    let order;
    try {
        order = topologicalSort(scene);
    }
    catch (e) {
        console.warn('Evaluation failed - cycle detected:', e);
        return false;
    }
    const nodeMap = new Map();
    for (const n of scene.nodes)
        nodeMap.set(n.id, n);
    // Collect input values per port: nodeId+portName -> value
    const portValues = new Map();
    for (const node of order) {
        const def = types_1.NODE_DEFS[node.type];
        if (!def)
            continue;
        // For gate nodes, gather inputs from connected wires and compute output
        if (types_1.GATE_FNS[node.type]) {
            const inputPorts = def.ports.filter(p => p.direction === 'in');
            const values = [];
            for (const port of inputPorts) {
                const wire = (0, state_1.getInputWire)(scene, node.id, port.name);
                if (wire) {
                    const srcNode = nodeMap.get(wire.fromNodeId);
                    if (!srcNode)
                        continue;
                    // Get the source node's output value
                    const key = `${wire.fromNodeId}:${wire.fromPortName}`;
                    values.push(portValues.get(key) || false);
                }
                else {
                    // Unconnected input defaults to false
                    values.push(false);
                }
            }
            const gateFn = types_1.GATE_FNS[node.type];
            node.state = gateFn(...values);
        }
        // For output nodes (lamp, hex), set state based on connected inputs
        if (node.type === 'lamp') {
            const wire = (0, state_1.getInputWire)(scene, node.id, 'in');
            if (wire) {
                const key = `${wire.fromNodeId}:${wire.fromPortName}`;
                node.state = portValues.get(key) || false;
            }
            else {
                node.state = false;
            }
        }
        // For hex display, compute value from 4 input bits
        if (node.type === 'hex') {
            let val = 0;
            const ports = ['d0', 'd1', 'd2', 'd3']; // LSB to MSB
            for (let i = 0; i < ports.length; i++) {
                const wire = (0, state_1.getInputWire)(scene, node.id, ports[i]);
                if (wire) {
                    const key = `${wire.fromNodeId}:${wire.fromPortName}`;
                    if (portValues.get(key))
                        val |= (1 << i);
                }
            }
            node.state = val > 0; // state is true if any bit is set
            node.label = val.toString(16).toUpperCase();
        }
        // Record output port values for downstream nodes
        const outputPorts = def.ports.filter(p => p.direction === 'out');
        for (const port of outputPorts) {
            const key = `${node.id}:${port.name}`;
            if (node.type === 'toggle' || node.type === 'clock') {
                // Input nodes: their state IS the output value
                portValues.set(key, node.state);
            }
            else if (types_1.GATE_FNS[node.type]) {
                // Gate nodes: output = computed state
                portValues.set(key, node.state);
            }
        }
    }
    return true;
}
/**
 * Re-evaluate only the downstream subgraph affected by a changed node.
 * More efficient than full re-evaluation for single changes.
 */
function evaluateDownstream(scene, startNodeId) {
    const nodeMap = new Map();
    for (const n of scene.nodes)
        nodeMap.set(n.id, n);
    // Build adjacency list
    const adj = new Map();
    for (const n of scene.nodes) {
        adj.set(n.id, new Set());
    }
    for (const w of scene.wires) {
        if (adj.has(w.fromNodeId) && adj.has(w.toNodeId)) {
            adj.get(w.fromNodeId).add(w.toNodeId);
        }
    }
    // BFS/DFS to find all downstream nodes in topological order
    const visited = new Set();
    const queue = [startNodeId];
    const evalOrder = [];
    while (queue.length > 0) {
        const id = queue.shift();
        if (visited.has(id))
            continue;
        visited.add(id);
        evalOrder.push(nodeMap.get(id));
        for (const neighbor of adj.get(id) || []) {
            if (!visited.has(neighbor)) {
                // Only add if all its predecessors are already visited or in queue
                const node = nodeMap.get(neighbor);
                if (node) {
                    const def = types_1.NODE_DEFS[node.type];
                    if (def) {
                        let allPredsVisited = true;
                        for (const port of def.ports.filter(p => p.direction === 'in')) {
                            const wire = (0, state_1.getInputWire)(scene, neighbor, port.name);
                            if (wire && !visited.has(wire.fromNodeId)) {
                                allPredsVisited = false;
                                break;
                            }
                        }
                        if (allPredsVisited) {
                            queue.push(neighbor);
                        }
                    }
                }
            }
        }
    }
    // Evaluate in order
    const portValues = new Map();
    for (const node of evalOrder) {
        const def = types_1.NODE_DEFS[node.type];
        if (!def)
            continue;
        if (types_1.GATE_FNS[node.type]) {
            const inputPorts = def.ports.filter(p => p.direction === 'in');
            const values = [];
            for (const port of inputPorts) {
                const wire = (0, state_1.getInputWire)(scene, node.id, port.name);
                if (wire) {
                    const srcNode = nodeMap.get(wire.fromNodeId);
                    if (!srcNode)
                        continue;
                    const key = `${wire.fromNodeId}:${wire.fromPortName}`;
                    values.push(portValues.get(key) || false);
                }
                else {
                    values.push(false);
                }
            }
            const gateFn = types_1.GATE_FNS[node.type];
            node.state = gateFn(...values);
        }
        // For output nodes (lamp, hex), set state based on connected inputs
        if (node.type === 'lamp') {
            const wire = (0, state_1.getInputWire)(scene, node.id, 'in');
            if (wire) {
                const key = `${wire.fromNodeId}:${wire.fromPortName}`;
                node.state = portValues.get(key) || false;
            }
            else {
                node.state = false;
            }
        }
        // For hex display, compute value from 4 input bits
        if (node.type === 'hex') {
            let val = 0;
            const ports = ['d0', 'd1', 'd2', 'd3']; // LSB to MSB
            for (let i = 0; i < ports.length; i++) {
                const wire = (0, state_1.getInputWire)(scene, node.id, ports[i]);
                if (wire) {
                    const key = `${wire.fromNodeId}:${wire.fromPortName}`;
                    if (portValues.get(key))
                        val |= (1 << i);
                }
            }
            node.state = val > 0;
            node.label = val.toString(16).toUpperCase();
        }
        // Record outputs
        const outputPorts = def.ports.filter(p => p.direction === 'out');
        for (const port of outputPorts) {
            const key = `${node.id}:${port.name}`;
            if (node.type === 'toggle' || node.type === 'clock') {
                portValues.set(key, node.state);
            }
            else if (types_1.GATE_FNS[node.type]) {
                portValues.set(key, node.state);
            }
        }
    }
    return true;
}
};

// ── module: src/rendering.ts ──
__mods["src/rendering.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.worldToScreen = worldToScreen;
exports.screenToWorld = screenToWorld;
exports.drawGrid = drawGrid;
exports.drawWires = drawWires;
exports.drawWirePreview = drawWirePreview;
exports.drawNode = drawNode;
exports.roundRect = roundRect;
exports.hitTestPorts = hitTestPorts;
exports.hitTestNode = hitTestNode;
exports.renderScene = renderScene;
const types_1 = require("./types");
const state_1 = require("./state");
// ─── Coordinate Transforms ──────────────────────────────────────────
function worldToScreen(wx, wy, v) {
    return [wx * v.zoom + v.panX, wy * v.zoom + v.panY];
}
function screenToWorld(sx, sy, v) {
    return [(sx - v.panX) / v.zoom, (sy - v.panY) / v.zoom];
}
// ─── Grid Rendering ────────────────────────────────────────────────
function drawGrid(ctx, w, h, v) {
    const spacing = types_1.CONSTANTS.NODE_WIDTH; // grid dot spacing in world coords
    const [startWX, startWY] = screenToWorld(0, 0, v);
    const [endWX, endWY] = screenToWorld(w, h, v);
    ctx.fillStyle = types_1.CONSTANTS.COLOR_GRID_DOT;
    // Calculate visible range with padding
    const pad = spacing * 2;
    const startX = Math.floor((startWX - pad) / spacing) * spacing;
    const startY = Math.floor((startWY - pad) / spacing) * spacing;
    const endX = endWX + pad;
    const endY = endWY + pad;
    for (let wx = startX; wx <= endX; wx += spacing) {
        for (let wy = startY; wy <= endY; wy += spacing) {
            const [sx, sy] = worldToScreen(wx, wy, v);
            ctx.beginPath();
            ctx.arc(sx, sy, Math.max(1.5, 2 * v.zoom), 0, Math.PI * 2);
            ctx.fill();
        }
    }
}
function drawWireCurve(ctx, fromX, fromY, toX, toY, energized, v, isPreview = false) {
    // Adaptive control arm length based on distance between nodes
    const dx = Math.abs(toX - fromX);
    const minArm = types_1.CONSTANTS.WIRE_ARM_LENGTH * v.zoom;
    const maxArm = dx * 0.45; // Don't let arms exceed 45% of total distance
    const armLen = Math.max(minArm, Math.min(maxArm, (dx / 2) * 0.6));
    // Bezier control points - horizontal curves
    const cp1x = fromX + armLen;
    const cp1y = fromY;
    const cp2x = toX - armLen;
    const cp2y = toY;
    ctx.beginPath();
    ctx.moveTo(fromX, fromY);
    ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, toX, toY);
    if (energized && !isPreview) {
        // Outer glow layer
        ctx.save();
        ctx.strokeStyle = types_1.CONSTANTS.COLOR_WIRE_GLOW;
        ctx.lineWidth = Math.max(4, 8 * v.zoom);
        ctx.lineCap = 'round';
        ctx.stroke();
        ctx.restore();
        // Inner bright line
        ctx.strokeStyle = types_1.CONSTANTS.COLOR_WIRE_ON;
        ctx.lineWidth = Math.max(2, 3 * v.zoom);
        ctx.lineCap = 'round';
        ctx.stroke();
        // Core highlight
        ctx.strokeStyle = 'rgba(255,255,255,0.3)';
        ctx.lineWidth = Math.max(1, 1.5 * v.zoom);
        ctx.stroke();
    }
    else if (isPreview) {
        ctx.setLineDash([6 * v.zoom, 4 * v.zoom]);
        ctx.strokeStyle = energized ? types_1.CONSTANTS.COLOR_WIRE_ON : 'rgba(100,120,160,0.5)';
        ctx.lineWidth = Math.max(1.5, 2 * v.zoom);
        ctx.stroke();
        ctx.setLineDash([]);
    }
    else if (!isPreview) {
        // Dim wire for de-energized signals
        ctx.strokeStyle = types_1.CONSTANTS.COLOR_WIRE_OFF;
        ctx.lineWidth = Math.max(1.5, 2 * v.zoom);
        ctx.lineCap = 'round';
        ctx.stroke();
    }
}
function drawWires(ctx, scene, v) {
    const nodeMap = new Map();
    for (const n of scene.nodes)
        nodeMap.set(n.id, n);
    for (const wire of scene.wires) {
        const fromNode = nodeMap.get(wire.fromNodeId);
        const toNode = nodeMap.get(wire.toNodeId);
        if (!fromNode || !toNode)
            continue;
        const fromPos = (0, state_1.getPortPosition)(fromNode, wire.fromPortName);
        const toPos = (0, state_1.getPortPosition)(toNode, wire.toPortName);
        if (!fromPos || !toPos)
            continue;
        // Determine if energized: source node's output is true
        const energized = fromNode.state === true;
        const [sx1, sy1] = worldToScreen(fromPos.x, fromPos.y, v);
        const [sx2, sy2] = worldToScreen(toPos.x, toPos.y, v);
        drawWireCurve(ctx, sx1, sy1, sx2, sy2, energized, v);
    }
}
function drawWirePreview(ctx, fromX, fromY, toX, toY, energized, v) {
    const [sx1, sy1] = worldToScreen(fromX, fromY, v);
    const [sx2, sy2] = worldToScreen(toX, toY, v);
    drawWireCurve(ctx, sx1, sy1, sx2, sy2, energized, v, true);
}
// ─── Node Rendering ────────────────────────────────────────────────
function drawNode(ctx, node, v, isSelected, isHovered) {
    const def = types_1.NODE_DEFS[node.type];
    if (!def)
        return;
    const [sx, sy] = worldToScreen(node.x, node.y, v);
    const nw = types_1.CONSTANTS.NODE_WIDTH * v.zoom;
    const nh = (0, state_1.getNodeHeight)(node.type) * v.zoom;
    const headerH = types_1.CONSTANTS.NODE_HEADER_HEIGHT * v.zoom;
    const r = types_1.CONSTANTS.NODE_CORNER_RADIUS * v.zoom;
    // Node body shadow
    ctx.save();
    if (isHovered) {
        ctx.shadowColor = 'rgba(0,0,0,0.4)';
        ctx.shadowBlur = 12 * v.zoom;
        ctx.shadowOffsetY = 4 * v.zoom;
    }
    // Background
    ctx.fillStyle = types_1.CONSTANTS.COLOR_NODE_BG;
    roundRect(ctx, sx, sy, nw, nh, r);
    ctx.fill();
    // Border
    ctx.strokeStyle = isSelected ? types_1.CONSTANTS.COLOR_NODE_SELECTED : types_1.CONSTANTS.COLOR_NODE_BORDER;
    ctx.lineWidth = isSelected ? Math.max(2, 2.5 * v.zoom) : Math.max(1, 1.5 * v.zoom);
    roundRect(ctx, sx, sy, nw, nh, r);
    ctx.stroke();
    // Header bar
    ctx.fillStyle = types_1.CONSTANTS.COLOR_NODE_HEADER;
    ctx.beginPath();
    ctx.moveTo(sx + r, sy);
    ctx.lineTo(sx + nw - r, sy);
    ctx.arcTo(sx + nw, sy, sx + nw, sy + r, r);
    ctx.lineTo(sx + nw, sy + headerH);
    ctx.lineTo(sx, sy + headerH);
    ctx.lineTo(sx, sy + r);
    ctx.arcTo(sx, sy, sx + r, sy, r);
    ctx.closePath();
    ctx.fill();
    // State tint overlay for gates/inputs
    if (node.state && types_1.GATE_FNS[node.type]) {
        ctx.fillStyle = 'rgba(74,234,255,0.08)';
        roundRect(ctx, sx + 1, sy + headerH + 1, nw - 2, nh - headerH - 2, Math.max(0, r - 1));
        ctx.fill();
    }
    ctx.restore();
    // Node label
    const fontSize = Math.max(11, 13 * v.zoom);
    ctx.font = `bold ${fontSize}px 'Segoe UI', system-ui, sans-serif`;
    ctx.fillStyle = types_1.CONSTANTS.COLOR_TEXT;
    ctx.textAlign = 'left';
    ctx.textBaseline = 'middle';
    ctx.fillText(def.label, sx + 8 * v.zoom, sy + headerH / 2);
    // State indicator text (for gates and inputs)
    if (types_1.GATE_FNS[node.type] || node.type === 'toggle' || node.type === 'clock') {
        const stateText = node.state ? 'ON' : 'OFF';
        ctx.font = `${Math.max(9, 10 * v.zoom)}px 'Segoe UI', system-ui, sans-serif`;
        ctx.fillStyle = node.state ? types_1.CONSTANTS.COLOR_WIRE_ON : types_1.CONSTANTS.COLOR_TEXT_DIM;
        ctx.textAlign = 'right';
        ctx.fillText(stateText, sx + nw - 8 * v.zoom, sy + headerH / 2);
    }
    // Draw ports
    const portPositions = (0, state_1.getAllPortPositions)(node);
    for (const [portName, pos] of portPositions) {
        const portDef = def.ports.find(p => p.name === portName);
        const [px, py] = worldToScreen(pos.x, pos.y, v);
        const radius = types_1.CONSTANTS.PORT_RADIUS * v.zoom;
        // Port circle
        ctx.beginPath();
        ctx.arc(px, py, radius, 0, Math.PI * 2);
        ctx.fillStyle = portDef.direction === 'in' ? types_1.CONSTANTS.COLOR_PORT_INPUT : types_1.CONSTANTS.COLOR_PORT_OUTPUT;
        ctx.fill();
        // Port border
        ctx.strokeStyle = '#1a1d23';
        ctx.lineWidth = Math.max(1.5, 2 * v.zoom);
        ctx.stroke();
        // Check if port has a wire connected (for input ports)
        if (portDef.direction === 'in') {
            const hasWire = false; // Will be checked by caller
            if (hasWire) {
                ctx.beginPath();
                ctx.arc(px, py, radius * 0.5, 0, Math.PI * 2);
                ctx.fillStyle = '#fff';
                ctx.fill();
            }
        }
    }
    // Special rendering for Lamp node
    if (node.type === 'lamp') {
        const lampX = sx + nw / 2;
        const lampY = sy + headerH + (nh - headerH) / 2;
        const lampR = Math.min(nw, nh - headerH) * 0.35;
        ctx.save();
        if (node.state) {
            ctx.shadowColor = types_1.CONSTANTS.COLOR_LAMP_GLOW;
            ctx.shadowBlur = 20 * v.zoom;
        }
        ctx.beginPath();
        ctx.arc(lampX, lampY, lampR, 0, Math.PI * 2);
        ctx.fillStyle = node.state ? types_1.CONSTANTS.COLOR_LAMP_ON : types_1.CONSTANTS.COLOR_LAMP_OFF;
        ctx.fill();
        ctx.strokeStyle = '#1a1d23';
        ctx.lineWidth = Math.max(1.5, 2 * v.zoom);
        ctx.stroke();
        ctx.restore();
        // Inner glow for lamp when on
        if (node.state) {
            const grad = ctx.createRadialGradient(lampX, lampY, 0, lampX, lampY, lampR);
            grad.addColorStop(0, 'rgba(255,255,255,0.4)');
            grad.addColorStop(1, 'rgba(255,204,51,0)');
            ctx.beginPath();
            ctx.arc(lampX, lampY, lampR, 0, Math.PI * 2);
            ctx.fillStyle = grad;
            ctx.fill();
        }
    }
    // Special rendering for Hex Display node
    if (node.type === 'hex') {
        drawHexDisplay(ctx, node, sx, sy, nw, nh, headerH, v);
    }
    // Clock rate indicator
    if (node.type === 'clock' && node.clockRateIdx !== undefined) {
        const rate = types_1.CONSTANTS.CLOCK_RATES[node.clockRateIdx] || 500;
        ctx.font = `${Math.max(8, 9 * v.zoom)}px monospace`;
        ctx.fillStyle = types_1.CONSTANTS.COLOR_TEXT_DIM;
        ctx.textAlign = 'center';
        ctx.fillText(`${rate}ms`, sx + nw / 2, sy + nh - 4 * v.zoom);
    }
}
function drawHexDisplay(ctx, node, sx, sy, nw, nh, headerH, v) {
    const hexVal = parseInt(node.label || '0', 16) || 0;
    const segments = types_1.HEX_SEGMENTS[hexVal] || [0, 0, 0, 0, 0, 0, 0];
    // Display area within node body
    const padX = nw * 0.12;
    const padY = (nh - headerH) * 0.15;
    const dispW = nw - padX * 2;
    const dispH = nh - headerH - padY * 2;
    const dx = sx + padX;
    const dy = sy + headerH + padY;
    // Segment dimensions (proportional to display area)
    const segThick = Math.min(dispW, dispH) * 0.1;
    const gap = segThick * 0.5;
    // Define segment positions: a(top), b(tr), c(br), d(bot), e(bl), f(tl), g(mid)
    const halfW = dispW / 2;
    const midY = dy + dispH / 2;
    const topY = dy + gap;
    const botY = dy + dispH - segThick - gap;
    // Horizontal segments (a, d, g)
    const hSegs = [
        { x: dx + halfW / 4, y: topY, w: dispW * 0.75, h: segThick }, // a (top)
        { x: dx + halfW / 4, y: midY - segThick / 2, w: dispW * 0.75, h: segThick }, // g (mid)
        { x: dx + halfW / 4, y: botY, w: dispW * 0.75, h: segThick }, // d (bot)
    ];
    // Vertical segments (f=tl, b=tr, e=bl, c=br)
    const vSegH = (dispH - gap * 2 - segThick) / 2;
    const vSegs = [
        { x: dx + halfW / 4, y: topY, w: segThick, h: vSegH }, // f (top-left)
        { x: dx + dispW - halfW / 4 - segThick, y: topY, w: segThick, h: vSegH }, // b (top-right)
        { x: dx + halfW / 4, y: midY + segThick / 2, w: segThick, h: vSegH }, // e (bot-left)
        { x: dx + dispW - halfW / 4 - segThick, y: midY + segThick / 2, w: segThick, h: vSegH }, // c (bot-right)
    ];
    const allSegs = [...hSegs.slice(0), ...vSegs.slice(0), hSegs[1]]; // a,f,b,e,c,g,d order → segments[a,b,c,d,e,f,g]
    // Map to segment indices: a=0, b=1, c=2, d=3, e=4, f=5, g=6
    const segMap = [hSegs[0], vSegs[1], vSegs[3], hSegs[2], vSegs[2], vSegs[0], hSegs[1]];
    for (let i = 0; i < 7; i++) {
        const s = segMap[i];
        const isOn = segments[i] === 1;
        ctx.save();
        if (isOn) {
            ctx.shadowColor = types_1.CONSTANTS.COLOR_LAMP_GLOW;
            ctx.shadowBlur = 6 * v.zoom;
        }
        ctx.fillStyle = isOn ? types_1.CONSTANTS.COLOR_LAMP_ON : 'rgba(50,55,70,0.6)';
        const r = segThick / 2;
        roundRect(ctx, s.x, s.y, s.w, s.h, r);
        ctx.fill();
        // Add subtle border to segments
        ctx.strokeStyle = 'rgba(30,33,40,0.8)';
        ctx.lineWidth = Math.max(0.5, v.zoom * 0.5);
        roundRect(ctx, s.x, s.y, s.w, s.h, r);
        ctx.stroke();
        ctx.restore();
    }
    // Hex character label below display
    const hexChar = hexVal.toString(16).toUpperCase();
    ctx.font = `bold ${Math.max(9, 11 * v.zoom)}px monospace`;
    ctx.fillStyle = hexVal > 0 ? types_1.CONSTANTS.COLOR_LAMP_ON : types_1.CONSTANTS.COLOR_TEXT_DIM;
    ctx.textAlign = 'center';
    ctx.fillText(hexChar, sx + nw / 2, sy + nh - 3 * v.zoom);
}
// ─── Utility: Rounded Rectangle ─────────────────────────────────────
function roundRect(ctx, x, y, w, h, r) {
    r = Math.min(r, w / 2, h / 2);
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y);
    ctx.arcTo(x + w, y, x + w, y + r, r);
    ctx.lineTo(x + w, y + h - r);
    ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
    ctx.lineTo(x + r, y + h);
    ctx.arcTo(x, y + h, x, y + h - r, r);
    ctx.lineTo(x, y + r);
    ctx.arcTo(x, y, x + r, y, r);
    ctx.closePath();
}
function hitTestPorts(scene, screenX, screenY, v, direction) {
    const [wx, wy] = screenToWorld(screenX, screenY, v);
    const hitRadius = types_1.CONSTANTS.PORT_HIT_TOLERANCE / v.zoom; // scale up at low zoom
    let best = null;
    let bestDist = Infinity;
    for (const node of scene.nodes) {
        const def = types_1.NODE_DEFS[node.type];
        if (!def)
            continue;
        for (const port of def.ports) {
            if (direction && port.direction !== direction)
                continue;
            const pos = (0, state_1.getPortPosition)(node, port.name);
            if (!pos)
                continue;
            const dx = wx - pos.x;
            const dy = wy - pos.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist < hitRadius && dist < bestDist) {
                bestDist = dist;
                best = { nodeId: node.id, portName: port.name, worldX: pos.x, worldY: pos.y };
            }
        }
    }
    return best;
}
// ─── Node Hit Testing ──────────────────────────────────────────────
function hitTestNode(scene, screenX, screenY, v) {
    const [wx, wy] = screenToWorld(screenX, screenY, v);
    // Check nodes in reverse order (top-most first)
    for (let i = scene.nodes.length - 1; i >= 0; i--) {
        const node = scene.nodes[i];
        const h = (0, state_1.getNodeHeight)(node.type);
        if (wx >= node.x &&
            wx <= node.x + types_1.CONSTANTS.NODE_WIDTH &&
            wy >= node.y &&
            wy <= node.y + h) {
            return node;
        }
    }
    return null;
}
// ─── Full Scene Render ──────────────────────────────────────────────
function renderScene(ctx, scene, v, selectedNodeId, hoveredNodeId) {
    const w = ctx.canvas.width;
    const h = ctx.canvas.height;
    // Clear background
    ctx.fillStyle = types_1.CONSTANTS.COLOR_BG;
    ctx.fillRect(0, 0, w, h);
    // Draw grid
    drawGrid(ctx, w, h, v);
    // Draw wires (behind nodes)
    drawWires(ctx, scene, v);
    // Draw nodes
    for (const node of scene.nodes) {
        drawNode(ctx, node, v, node.id === selectedNodeId, node.id === hoveredNodeId);
    }
}
};

// ── module: src/interaction.ts ──
__mods["src/interaction.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InteractionManager = void 0;
const types_1 = require("./types");
const state_1 = require("./state");
const rendering_1 = require("./rendering");
const evaluation_1 = require("./evaluation");
class InteractionManager {
    constructor(canvas, scene, view) {
        this.drag = { type: 'none' };
        this.selectedNodeId = null;
        this.hoveredNodeId = null;
        this.mouseDownPos = null;
        this.canvas = canvas;
        this.scene = scene;
        this.view = view;
        this.setupEvents();
    }
    setupEvents() {
        this.canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
        this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
        this.canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
        this.canvas.addEventListener('wheel', (e) => this.onWheel(e), { passive: false });
        this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
        window.addEventListener('keydown', (e) => this.onKeyDown(e));
    }
    onMouseDown(e) {
        const rect = this.canvas.getBoundingClientRect();
        const sx = e.clientX - rect.left;
        const sy = e.clientY - rect.top;
        // Track mousedown position for click detection
        this.mouseDownPos = [sx, sy];
        // Right-click: open palette
        if (e.button === 2) {
            return;
        }
        // Check port hit first (for wire creation)
        const portHit = (0, rendering_1.hitTestPorts)(this.scene, sx, sy, this.view);
        if (portHit) {
            const node = this.scene.nodes.find(n => n.id === portHit.nodeId);
            if (!node)
                return;
            const def = types_1.NODE_DEFS[node.type];
            const portDef = def.ports.find(p => p.name === portHit.portName);
            if (portDef.direction === 'out') {
                // Start dragging wire from output port
                this.drag = {
                    type: 'create-wire',
                    fromNodeId: node.id,
                    fromPortName: portHit.portName,
                    fromWorldX: portHit.worldX,
                    fromWorldY: portHit.worldY,
                    toScreenX: sx,
                    toScreenY: sy,
                };
            }
            else {
                // Dragging from input port: pick up existing wire and re-route
                const existingWire = (0, state_1.getInputWire)(this.scene, node.id, portHit.portName);
                if (existingWire) {
                    (0, state_1.removeWire)(this.scene, existingWire.id);
                    const srcNode = this.scene.nodes.find(n => n.id === existingWire.fromNodeId);
                    if (!srcNode)
                        return;
                    const srcPos = (0, state_1.getPortPosition)(srcNode, existingWire.fromPortName);
                    if (!srcPos)
                        return;
                    this.drag = {
                        type: 'create-wire',
                        fromNodeId: existingWire.fromNodeId,
                        fromPortName: existingWire.fromPortName,
                        fromWorldX: srcPos.x,
                        fromWorldY: srcPos.y,
                        toScreenX: sx,
                        toScreenY: sy,
                    };
                }
            }
            this.onRender?.();
            return;
        }
        // Check node hit (for dragging nodes)
        const nodeHit = (0, rendering_1.hitTestNode)(this.scene, sx, sy, this.view);
        if (nodeHit) {
            const [wx, wy] = (0, rendering_1.screenToWorld)(sx, sy, this.view);
            this.drag = {
                type: 'drag-node',
                nodeId: nodeHit.id,
                offsetX: wx - nodeHit.x,
                offsetY: wy - nodeHit.y,
            };
            this.selectedNodeId = nodeHit.id;
            this.onSelectChange?.(this.selectedNodeId);
            this.onRender?.();
            return;
        }
        // Click on empty space: start panning
        this.drag = { type: 'pan' };
        this.selectedNodeId = null;
        this.onSelectChange?.(null);
        this.onRender?.();
    }
    onMouseMove(e) {
        const rect = this.canvas.getBoundingClientRect();
        const sx = e.clientX - rect.left;
        const sy = e.clientY - rect.top;
        if (this.drag.type === 'pan') {
            this.view.panX += e.movementX;
            this.view.panY += e.movementY;
            this.onRender?.();
            return;
        }
        if (this.drag.type === 'drag-node' && this.drag.nodeId) {
            const [wx, wy] = (0, rendering_1.screenToWorld)(sx, sy, this.view);
            const node = this.scene.nodes.find(n => n.id === this.drag.nodeId);
            if (node) {
                node.x = wx - (this.drag.offsetX || 0);
                node.y = wy - (this.drag.offsetY || 0);
                // Snap to grid-ish positions for cleaner layout
                const snap = 5;
                node.x = Math.round(node.x / snap) * snap;
                node.y = Math.round(node.y / snap) * snap;
            }
            this.onRender?.();
            return;
        }
        if (this.drag.type === 'create-wire' || this.drag.type === 'cycle-rejected') {
            this.drag.toScreenX = sx;
            this.drag.toScreenY = sy;
            this.onRender?.();
            return;
        }
        // Hover detection when not dragging
        if (this.drag.type === 'none') {
            const nodeHit = (0, rendering_1.hitTestNode)(this.scene, sx, sy, this.view);
            const newHoveredId = nodeHit ? nodeHit.id : null;
            if (newHoveredId !== this.hoveredNodeId) {
                this.hoveredNodeId = newHoveredId;
                this.canvas.style.cursor = nodeHit ? 'move' : 'default';
                this.onRender?.();
            }
            // Check port hover for cursor change
            const portHit = (0, rendering_1.hitTestPorts)(this.scene, sx, sy, this.view);
            if (portHit && !nodeHit) {
                this.canvas.style.cursor = 'crosshair';
            }
            else if (!nodeHit) {
                this.canvas.style.cursor = 'default';
            }
        }
    }
    onMouseUp(e) {
        const rect = this.canvas.getBoundingClientRect();
        const sx = e.clientX - rect.left;
        const sy = e.clientY - rect.top;
        // Detect click (minimal movement from mousedown position)
        if (this.mouseDownPos && this.drag.type === 'drag-node') {
            const [mdx, mdy] = this.mouseDownPos;
            const dx = sx - mdx;
            const dy = sy - mdy;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist < 5) {
                // This was a click, not a drag
                const [wx, wy] = (0, rendering_1.screenToWorld)(sx, sy, this.view);
                for (let i = this.scene.nodes.length - 1; i >= 0; i--) {
                    const node = this.scene.nodes[i];
                    if (node.type === 'toggle') {
                        // Check bounds (entire node body)
                        const h = (0, state_1.getNodeHeight)(node.type);
                        if (wx >= node.x && wx <= node.x + types_1.CONSTANTS.NODE_WIDTH && wy >= node.y && wy <= node.y + h) {
                            node.state = !node.state;
                            (0, evaluation_1.evaluateDownstream)(this.scene, node.id);
                            this.onRender?.();
                            return; // Don't process further
                        }
                    }
                }
            }
        }
        if (this.drag.type === 'create-wire' || this.drag.type === 'cycle-rejected') {
            // Check if we're over a valid input port
            const targetPort = (0, rendering_1.hitTestPorts)(this.scene, sx, sy, this.view, 'in');
            if (targetPort && this.drag.fromNodeId) {
                // Don't connect to same node's output
                if (targetPort.nodeId !== this.drag.fromNodeId) {
                    // Check for cycles
                    const cycle = (0, evaluation_1.wouldCreateCycle)(this.scene, this.drag.fromNodeId, targetPort.nodeId);
                    if (!cycle) {
                        (0, state_1.addWire)(this.scene, this.drag.fromNodeId, this.drag.fromPortName, targetPort.nodeId, targetPort.portName);
                        // Re-evaluate downstream from the source node
                        (0, evaluation_1.evaluateDownstream)(this.scene, this.drag.fromNodeId);
                    }
                    else {
                        // Cycle detected - show rejection feedback briefly
                        this.drag.type = 'cycle-rejected';
                        setTimeout(() => {
                            if (this.drag.type === 'cycle-rejected') {
                                this.drag = { type: 'none' };
                                this.onRender?.();
                            }
                        }, 500);
                        this.onRender?.();
                        return;
                    }
                }
            }
            // If we didn't connect to anything, the wire is discarded (already removed if re-routing)
        }
        this.drag = { type: 'none' };
        this.onRender?.();
    }
    onWheel(e) {
        e.preventDefault();
        const rect = this.canvas.getBoundingClientRect();
        const mx = e.clientX - rect.left;
        const my = e.clientY - rect.top;
        // Zoom toward cursor
        const [wx, wy] = (0, rendering_1.screenToWorld)(mx, my, this.view);
        const zoomFactor = e.deltaY < 0 ? 1.12 : 1 / 1.12;
        const newZoom = Math.max(types_1.CONSTANTS.ZOOM_MIN, types_1.CONSTANTS.ZOOM_MAX * (e.deltaY > 0 ? -1 : 1) +
            (e.deltaY > 0 ? types_1.CONSTANTS.ZOOM_MIN : 0));
        // Clamp properly
        let targetZoom = this.view.zoom * zoomFactor;
        targetZoom = Math.max(types_1.CONSTANTS.ZOOM_MIN, Math.min(types_1.CONSTANTS.ZOOM_MAX, targetZoom));
        // Adjust pan so the world point under cursor stays fixed
        this.view.panX = mx - wx * targetZoom;
        this.view.panY = my - wy * targetZoom;
        this.view.zoom = targetZoom;
        this.onRender?.();
    }
    onKeyDown(e) {
        if (e.key === 'Delete' || e.key === 'Backspace') {
            if (this.selectedNodeId) {
                (0, state_1.removeNode)(this.scene, this.selectedNodeId);
                (0, evaluation_1.evaluateDownstream)(this.scene, this.selectedNodeId);
                this.selectedNodeId = null;
                this.onSelectChange?.(null);
                this.onRender?.();
            }
        }
        // Space to toggle selected node (if it's a toggle)
        if (e.key === ' ' && this.selectedNodeId) {
            e.preventDefault();
            const node = this.scene.nodes.find(n => n.id === this.selectedNodeId);
            if (node && node.type === 'toggle') {
                node.state = !node.state;
                (0, evaluation_1.evaluateDownstream)(this.scene, node.id);
                this.onRender?.();
            }
        }
        // Arrow keys to move selected node
        if (this.selectedNodeId) {
            const step = e.shiftKey ? 20 : 5;
            const node = this.scene.nodes.find(n => n.id === this.selectedNodeId);
            if (node) {
                let moved = false;
                if (e.key === 'ArrowLeft') {
                    node.x -= step;
                    moved = true;
                }
                if (e.key === 'ArrowRight') {
                    node.x += step;
                    moved = true;
                }
                if (e.key === 'ArrowUp') {
                    node.y -= step;
                    moved = true;
                }
                if (e.key === 'ArrowDown') {
                    node.y += step;
                    moved = true;
                }
                if (moved) {
                    e.preventDefault();
                    this.onRender?.();
                }
            }
        }
    }
    // ─── Public API ──────────────────────────────────────────────────
    getSelectedNodeId() {
        return this.selectedNodeId;
    }
    getHoveredNodeId() {
        return this.hoveredNodeId;
    }
    getDragState() {
        return this.drag;
    }
    addNode(type, worldX, worldY) {
        const node = (0, state_1.addNode)(this.scene, type, worldX, worldY);
        (0, evaluation_1.evaluateDownstream)(this.scene, node.id);
        this.onRender?.();
        return node;
    }
    toggleSelectedNode() {
        if (!this.selectedNodeId)
            return;
        const node = this.scene.nodes.find(n => n.id === this.selectedNodeId);
        if (node && node.type === 'toggle') {
            node.state = !node.state;
            (0, evaluation_1.evaluateDownstream)(this.scene, node.id);
            this.onRender?.();
        }
    }
    cycleClockRate() {
        if (!this.selectedNodeId)
            return;
        const node = this.scene.nodes.find(n => n.id === this.selectedNodeId);
        if (node && node.type === 'clock') {
            node.clockRateIdx = ((node.clockRateIdx || 0) + 1) % types_1.CONSTANTS.CLOCK_RATES.length;
            this.onRender?.();
        }
    }
    // ─── Wire Preview Drawing ────────────────────────────────────────
    drawWirePreview(ctx) {
        if ((this.drag.type !== 'create-wire' && this.drag.type !== 'cycle-rejected') || !this.drag.fromWorldX)
            return;
        const [sx1, sy1] = (0, rendering_1.worldToScreen)(this.drag.fromWorldX, this.drag.fromWorldY, this.view);
        const sx2 = this.drag.toScreenX;
        const sy2 = this.drag.toScreenY;
        // Determine if energized based on source node state
        const srcNode = this.scene.nodes.find(n => n.id === this.drag.fromNodeId);
        const energized = srcNode?.state || false;
        // Adaptive control arm length based on distance between nodes
        const dx = Math.abs(sx2 - sx1);
        const minArm = types_1.CONSTANTS.WIRE_ARM_LENGTH * this.view.zoom;
        const maxArm = dx * 0.45;
        const armLen = Math.max(minArm, Math.min(maxArm, (dx / 2) * 0.6));
        ctx.beginPath();
        ctx.moveTo(sx1, sy1);
        ctx.bezierCurveTo(sx1 + armLen, sy1, sx2 - armLen, sy2, sx2, sy2);
        if (this.drag.type === 'cycle-rejected') {
            // Red flash for cycle rejection
            ctx.strokeStyle = '#ff4444';
            ctx.lineWidth = Math.max(3, 4 * this.view.zoom);
            ctx.shadowColor = 'rgba(255,68,68,0.5)';
            ctx.shadowBlur = 10;
        }
        else {
            // Check if target port is valid (highlight when hovering over one)
            const [twx, twy] = (0, rendering_1.screenToWorld)(sx2, sy2, this.view);
            const hitRadius = types_1.CONSTANTS.PORT_HIT_TOLERANCE / this.view.zoom;
            let isValidTarget = false;
            for (const node of this.scene.nodes) {
                const def = types_1.NODE_DEFS[node.type];
                if (!def)
                    continue;
                for (const port of def.ports.filter(p => p.direction === 'in')) {
                    const pos = (0, state_1.getPortPosition)(node, port.name);
                    if (!pos)
                        continue;
                    const dx = twx - pos.x;
                    const dy = twy - pos.y;
                    if (Math.sqrt(dx * dx + dy * dy) < hitRadius && node.id !== this.drag.fromNodeId) {
                        isValidTarget = true;
                        break;
                    }
                }
                if (isValidTarget)
                    break;
            }
            ctx.setLineDash([6 * this.view.zoom, 4 * this.view.zoom]);
            ctx.strokeStyle = isValidTarget ? '#4aeaff' : 'rgba(100,120,160,0.5)';
            ctx.lineWidth = Math.max(2, 3 * this.view.zoom);
        }
        ctx.stroke();
        ctx.setLineDash([]);
        ctx.shadowColor = 'transparent';
        ctx.shadowBlur = 0;
        // Highlight target port if valid
        const [twx, twy] = (0, rendering_1.screenToWorld)(sx2, sy2, this.view);
        const hitRadius = types_1.CONSTANTS.PORT_HIT_TOLERANCE / this.view.zoom;
        for (const node of this.scene.nodes) {
            const def = types_1.NODE_DEFS[node.type];
            if (!def)
                continue;
            for (const port of def.ports.filter(p => p.direction === 'in')) {
                const pos = (0, state_1.getPortPosition)(node, port.name);
                if (!pos)
                    continue;
                const dx = twx - pos.x;
                const dy = twy - pos.y;
                if (Math.sqrt(dx * dx + dy * dy) < hitRadius && node.id !== this.drag.fromNodeId) {
                    // Draw highlight ring around target port
                    const [px, py] = (0, rendering_1.worldToScreen)(pos.x, pos.y, this.view);
                    ctx.beginPath();
                    ctx.arc(px, py, types_1.CONSTANTS.PORT_RADIUS * this.view.zoom + 4, 0, Math.PI * 2);
                    ctx.strokeStyle = '#4aeaff';
                    ctx.lineWidth = 2;
                    ctx.stroke();
                    break;
                }
            }
        }
    }
    // ─── Hex Display Update ──────────────────────────────────────────
    updateHexDisplays() {
        for (const node of this.scene.nodes) {
            if (node.type === 'hex') {
                let val = 0;
                const ports = ['d0', 'd1', 'd2', 'd3']; // LSB to MSB
                for (let i = 0; i < ports.length; i++) {
                    const wire = (0, state_1.getInputWire)(this.scene, node.id, ports[i]);
                    if (wire) {
                        const srcNode = this.scene.nodes.find(n => n.id === wire.fromNodeId);
                        if (srcNode && srcNode.state) {
                            val |= (1 << i);
                        }
                    }
                }
                node.label = val.toString(16).toUpperCase();
            }
        }
    }
    // ─── Clock Ticking ──────────────────────────────────────────────
    tickClocks() {
        for (const node of this.scene.nodes) {
            if (node.type === 'clock') {
                node.state = !node.state;
                (0, evaluation_1.evaluateDownstream)(this.scene, node.id);
            }
        }
        this.updateHexDisplays();
        this.onRender?.();
    }
    // ─── Scene Accessors ─────────────────────────────────────────────
    getScene() {
        return this.scene;
    }
    getView() {
        return this.view;
    }
}
exports.InteractionManager = InteractionManager;
};

// ── module: src/persistence.ts ──
__mods["src/persistence.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveScene = saveScene;
exports.loadScene = loadScene;
exports.clearScene = clearScene;
const STORAGE_KEY = 'logic-circuit-editor-scene';
function saveScene(scene) {
    try {
        const data = JSON.stringify(scene);
        localStorage.setItem(STORAGE_KEY, data);
    }
    catch (e) {
        // Storage might be full or unavailable
        console.warn('Failed to save scene:', e);
    }
}
function loadScene() {
    try {
        const data = localStorage.getItem(STORAGE_KEY);
        if (!data)
            return null;
        return JSON.parse(data);
    }
    catch (e) {
        console.warn('Failed to load scene:', e);
        return null;
    }
}
function clearScene() {
    localStorage.removeItem(STORAGE_KEY);
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"3451f853-8a87-42c4-9b42-b16d1972f0c0","tokensIn":7688852,"tokensOut":70461,"tokensTotal":7759313,"turns":126,"toolCalls":125,"failedToolCalls":2,"timestamp":"2026-07-30T23:48:07.479Z"} -->