// Gate evaluation and port geometry import { GATE_W, GATE_H, COMP_W, PORT_R, gateInputCount as baseGateInputCount, gateOutputCount as baseGateOutputCount } from './constants.js'; import { state } from './state.js'; import { recordSample, setEvaluateAll } from './waveform.js'; import { evaluateComponent } from './components.js'; // Wrappers that handle component and BUS types export function gateInputCount(type) { if (type.startsWith('COMPONENT:')) { const componentId = type.substring(10); const component = state.customComponents?.[componentId]; return component ? component.inputCount : 0; } if (type.startsWith('BUS_IN:')) return parseInt(type.substring(7)) || 0; if (type.startsWith('BUS_OUT:')) return 0; return baseGateInputCount(type); } export function gateOutputCount(type) { if (type.startsWith('COMPONENT:')) { const componentId = type.substring(10); const component = state.customComponents?.[componentId]; return component ? component.outputCount : 0; } if (type.startsWith('BUS_IN:')) return 0; if (type.startsWith('BUS_OUT:')) return parseInt(type.substring(8)) || 0; return baseGateOutputCount(type); } function isBusType(type) { return type.startsWith('BUS_IN:') || type.startsWith('BUS_OUT:'); } function getBusSize(type) { if (type.startsWith('BUS_IN:')) return parseInt(type.substring(7)) || 1; if (type.startsWith('BUS_OUT:')) return parseInt(type.substring(8)) || 1; return 1; } export function getComponentWidth(gate) { if (isBusType(gate.type)) return 30; if (gate.type.startsWith('COMPONENT:')) { const count = Math.max(gate.component?.inputCount || 1, gate.component?.outputCount || 1); return Math.max(120, (count + 1) * 25); } return GATE_W; } export function getComponentHeight(gate) { if (isBusType(gate.type)) { const n = getBusSize(gate.type); return Math.max(40, (n + 1) * 22); } if (gate.type.startsWith('COMPONENT:')) { const count = Math.max(gate.component?.inputCount || 1, gate.component?.outputCount || 1); return Math.max(60, (count + 1) * 25); } return GATE_H; } export function getInputPorts(gate) { const count = gateInputCount(gate.type); const ports = []; const isDynamic = gate.type.startsWith('COMPONENT:') || isBusType(gate.type); const gateHeight = isDynamic ? getComponentHeight(gate) : GATE_H; for (let i = 0; i < count; i++) { const spacing = gateHeight / (count + 1); ports.push({ x: gate.x, y: gate.y + spacing * (i + 1), index: i, type: 'input' }); } return ports; } export function getOutputPorts(gate) { const count = gateOutputCount(gate.type); const ports = []; const isDynamic = gate.type.startsWith('COMPONENT:') || isBusType(gate.type); const gateWidth = isDynamic ? getComponentWidth(gate) : GATE_W; const gateHeight = isDynamic ? getComponentHeight(gate) : GATE_H; for (let i = 0; i < count; i++) { const spacing = gateHeight / (count + 1); ports.push({ x: gate.x + gateWidth, y: gate.y + spacing * (i + 1), index: i, type: 'output' }); } return ports; } /** * Read the value from a source gate at a specific output port. * For component gates with multiple outputs, reads from outputValues[]. * For normal gates (single output), reads gate.value. */ function readSourcePort(srcGate, fromPort) { if (srcGate.outputValues && fromPort < srcGate.outputValues.length) { return srcGate.outputValues[fromPort]; } return srcGate.value || 0; } /** * Compute the output of a single gate given its current input values. * Does NOT recurse — just reads source gate .value directly. * For COMPONENT gates, evaluates internal circuit and stores all outputs. */ function computeGate(gate) { if (gate.type === 'INPUT' || gate.type === 'CLOCK') return gate.value; const inputCount = gateInputCount(gate.type); const inputs = []; for (let i = 0; i < inputCount; i++) { const conn = state.connections.find(c => c.to === gate.id && c.toPort === i); if (conn) { const srcGate = state.gates.find(g => g.id === conn.from); inputs.push(srcGate ? readSourcePort(srcGate, conn.fromPort) : 0); } else { inputs.push(0); } } if (gate.type.startsWith('COMPONENT:')) { const outputs = evaluateComponent(gate, inputs); // Store all output values for multi-output components gate.outputValues = outputs; return outputs[0] || 0; } // BUS_IN: collect input values and store them for the paired BUS_OUT if (gate.type.startsWith('BUS_IN:')) { gate.busValues = [...inputs]; gate.value = inputs[0] || 0; return gate.value; } // BUS_OUT: read values from paired BUS_IN terminal if (gate.type.startsWith('BUS_OUT:')) { const pair = state.gates.find(g => g.id === gate.busPairId); if (pair && pair.busValues) { gate.outputValues = [...pair.busValues]; gate.value = gate.outputValues[0] || 0; } return gate.value || 0; } switch (gate.type) { case 'AND': return (inputs[0] && inputs[1]) ? 1 : 0; case 'OR': return (inputs[0] || inputs[1]) ? 1 : 0; case 'NOT': return inputs[0] ? 0 : 1; case 'NAND': return (inputs[0] && inputs[1]) ? 0 : 1; case 'NOR': return (inputs[0] || inputs[1]) ? 0 : 1; case 'XOR': return (inputs[0] !== inputs[1]) ? 1 : 0; case 'OUTPUT': return inputs[0] || 0; default: return 0; } } /** * Iterative fixed-point evaluation. * Runs multiple passes over all gates until no values change (stable) * or a max iteration limit is reached. Does NOT reset gate values, * preserving latch/flip-flop state across evaluations. */ const MAX_ITERATIONS = 20; export function evaluateAll(recordWave = false) { for (let iter = 0; iter < MAX_ITERATIONS; iter++) { let changed = false; for (const gate of state.gates) { if (gate.type === 'INPUT' || gate.type === 'CLOCK') continue; const oldVal = gate.value; const oldOutputs = gate.outputValues ? [...gate.outputValues] : null; const newVal = computeGate(gate); if (newVal !== oldVal) { gate.value = newVal; changed = true; } // Also check if outputValues changed (for multi-output components) if (gate.outputValues && oldOutputs) { for (let i = 0; i < gate.outputValues.length; i++) { if (gate.outputValues[i] !== oldOutputs[i]) { changed = true; break; } } } } if (!changed) { if (iter > 0) console.log(`[eval] stable after ${iter + 1} iteration(s)`); break; } if (iter === MAX_ITERATIONS - 1) { console.warn(`[eval] did not stabilize after ${MAX_ITERATIONS} iterations (oscillation?)`); } } if (recordWave && state.recording && state.waveformVisible) recordSample(); } // Keep legacy export name for components.js internal use export function evaluate(gate) { return computeGate(gate); } // Register evaluateAll in waveform to break circular dependency setEvaluateAll(evaluateAll); export function findGateAt(x, y) { return state.gates.find(g => { const isDynamic = g.type.startsWith('COMPONENT:') || isBusType(g.type); const w = isDynamic ? getComponentWidth(g) : GATE_W; const h = isDynamic ? getComponentHeight(g) : GATE_H; return x >= g.x && x <= g.x + w && y >= g.y && y <= g.y + h; }); } export function findPortAt(x, y) { for (const gate of state.gates) { for (const p of getInputPorts(gate)) { if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) return { gate, index: p.index, type: 'input' }; } for (const p of getOutputPorts(gate)) { if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) return { gate, index: p.index, type: 'output' }; } } return null; }