BUS_IN has input pins only (left side), BUS_OUT has output pins only (right side). No internal connections between them — BUS_OUT reads values directly from its paired BUS_IN via busPairId. The bus cable between them is purely visual, representing the grouped signal bundle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
225 lines
8.1 KiB
JavaScript
225 lines
8.1 KiB
JavaScript
// 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;
|
|
}
|