diff --git a/js/components.js b/js/components.js index 685fdc9..8a3417d 100644 --- a/js/components.js +++ b/js/components.js @@ -18,15 +18,22 @@ export function saveComponentFromCircuit(name) { const outputGates = state.gates.filter(g => g.type === 'OUTPUT'); if (inputGates.length === 0 || outputGates.length === 0) { + alert('Component must have at least one INPUT and one OUTPUT'); return { success: false, error: 'Component must have at least one INPUT and one OUTPUT' }; } + // Store the input/output gate IDs in order so we can map ports consistently + const inputIds = inputGates.map(g => g.id); + const outputIds = outputGates.map(g => g.id); + // Create component definition const component = { id: sanitizeComponentName(name), name, inputCount: inputGates.length, outputCount: outputGates.length, + inputIds, + outputIds, gates: JSON.parse(JSON.stringify(state.gates)), connections: JSON.parse(JSON.stringify(state.connections)) }; @@ -37,125 +44,108 @@ export function saveComponentFromCircuit(name) { } state.customComponents[component.id] = component; + console.log(`[component] saved "${name}" (${component.inputCount} in, ${component.outputCount} out)`, + `inputIds=${inputIds}`, `outputIds=${outputIds}`); + return { success: true, component }; } /** - * Instantiate a component on the canvas - */ -export function instantiateComponent(componentId, x, y) { - if (!state.customComponents || !state.customComponents[componentId]) { - return { success: false, error: 'Component not found' }; - } - - const component = state.customComponents[componentId]; - const instanceId = state.nextId++; - - // Create a component instance gate - const gate = { - id: instanceId, - type: `COMPONENT:${componentId}`, - x, - y, - value: 0, - component - }; - - state.gates.push(gate); - - return { success: true, gate }; -} - -/** - * Evaluate a component instance - * Simulates the internal circuit and returns output + * Evaluate a component instance. + * Simulates the internal circuit and returns an array of output values. */ export function evaluateComponent(gate, inputs) { - if (!gate.component) return 0; + if (!gate.component) { + console.warn('[component] evaluateComponent called without component data', gate); + return [0]; + } const comp = gate.component; - const internalState = { - gates: JSON.parse(JSON.stringify(comp.gates)), - connections: JSON.parse(JSON.stringify(comp.connections)), - nextId: Math.max(...comp.gates.map(g => g.id), 0) + 1 - }; - // Set inputs - const inputGates = internalState.gates.filter(g => g.type === 'INPUT'); - inputs.forEach((val, i) => { - if (inputGates[i]) inputGates[i].value = val; - }); + // Deep clone internal circuit for simulation + const internalGates = JSON.parse(JSON.stringify(comp.gates)); + const internalConns = comp.connections; // read-only, no need to clone - // Evaluate internal circuit - evaluateInternalCircuit(internalState); + // Map external inputs to internal INPUT gates using stored inputIds + const inputIds = comp.inputIds || []; + for (let i = 0; i < inputs.length; i++) { + const targetId = inputIds[i]; + const inputGate = targetId != null + ? internalGates.find(g => g.id === targetId) + : internalGates.filter(g => g.type === 'INPUT')[i]; // fallback for old components + if (inputGate) { + inputGate.value = inputs[i]; + } + } - // Get outputs - const outputGates = internalState.gates.filter(g => g.type === 'OUTPUT'); - const outputs = outputGates.map(g => g.value || 0); + // Iterative fixed-point evaluation (same approach as main evaluateAll) + const MAX_ITER = 20; + for (let iter = 0; iter < MAX_ITER; iter++) { + let changed = false; + for (const g of internalGates) { + if (g.type === 'INPUT' || g.type === 'CLOCK') continue; + const inCount = getGateInputCount(g.type); + const gInputs = []; + for (let j = 0; j < inCount; j++) { + const conn = internalConns.find(c => c.to === g.id && c.toPort === j); + if (conn) { + const src = internalGates.find(s => s.id === conn.from); + gInputs.push(src ? (src.value || 0) : 0); + } else { + gInputs.push(0); + } + } + + let result = 0; + switch (g.type) { + case 'AND': result = (gInputs[0] && gInputs[1]) ? 1 : 0; break; + case 'OR': result = (gInputs[0] || gInputs[1]) ? 1 : 0; break; + case 'NOT': result = gInputs[0] ? 0 : 1; break; + case 'NAND': result = (gInputs[0] && gInputs[1]) ? 0 : 1; break; + case 'NOR': result = (gInputs[0] || gInputs[1]) ? 0 : 1; break; + case 'XOR': result = (gInputs[0] !== gInputs[1]) ? 1 : 0; break; + case 'OUTPUT': result = gInputs[0] || 0; break; + default: result = 0; + } + + if (result !== g.value) { + g.value = result; + changed = true; + } + } + if (!changed) break; + } + + // Read outputs using stored outputIds + const outputIds = comp.outputIds || []; + const outputs = []; + if (outputIds.length > 0) { + for (const outId of outputIds) { + const outGate = internalGates.find(g => g.id === outId); + outputs.push(outGate ? (outGate.value || 0) : 0); + } + } else { + // Fallback for old components without outputIds + const outputGates = internalGates.filter(g => g.type === 'OUTPUT'); + for (const g of outputGates) { + outputs.push(g.value || 0); + } + } + + console.log(`[component] eval "${comp.name}" inputs=[${inputs}] → outputs=[${outputs}]`); return outputs; } /** - * Helper to evaluate internal circuit - */ -function evaluateInternalCircuit(internalState) { - const { gates, connections } = internalState; - - // Simple evaluation - may need optimization for complex circuits - for (let i = 0; i < 10; i++) { - for (const gate of gates) { - if (gate.type === 'INPUT' || gate.type === 'CLOCK') continue; - - const inputCount = getGateInputCount(gate.type); - const inputs = []; - - for (let j = 0; j < inputCount; j++) { - const conn = connections.find(c => c.to === gate.id && c.toPort === j); - if (conn) { - const srcGate = gates.find(g => g.id === conn.from); - inputs.push(srcGate ? srcGate.value || 0 : 0); - } else { - inputs.push(0); - } - } - - // Evaluate based on gate type - let result = 0; - if (gate.type === 'AND') result = (inputs[0] && inputs[1]) ? 1 : 0; - else if (gate.type === 'OR') result = (inputs[0] || inputs[1]) ? 1 : 0; - else if (gate.type === 'NOT') result = inputs[0] ? 0 : 1; - else if (gate.type === 'NAND') result = (inputs[0] && inputs[1]) ? 0 : 1; - else if (gate.type === 'NOR') result = (inputs[0] || inputs[1]) ? 0 : 1; - else if (gate.type === 'XOR') result = (inputs[0] !== inputs[1]) ? 1 : 0; - else if (gate.type === 'OUTPUT') result = inputs[0] || 0; - - gate.value = result; - } - } -} - -/** - * Get input count for a gate type (includes component types) + * Get input count for a gate type */ function getGateInputCount(type) { if (type === 'CLOCK' || type === 'INPUT') return 0; if (type === 'NOT' || type === 'OUTPUT') return 1; - if (type.startsWith('COMPONENT:')) { - // Return the component's input count - return 2; // Default for now, should lookup - } return 2; } -/** - * Get output count for a gate type - */ -function getGateOutputCount(type) { - if (type === 'OUTPUT') return 0; - return 1; -} - /** * Sanitize component name for use as ID */ diff --git a/js/gates.js b/js/gates.js index 9f62ceb..3126a16 100644 --- a/js/gates.js +++ b/js/gates.js @@ -66,9 +66,22 @@ export function getOutputPorts(gate) { 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; @@ -79,7 +92,7 @@ function computeGate(gate) { 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 ? (srcGate.value || 0) : 0); + inputs.push(srcGate ? readSourcePort(srcGate, conn.fromPort) : 0); } else { inputs.push(0); } @@ -87,6 +100,8 @@ function computeGate(gate) { 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; } @@ -115,14 +130,25 @@ export function evaluateAll(recordWave = false) { 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 !== gate.value) { + 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) { - console.log(`[eval] stable after ${iter + 1} iteration(s)`); + if (iter > 0) console.log(`[eval] stable after ${iter + 1} iteration(s)`); break; } if (iter === MAX_ITERATIONS - 1) { diff --git a/js/renderer.js b/js/renderer.js index 87ffbeb..45703b8 100644 --- a/js/renderer.js +++ b/js/renderer.js @@ -115,10 +115,11 @@ function drawGate(gate) { state.hoveredPort.gate === gate && state.hoveredPort.index === p.index && state.hoveredPort.type === 'output'; + const portVal = gate.outputValues ? (gate.outputValues[p.index] || 0) : gate.value; ctx.beginPath(); ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2); - ctx.fillStyle = isPortHovered ? '#fff' : (gate.value ? '#00ff88' : '#1a1a2e'); + ctx.fillStyle = isPortHovered ? '#fff' : (portVal ? '#00ff88' : '#1a1a2e'); ctx.fill(); ctx.strokeStyle = isPortHovered ? '#fff' : '#555'; ctx.lineWidth = 1.5; @@ -185,10 +186,11 @@ function drawComponentGate(gate) { state.hoveredPort.gate === gate && state.hoveredPort.index === p.index && state.hoveredPort.type === 'output'; + const portVal = gate.outputValues ? (gate.outputValues[p.index] || 0) : gate.value; ctx.beginPath(); ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2); - ctx.fillStyle = isPortHovered ? '#fff' : (gate.value ? '#00ff88' : '#1a1a2e'); + ctx.fillStyle = isPortHovered ? '#fff' : (portVal ? '#00ff88' : '#1a1a2e'); ctx.fill(); ctx.strokeStyle = isPortHovered ? '#fff' : '#555'; ctx.lineWidth = 1.5; @@ -205,7 +207,10 @@ function drawConnection(conn) { const toPort = getInputPorts(toGate)[conn.toPort]; if (!fromPort || !toPort) return; - const active = fromGate.value === 1; + // Read correct output port value for multi-output gates (components) + const active = fromGate.outputValues + ? (fromGate.outputValues[conn.fromPort] || 0) === 1 + : fromGate.value === 1; const midX = (fromPort.x + toPort.x) / 2; ctx.beginPath();