From c116b6cf84e012a57915da8fd617a7aac3e18cfe Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Fri, 20 Mar 2026 04:50:42 +0100 Subject: [PATCH] refactor: bus terminals with single-sided pins only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- js/bus.js | 49 +++++++------------------- js/events.js | 2 +- js/gates.js | 47 +++++++++++++++++------- js/renderer.js | 96 ++++++++++++++++++++++++++++---------------------- 4 files changed, 102 insertions(+), 92 deletions(-) diff --git a/js/bus.js b/js/bus.js index 8ee250c..718ce84 100644 --- a/js/bus.js +++ b/js/bus.js @@ -125,33 +125,32 @@ export function createBusFromCut() { const busInId = state.nextId++; const busOutId = state.nextId++; - // Create BUS_IN terminal (left — collects wires into bus) + // Create BUS_IN terminal (left — collects wires into bus, only input pins) const busIn = { id: busInId, - type: `BUS:${n}`, + type: `BUS_IN:${n}`, x: avgX - gap / 2 - 15, y: avgY - busH / 2, value: 0, - outputValues: new Array(n).fill(0), - busRole: 'in', + busValues: new Array(n).fill(0), busPairId: busOutId }; - // Create BUS_OUT terminal (right — distributes bus back to wires) + // Create BUS_OUT terminal (right — distributes bus back to wires, only output pins) const busOut = { id: busOutId, - type: `BUS:${n}`, + type: `BUS_OUT:${n}`, x: avgX + gap / 2 - 15, y: avgY - busH / 2, value: 0, outputValues: new Array(n).fill(0), - busRole: 'out', busPairId: busInId }; state.gates.push(busIn, busOut); - // Rewire connections through both terminals + // Rewire: source → BUS_IN input, BUS_OUT output → destination + // No internal connections — BUS_OUT reads from BUS_IN directly via busPairId hits.forEach((hit, i) => { const orig = hit.conn; @@ -166,14 +165,6 @@ export function createBusFromCut() { toPort: i }); - // BUS_IN output[i] → BUS_OUT input[i] (internal bus link, rendered as cable) - state.connections.push({ - from: busIn.id, - fromPort: i, - to: busOut.id, - toPort: i - }); - // BUS_OUT output[i] → original destination state.connections.push({ from: busOut.id, @@ -187,19 +178,6 @@ export function createBusFromCut() { evaluateAll(); } -/** - * Check if a connection is an internal bus link (between paired terminals). - * Used by the renderer to skip drawing these as normal wires. - */ -export function isBusInternalConnection(conn) { - const fromGate = state.gates.find(g => g.id === conn.from); - const toGate = state.gates.find(g => g.id === conn.to); - if (!fromGate || !toGate) return false; - return fromGate.type.startsWith('BUS:') && - toGate.type.startsWith('BUS:') && - fromGate.busPairId === toGate.id; -} - /** * Get all bus pairs for rendering the bus cables. * Returns array of { inGate, outGate } for each pair. @@ -208,15 +186,12 @@ export function getBusPairs() { const pairs = []; const seen = new Set(); for (const gate of state.gates) { - if (!gate.type.startsWith('BUS:') || !gate.busPairId || seen.has(gate.id)) continue; - const pair = state.gates.find(g => g.id === gate.busPairId); - if (!pair) continue; + if (!gate.type.startsWith('BUS_IN:') || !gate.busPairId || seen.has(gate.id)) continue; + const outGate = state.gates.find(g => g.id === gate.busPairId); + if (!outGate || !outGate.type.startsWith('BUS_OUT:')) continue; seen.add(gate.id); - seen.add(pair.id); - // Determine which is in, which is out - const inGate = gate.busRole === 'in' ? gate : pair; - const outGate = gate.busRole === 'out' ? gate : pair; - pairs.push({ inGate, outGate }); + seen.add(outGate.id); + pairs.push({ inGate: gate, outGate }); } return pairs; } diff --git a/js/events.js b/js/events.js index dcb064c..c33990d 100644 --- a/js/events.js +++ b/js/events.js @@ -226,7 +226,7 @@ export function initEvents() { if (Math.abs(x2 - x1) > 5 || Math.abs(y2 - y1) > 5) { state.selectedGates = state.gates .filter(g => { - const isDynamic = g.type.startsWith('COMPONENT:') || g.type.startsWith('BUS:'); + const isDynamic = g.type.startsWith('COMPONENT:') || g.type.startsWith('BUS_IN:') || g.type.startsWith('BUS_OUT:'); const gw = isDynamic ? getComponentWidth(g) : GATE_W; const gh = isDynamic ? getComponentHeight(g) : GATE_H; // Gate overlaps selection box diff --git a/js/gates.js b/js/gates.js index 5a7b9ff..10ce540 100644 --- a/js/gates.js +++ b/js/gates.js @@ -11,7 +11,8 @@ export function gateInputCount(type) { const component = state.customComponents?.[componentId]; return component ? component.inputCount : 0; } - if (type.startsWith('BUS:')) return parseInt(type.substring(4)) || 0; + if (type.startsWith('BUS_IN:')) return parseInt(type.substring(7)) || 0; + if (type.startsWith('BUS_OUT:')) return 0; return baseGateInputCount(type); } @@ -21,12 +22,23 @@ export function gateOutputCount(type) { const component = state.customComponents?.[componentId]; return component ? component.outputCount : 0; } - if (type.startsWith('BUS:')) return parseInt(type.substring(4)) || 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 (gate.type.startsWith('BUS:')) return 30; + 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); @@ -35,8 +47,8 @@ export function getComponentWidth(gate) { } export function getComponentHeight(gate) { - if (gate.type.startsWith('BUS:')) { - const n = parseInt(gate.type.substring(4)) || 1; + if (isBusType(gate.type)) { + const n = getBusSize(gate.type); return Math.max(40, (n + 1) * 22); } if (gate.type.startsWith('COMPONENT:')) { @@ -49,7 +61,7 @@ export function getComponentHeight(gate) { export function getInputPorts(gate) { const count = gateInputCount(gate.type); const ports = []; - const isDynamic = gate.type.startsWith('COMPONENT:') || gate.type.startsWith('BUS:'); + const isDynamic = gate.type.startsWith('COMPONENT:') || isBusType(gate.type); const gateHeight = isDynamic ? getComponentHeight(gate) : GATE_H; for (let i = 0; i < count; i++) { @@ -62,7 +74,7 @@ export function getInputPorts(gate) { export function getOutputPorts(gate) { const count = gateOutputCount(gate.type); const ports = []; - const isDynamic = gate.type.startsWith('COMPONENT:') || gate.type.startsWith('BUS:'); + const isDynamic = gate.type.startsWith('COMPONENT:') || isBusType(gate.type); const gateWidth = isDynamic ? getComponentWidth(gate) : GATE_W; const gateHeight = isDynamic ? getComponentHeight(gate) : GATE_H; @@ -112,10 +124,21 @@ function computeGate(gate) { return outputs[0] || 0; } - // BUS: pass-through, each input maps to corresponding output - if (gate.type.startsWith('BUS:')) { - gate.outputValues = [...inputs]; - return inputs[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) { @@ -181,7 +204,7 @@ setEvaluateAll(evaluateAll); export function findGateAt(x, y) { return state.gates.find(g => { - const isDynamic = g.type.startsWith('COMPONENT:') || g.type.startsWith('BUS:'); + 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; diff --git a/js/renderer.js b/js/renderer.js index 24a6c7f..88b62a1 100644 --- a/js/renderer.js +++ b/js/renderer.js @@ -3,7 +3,7 @@ import { GATE_W, GATE_H, COMP_W, PORT_R, GATE_COLORS } from './constants.js'; import { state } from './state.js'; import { getInputPorts, getOutputPorts, getComponentWidth, getComponentHeight } from './gates.js'; import { getGateLabel, drawWaveLabels, drawWaveforms } from './waveform.js'; -import { isBusInternalConnection, getBusPairs } from './bus.js'; +import { getBusPairs } from './bus.js'; let canvas, ctx; @@ -32,9 +32,13 @@ export function screenToWorld(sx, sy) { }; } +function isBusType(type) { + return type.startsWith('BUS_IN:') || type.startsWith('BUS_OUT:'); +} + function drawSelectionHighlight(gate) { if (!state.selectedGates.includes(gate.id)) return; - const isDynamic = gate.type.startsWith('COMPONENT:') || gate.type.startsWith('BUS:'); + const isDynamic = gate.type.startsWith('COMPONENT:') || isBusType(gate.type); const w = isDynamic ? getComponentWidth(gate) : GATE_W; const h = isDynamic ? getComponentHeight(gate) : GATE_H; const pad = 4; @@ -49,7 +53,7 @@ function drawSelectionHighlight(gate) { function drawGate(gate) { // Special gate types have different rendering - if (gate.type.startsWith('BUS:')) { drawBusGate(gate); drawSelectionHighlight(gate); return; } + if (isBusType(gate.type)) { drawBusGate(gate); drawSelectionHighlight(gate); return; } if (gate.type.startsWith('COMPONENT:')) { drawComponentGate(gate); drawSelectionHighlight(gate); return; } const color = GATE_COLORS[gate.type]; @@ -149,8 +153,14 @@ function drawBusGate(gate) { const w = getComponentWidth(gate); // 30 const h = getComponentHeight(gate); const color = '#44ddff'; - const n = parseInt(gate.type.substring(4)) || 1; - const hasActive = gate.outputValues?.some(v => v === 1); + const isIn = gate.type.startsWith('BUS_IN:'); + const n = isIn + ? parseInt(gate.type.substring(7)) || 1 + : parseInt(gate.type.substring(8)) || 1; + + // Check if any channel is active + const values = isIn ? gate.busValues : gate.outputValues; + const hasActive = values?.some(v => v === 1); if (hasActive) { ctx.shadowColor = color; @@ -180,43 +190,48 @@ function drawBusGate(gate) { ctx.fillStyle = color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - const roleIcon = gate.busRole === 'in' ? '▶' : gate.busRole === 'out' ? '◀' : ''; + const roleIcon = isIn ? '▶' : '◀'; ctx.fillText(`${roleIcon} ${n}`, gate.x + w / 2, gate.y + h + 10); - // Input ports (left) - getInputPorts(gate).forEach(p => { - const isPortHovered = state.hoveredPort && - state.hoveredPort.gate === gate && - state.hoveredPort.index === p.index && - state.hoveredPort.type === 'input'; - const conn = state.connections.find(c => c.to === gate.id && c.toPort === p.index); - const portActive = conn ? state.gates.find(g => g.id === conn.from)?.value : 0; + // BUS_IN: only input ports (left side) + if (isIn) { + getInputPorts(gate).forEach(p => { + const isPortHovered = state.hoveredPort && + state.hoveredPort.gate === gate && + state.hoveredPort.index === p.index && + state.hoveredPort.type === 'input'; + const conn = state.connections.find(c => c.to === gate.id && c.toPort === p.index); + const srcGate = conn ? state.gates.find(g => g.id === conn.from) : null; + const portActive = srcGate ? (srcGate.outputValues ? (srcGate.outputValues[conn.fromPort] || 0) : srcGate.value) : 0; - ctx.beginPath(); - ctx.arc(p.x, p.y, PORT_R - 1, 0, Math.PI * 2); - ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e'); - ctx.fill(); - ctx.strokeStyle = isPortHovered ? '#fff' : '#555'; - ctx.lineWidth = 1.5; - ctx.stroke(); - }); + ctx.beginPath(); + ctx.arc(p.x, p.y, PORT_R - 1, 0, Math.PI * 2); + ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e'); + ctx.fill(); + ctx.strokeStyle = isPortHovered ? '#fff' : '#555'; + ctx.lineWidth = 1.5; + ctx.stroke(); + }); + } - // Output ports (right) - getOutputPorts(gate).forEach(p => { - const isPortHovered = state.hoveredPort && - state.hoveredPort.gate === gate && - state.hoveredPort.index === p.index && - state.hoveredPort.type === 'output'; - const portVal = gate.outputValues ? (gate.outputValues[p.index] || 0) : 0; + // BUS_OUT: only output ports (right side) + if (!isIn) { + getOutputPorts(gate).forEach(p => { + const isPortHovered = state.hoveredPort && + state.hoveredPort.gate === gate && + state.hoveredPort.index === p.index && + state.hoveredPort.type === 'output'; + const portVal = gate.outputValues ? (gate.outputValues[p.index] || 0) : 0; - ctx.beginPath(); - ctx.arc(p.x, p.y, PORT_R - 1, 0, Math.PI * 2); - ctx.fillStyle = isPortHovered ? '#fff' : (portVal ? '#00ff88' : '#1a1a2e'); - ctx.fill(); - ctx.strokeStyle = isPortHovered ? '#fff' : '#555'; - ctx.lineWidth = 1.5; - ctx.stroke(); - }); + ctx.beginPath(); + ctx.arc(p.x, p.y, PORT_R - 1, 0, Math.PI * 2); + ctx.fillStyle = isPortHovered ? '#fff' : (portVal ? '#00ff88' : '#1a1a2e'); + ctx.fill(); + ctx.strokeStyle = isPortHovered ? '#fff' : '#555'; + ctx.lineWidth = 1.5; + ctx.stroke(); + }); + } } function drawBusCables() { @@ -233,8 +248,8 @@ function drawBusCables() { const y2 = outGate.y + outH / 2; // Check if any channel is active - const hasActive = inGate.outputValues?.some(v => v === 1); - const n = parseInt(inGate.type.substring(4)) || 1; + const hasActive = inGate.busValues?.some(v => v === 1); + const n = parseInt(inGate.type.substring(7)) || 1; // Outer thick cable (bus background) const cableWidth = Math.max(6, n * 2.5); @@ -384,9 +399,6 @@ function drawComponentGate(gate) { } function drawConnection(conn) { - // Skip internal bus connections (rendered as bus cable instead) - if (isBusInternalConnection(conn)) return; - const fromGate = state.gates.find(g => g.id === conn.from); const toGate = state.gates.find(g => g.id === conn.to); if (!fromGate || !toGate) return;