fix: complete rewrite of component evaluation system
Major fixes for custom components when used in the main circuit: - Add outputValues[] array for multi-output component gates, so each output port carries its own independent value - readSourcePort() reads the correct port value from source gates instead of always reading gate.value - evaluateComponent() now uses iterative fixed-point evaluation (matching main evaluateAll) instead of a simple 10-pass loop - Store inputIds/outputIds in component definition for consistent port-to-gate mapping across save/load - Renderer reads per-port values for connection color and port glow - Added debug logs for component save and evaluation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
182
js/components.js
182
js/components.js
@@ -18,15 +18,22 @@ export function saveComponentFromCircuit(name) {
|
|||||||
const outputGates = state.gates.filter(g => g.type === 'OUTPUT');
|
const outputGates = state.gates.filter(g => g.type === 'OUTPUT');
|
||||||
|
|
||||||
if (inputGates.length === 0 || outputGates.length === 0) {
|
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' };
|
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
|
// Create component definition
|
||||||
const component = {
|
const component = {
|
||||||
id: sanitizeComponentName(name),
|
id: sanitizeComponentName(name),
|
||||||
name,
|
name,
|
||||||
inputCount: inputGates.length,
|
inputCount: inputGates.length,
|
||||||
outputCount: outputGates.length,
|
outputCount: outputGates.length,
|
||||||
|
inputIds,
|
||||||
|
outputIds,
|
||||||
gates: JSON.parse(JSON.stringify(state.gates)),
|
gates: JSON.parse(JSON.stringify(state.gates)),
|
||||||
connections: JSON.parse(JSON.stringify(state.connections))
|
connections: JSON.parse(JSON.stringify(state.connections))
|
||||||
};
|
};
|
||||||
@@ -37,125 +44,108 @@ export function saveComponentFromCircuit(name) {
|
|||||||
}
|
}
|
||||||
state.customComponents[component.id] = component;
|
state.customComponents[component.id] = component;
|
||||||
|
|
||||||
|
console.log(`[component] saved "${name}" (${component.inputCount} in, ${component.outputCount} out)`,
|
||||||
|
`inputIds=${inputIds}`, `outputIds=${outputIds}`);
|
||||||
|
|
||||||
return { success: true, component };
|
return { success: true, component };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate a component on the canvas
|
* Evaluate a component instance.
|
||||||
*/
|
* Simulates the internal circuit and returns an array of output values.
|
||||||
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
|
|
||||||
*/
|
*/
|
||||||
export function evaluateComponent(gate, inputs) {
|
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 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
|
// Deep clone internal circuit for simulation
|
||||||
const inputGates = internalState.gates.filter(g => g.type === 'INPUT');
|
const internalGates = JSON.parse(JSON.stringify(comp.gates));
|
||||||
inputs.forEach((val, i) => {
|
const internalConns = comp.connections; // read-only, no need to clone
|
||||||
if (inputGates[i]) inputGates[i].value = val;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Evaluate internal circuit
|
// Map external inputs to internal INPUT gates using stored inputIds
|
||||||
evaluateInternalCircuit(internalState);
|
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
|
// Iterative fixed-point evaluation (same approach as main evaluateAll)
|
||||||
const outputGates = internalState.gates.filter(g => g.type === 'OUTPUT');
|
const MAX_ITER = 20;
|
||||||
const outputs = outputGates.map(g => g.value || 0);
|
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;
|
return outputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to evaluate internal circuit
|
* Get input count for a gate type
|
||||||
*/
|
|
||||||
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)
|
|
||||||
*/
|
*/
|
||||||
function getGateInputCount(type) {
|
function getGateInputCount(type) {
|
||||||
if (type === 'CLOCK' || type === 'INPUT') return 0;
|
if (type === 'CLOCK' || type === 'INPUT') return 0;
|
||||||
if (type === 'NOT' || type === 'OUTPUT') return 1;
|
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;
|
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
|
* Sanitize component name for use as ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
32
js/gates.js
32
js/gates.js
@@ -66,9 +66,22 @@ export function getOutputPorts(gate) {
|
|||||||
return ports;
|
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.
|
* Compute the output of a single gate given its current input values.
|
||||||
* Does NOT recurse — just reads source gate .value directly.
|
* Does NOT recurse — just reads source gate .value directly.
|
||||||
|
* For COMPONENT gates, evaluates internal circuit and stores all outputs.
|
||||||
*/
|
*/
|
||||||
function computeGate(gate) {
|
function computeGate(gate) {
|
||||||
if (gate.type === 'INPUT' || gate.type === 'CLOCK') return gate.value;
|
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);
|
const conn = state.connections.find(c => c.to === gate.id && c.toPort === i);
|
||||||
if (conn) {
|
if (conn) {
|
||||||
const srcGate = state.gates.find(g => g.id === conn.from);
|
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 {
|
} else {
|
||||||
inputs.push(0);
|
inputs.push(0);
|
||||||
}
|
}
|
||||||
@@ -87,6 +100,8 @@ function computeGate(gate) {
|
|||||||
|
|
||||||
if (gate.type.startsWith('COMPONENT:')) {
|
if (gate.type.startsWith('COMPONENT:')) {
|
||||||
const outputs = evaluateComponent(gate, inputs);
|
const outputs = evaluateComponent(gate, inputs);
|
||||||
|
// Store all output values for multi-output components
|
||||||
|
gate.outputValues = outputs;
|
||||||
return outputs[0] || 0;
|
return outputs[0] || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,14 +130,25 @@ export function evaluateAll(recordWave = false) {
|
|||||||
let changed = false;
|
let changed = false;
|
||||||
for (const gate of state.gates) {
|
for (const gate of state.gates) {
|
||||||
if (gate.type === 'INPUT' || gate.type === 'CLOCK') continue;
|
if (gate.type === 'INPUT' || gate.type === 'CLOCK') continue;
|
||||||
|
const oldVal = gate.value;
|
||||||
|
const oldOutputs = gate.outputValues ? [...gate.outputValues] : null;
|
||||||
const newVal = computeGate(gate);
|
const newVal = computeGate(gate);
|
||||||
if (newVal !== gate.value) {
|
if (newVal !== oldVal) {
|
||||||
gate.value = newVal;
|
gate.value = newVal;
|
||||||
changed = true;
|
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 (!changed) {
|
||||||
console.log(`[eval] stable after ${iter + 1} iteration(s)`);
|
if (iter > 0) console.log(`[eval] stable after ${iter + 1} iteration(s)`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (iter === MAX_ITERATIONS - 1) {
|
if (iter === MAX_ITERATIONS - 1) {
|
||||||
|
|||||||
@@ -115,10 +115,11 @@ function drawGate(gate) {
|
|||||||
state.hoveredPort.gate === gate &&
|
state.hoveredPort.gate === gate &&
|
||||||
state.hoveredPort.index === p.index &&
|
state.hoveredPort.index === p.index &&
|
||||||
state.hoveredPort.type === 'output';
|
state.hoveredPort.type === 'output';
|
||||||
|
const portVal = gate.outputValues ? (gate.outputValues[p.index] || 0) : gate.value;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
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.fill();
|
||||||
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
@@ -185,10 +186,11 @@ function drawComponentGate(gate) {
|
|||||||
state.hoveredPort.gate === gate &&
|
state.hoveredPort.gate === gate &&
|
||||||
state.hoveredPort.index === p.index &&
|
state.hoveredPort.index === p.index &&
|
||||||
state.hoveredPort.type === 'output';
|
state.hoveredPort.type === 'output';
|
||||||
|
const portVal = gate.outputValues ? (gate.outputValues[p.index] || 0) : gate.value;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
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.fill();
|
||||||
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
@@ -205,7 +207,10 @@ function drawConnection(conn) {
|
|||||||
const toPort = getInputPorts(toGate)[conn.toPort];
|
const toPort = getInputPorts(toGate)[conn.toPort];
|
||||||
if (!fromPort || !toPort) return;
|
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;
|
const midX = (fromPort.x + toPort.x) / 2;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|||||||
Reference in New Issue
Block a user