diff --git a/js/bus.js b/js/bus.js index 167fce5..8ee250c 100644 --- a/js/bus.js +++ b/js/bus.js @@ -1,10 +1,9 @@ -// Bus system — shift+drag to cut wires and create bus connectors +// Bus system — shift+drag to cut wires and create paired bus terminals + cable import { state } from './state.js'; -import { getOutputPorts, getInputPorts, getComponentWidth, getComponentHeight, evaluateAll } from './gates.js'; +import { getOutputPorts, getInputPorts, evaluateAll } from './gates.js'; /** * Sample a cubic bezier curve into discrete line segments. - * Returns array of {x, y} points. */ function sampleBezier(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, steps = 24) { const points = []; @@ -23,8 +22,6 @@ function sampleBezier(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, steps = 24) { /** * Test intersection between two line segments. - * Returns { x, y, t } if they intersect, null otherwise. - * t is the parameter along the first segment (0..1). */ function segmentIntersect(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) { const dx1 = ax2 - ax1, dy1 = ay2 - ay1; @@ -36,17 +33,13 @@ function segmentIntersect(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) { const u = ((bx1 - ax1) * dy1 - (by1 - ay1) * dx1) / d; if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { - return { - x: ax1 + t * dx1, - y: ay1 + t * dy1, - t // position along the CUT line (for sorting) - }; + return { x: ax1 + t * dx1, y: ay1 + t * dy1, t }; } return null; } /** - * Get the bezier control points for a connection, matching drawConnection in renderer.js. + * Get bezier control points for a connection (matching renderer.js drawConnection). */ function getConnectionBezier(conn) { const fromGate = state.gates.find(g => g.id === conn.from); @@ -82,7 +75,6 @@ export function findIntersectingConnections(startX, startY, endX, endY) { bez.p2x, bez.p2y, bez.p3x, bez.p3y ); - // Test each bezier segment against the cut line for (let i = 0; i < points.length - 1; i++) { const hit = segmentIntersect( startX, startY, endX, endY, @@ -90,18 +82,25 @@ export function findIntersectingConnections(startX, startY, endX, endY) { ); if (hit) { results.push({ conn, hitPoint: hit }); - break; // one hit per connection is enough + break; } } } - // Sort by position along the cut line (t parameter) so bus ports are ordered visually results.sort((a, b) => a.hitPoint.t - b.hitPoint.t); return results; } /** - * Create a BUS gate from a cut line and rewire intersecting connections through it. + * Create two paired BUS terminals from a cut line and rewire connections through them. + * + * Layout: + * Wire1 ──┐ ┌── Wire1 + * Wire2 ──┤ ═══════ ├── Wire2 + * Wire3 ──┘ └── Wire3 + * Terminal IN Terminal OUT + * + * The thick cable between them is rendered by the renderer using busPairId. */ export function createBusFromCut() { const cut = state.busCutting; @@ -116,46 +115,108 @@ export function createBusFromCut() { const n = hits.length; console.log(`[bus] cut intersected ${n} connection(s)`); - // Calculate bus position: centroid of all hit points + // Calculate position from hit points const avgX = hits.reduce((s, h) => s + h.hitPoint.x, 0) / n; const avgY = hits.reduce((s, h) => s + h.hitPoint.y, 0) / n; + const busH = Math.max(40, (n + 1) * 22); + const gap = 120; // horizontal distance between the two terminals - // Create BUS gate - const busType = `BUS:${n}`; - const busGate = { - id: state.nextId++, - type: busType, - x: avgX - 15, // half of bus width (30) - y: avgY - Math.max(40, (n + 1) * 22) / 2, + // Reserve IDs + const busInId = state.nextId++; + const busOutId = state.nextId++; + + // Create BUS_IN terminal (left — collects wires into bus) + const busIn = { + id: busInId, + type: `BUS:${n}`, + x: avgX - gap / 2 - 15, + y: avgY - busH / 2, value: 0, - outputValues: new Array(n).fill(0) + outputValues: new Array(n).fill(0), + busRole: 'in', + busPairId: busOutId }; - state.gates.push(busGate); - // Rewire: for each intersected connection, split through the bus + // Create BUS_OUT terminal (right — distributes bus back to wires) + const busOut = { + id: busOutId, + type: `BUS:${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 hits.forEach((hit, i) => { - const origConn = hit.conn; + const orig = hit.conn; - // Remove the original connection - state.connections = state.connections.filter(c => c !== origConn); + // Remove original connection + state.connections = state.connections.filter(c => c !== orig); - // Source → BUS input port i + // Source → BUS_IN input[i] state.connections.push({ - from: origConn.from, - fromPort: origConn.fromPort, - to: busGate.id, + from: orig.from, + fromPort: orig.fromPort, + to: busIn.id, toPort: i }); - // BUS output port i → original destination + // BUS_IN output[i] → BUS_OUT input[i] (internal bus link, rendered as cable) state.connections.push({ - from: busGate.id, + from: busIn.id, fromPort: i, - to: origConn.to, - toPort: origConn.toPort + to: busOut.id, + toPort: i + }); + + // BUS_OUT output[i] → original destination + state.connections.push({ + from: busOut.id, + fromPort: i, + to: orig.to, + toPort: orig.toPort }); }); - console.log(`[bus] created BUS#${busGate.id} with ${n} channels`); + console.log(`[bus] created BUS_IN#${busIn.id} ↔ BUS_OUT#${busOut.id} with ${n} channels`); 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. + */ +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; + 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 }); + } + return pairs; +} diff --git a/js/renderer.js b/js/renderer.js index a0e0303..cba61e8 100644 --- a/js/renderer.js +++ b/js/renderer.js @@ -3,6 +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'; let canvas, ctx; @@ -157,12 +158,13 @@ function drawBusGate(gate) { ctx.lineTo(gate.x + w / 2, gate.y + h - 6); ctx.stroke(); - // Bus size label + // Bus size label + role indicator ctx.font = 'bold 9px monospace'; ctx.fillStyle = color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText(`${n}`, gate.x + w / 2, gate.y + h + 10); + const roleIcon = gate.busRole === 'in' ? '▶' : gate.busRole === 'out' ? '◀' : ''; + ctx.fillText(`${roleIcon} ${n}`, gate.x + w / 2, gate.y + h + 10); // Input ports (left) getInputPorts(gate).forEach(p => { @@ -200,6 +202,62 @@ function drawBusGate(gate) { }); } +function drawBusCables() { + const pairs = getBusPairs(); + for (const { inGate, outGate } of pairs) { + const inW = getComponentWidth(inGate); + const inH = getComponentHeight(inGate); + const outH = getComponentHeight(outGate); + + // Cable runs from right edge of inGate to left edge of outGate + const x1 = inGate.x + inW; + const y1 = inGate.y + inH / 2; + const x2 = outGate.x; + 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; + + // Outer thick cable (bus background) + const cableWidth = Math.max(6, n * 2.5); + ctx.beginPath(); + ctx.moveTo(x1, y1); + const midX = (x1 + x2) / 2; + ctx.bezierCurveTo(midX, y1, midX, y2, x2, y2); + ctx.strokeStyle = hasActive ? '#44ddff33' : '#44ddff11'; + ctx.lineWidth = cableWidth + 4; + ctx.stroke(); + + // Inner cable + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.bezierCurveTo(midX, y1, midX, y2, x2, y2); + ctx.strokeStyle = hasActive ? '#44ddff' : '#44ddff66'; + ctx.lineWidth = cableWidth; + ctx.stroke(); + + // Channel count label at midpoint + const labelX = (x1 + x2) / 2; + const labelY = (y1 + y2) / 2 - cableWidth / 2 - 6; + ctx.font = 'bold 10px monospace'; + ctx.fillStyle = '#44ddff'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(`/${n}`, labelX, labelY); + + // Draw small diagonal slash across cable (bus notation) + const slashX = labelX; + const slashY = (y1 + y2) / 2; + ctx.beginPath(); + ctx.moveTo(slashX - 6, slashY + 6); + ctx.lineTo(slashX + 6, slashY - 6); + ctx.strokeStyle = '#44ddff'; + ctx.lineWidth = 1.5; + ctx.stroke(); + } +} + function drawComponentGate(gate) { const isHovered = state.hoveredGate === gate; const isActive = gate.value === 1; @@ -309,6 +367,9 @@ 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; @@ -507,6 +568,7 @@ function draw() { drawGrid(); state.connections.forEach(drawConnection); + drawBusCables(); state.gates.forEach(drawGate); drawConnectingWire(); drawBusCutLine();