feat: port labels on component gates + persistent internal state

Show input/output labels next to ports on custom component chips,
and persist internal gate state between evaluations so latches and
flip-flops retain their values correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 04:15:37 +01:00
parent 1c45dc6104
commit 817dab43df
2 changed files with 46 additions and 3 deletions

View File

@@ -53,6 +53,8 @@ export function saveComponentFromCircuit(name) {
/** /**
* Evaluate a component instance. * Evaluate a component instance.
* Simulates the internal circuit and returns an array of output values. * Simulates the internal circuit and returns an array of output values.
* IMPORTANT: Uses persistent internal state so latches/flip-flops retain
* their values between evaluations (just like the main circuit).
*/ */
export function evaluateComponent(gate, inputs) { export function evaluateComponent(gate, inputs) {
if (!gate.component) { if (!gate.component) {
@@ -62,8 +64,11 @@ export function evaluateComponent(gate, inputs) {
const comp = gate.component; const comp = gate.component;
// Deep clone internal circuit for simulation // Persist internal gate state on the gate instance so latches hold their value
const internalGates = JSON.parse(JSON.stringify(comp.gates)); if (!gate._internalGates) {
gate._internalGates = JSON.parse(JSON.stringify(comp.gates));
}
const internalGates = gate._internalGates;
const internalConns = comp.connections; // read-only, no need to clone const internalConns = comp.connections; // read-only, no need to clone
// Map external inputs to internal INPUT gates using stored inputIds // Map external inputs to internal INPUT gates using stored inputIds
@@ -133,7 +138,8 @@ export function evaluateComponent(gate, inputs) {
} }
} }
console.log(`[component] eval "${comp.name}" inputs=[${inputs}] → outputs=[${outputs}]`); console.log(`[component] eval "${comp.name}" inputs=[${inputs}] → outputs=[${outputs}]`,
`(internal state preserved: ${gate._internalGates ? 'yes' : 'no'})`);
return outputs; return outputs;
} }

View File

@@ -162,6 +162,23 @@ function drawComponentGate(gate) {
ctx.fillStyle = '#444'; ctx.fillStyle = '#444';
ctx.fillText(getGateLabel(gate), gate.x + w / 2, gate.y + h - 6); ctx.fillText(getGateLabel(gate), gate.x + w / 2, gate.y + h - 6);
// Get port labels from the component definition
const comp = gate.component;
const inputLabels = [];
const outputLabels = [];
if (comp) {
const inputIds = comp.inputIds || [];
const outputIds = comp.outputIds || [];
for (const id of inputIds) {
const g = comp.gates.find(g => g.id === id);
inputLabels.push(g?.label || '');
}
for (const id of outputIds) {
const g = comp.gates.find(g => g.id === id);
outputLabels.push(g?.label || '');
}
}
// Input ports // Input ports
getInputPorts(gate).forEach(p => { getInputPorts(gate).forEach(p => {
const isPortHovered = state.hoveredPort && const isPortHovered = state.hoveredPort &&
@@ -178,6 +195,16 @@ function drawComponentGate(gate) {
ctx.strokeStyle = isPortHovered ? '#fff' : '#555'; ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
ctx.lineWidth = 1.5; ctx.lineWidth = 1.5;
ctx.stroke(); ctx.stroke();
// Port label (inside the gate, to the right of the port)
const label = inputLabels[p.index];
if (label) {
ctx.font = '9px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = '#aaa';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(label, p.x + PORT_R + 4, p.y);
}
}); });
// Output ports // Output ports
@@ -195,6 +222,16 @@ function drawComponentGate(gate) {
ctx.strokeStyle = isPortHovered ? '#fff' : '#555'; ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
ctx.lineWidth = 1.5; ctx.lineWidth = 1.5;
ctx.stroke(); ctx.stroke();
// Port label (inside the gate, to the left of the port)
const label = outputLabels[p.index];
if (label) {
ctx.font = '9px "Segoe UI", system-ui, sans-serif';
ctx.fillStyle = '#aaa';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(label, p.x - PORT_R - 4, p.y);
}
}); });
} }