// Bus system — shift+drag to cut wires and create paired bus terminals + cable import { state } from './state.js'; import { getOutputPorts, getInputPorts, evaluateAll } from './gates.js'; /** * Sample a cubic bezier curve into discrete line segments. */ function sampleBezier(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, steps = 24) { const points = []; for (let i = 0; i <= steps; i++) { const t = i / steps; const mt = 1 - t; const mt2 = mt * mt, mt3 = mt2 * mt; const t2 = t * t, t3 = t2 * t; points.push({ x: mt3 * p0x + 3 * mt2 * t * p1x + 3 * mt * t2 * p2x + t3 * p3x, y: mt3 * p0y + 3 * mt2 * t * p1y + 3 * mt * t2 * p2y + t3 * p3y }); } return points; } /** * Test intersection between two line segments. */ function segmentIntersect(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) { const dx1 = ax2 - ax1, dy1 = ay2 - ay1; const dx2 = bx2 - bx1, dy2 = by2 - by1; const d = dx1 * dy2 - dy1 * dx2; if (Math.abs(d) < 1e-10) return null; const t = ((bx1 - ax1) * dy2 - (by1 - ay1) * dx2) / d; 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 }; } return null; } /** * Get bezier control points for a connection (matching renderer.js drawConnection). */ function getConnectionBezier(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 null; const fromPort = getOutputPorts(fromGate)[conn.fromPort]; const toPort = getInputPorts(toGate)[conn.toPort]; if (!fromPort || !toPort) return null; const midX = (fromPort.x + toPort.x) / 2; return { p0x: fromPort.x, p0y: fromPort.y, p1x: midX, p1y: fromPort.y, p2x: midX, p2y: toPort.y, p3x: toPort.x, p3y: toPort.y }; } /** * Find all connections that intersect a cut line. * Returns array of { conn, hitPoint } sorted by position along the cut line. */ export function findIntersectingConnections(startX, startY, endX, endY) { const results = []; for (const conn of state.connections) { const bez = getConnectionBezier(conn); if (!bez) continue; const points = sampleBezier( bez.p0x, bez.p0y, bez.p1x, bez.p1y, bez.p2x, bez.p2y, bez.p3x, bez.p3y ); for (let i = 0; i < points.length - 1; i++) { const hit = segmentIntersect( startX, startY, endX, endY, points[i].x, points[i].y, points[i + 1].x, points[i + 1].y ); if (hit) { results.push({ conn, hitPoint: hit }); break; } } } results.sort((a, b) => a.hitPoint.t - b.hitPoint.t); return results; } /** * 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; if (!cut) return; const hits = findIntersectingConnections(cut.startX, cut.startY, cut.endX, cut.endY); if (hits.length === 0) { console.log('[bus] no connections intersected'); return; } const n = hits.length; console.log(`[bus] cut intersected ${n} connection(s)`); // 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 // Reserve IDs const busInId = state.nextId++; const busOutId = state.nextId++; // Create BUS_IN terminal (left — collects wires into bus, only input pins) const busIn = { id: busInId, type: `BUS_IN:${n}`, x: avgX - gap / 2 - 15, y: avgY - busH / 2, value: 0, busValues: new Array(n).fill(0), busPairId: busOutId }; // Create BUS_OUT terminal (right — distributes bus back to wires, only output pins) const busOut = { id: busOutId, type: `BUS_OUT:${n}`, x: avgX + gap / 2 - 15, y: avgY - busH / 2, value: 0, outputValues: new Array(n).fill(0), busPairId: busInId }; state.gates.push(busIn, busOut); // 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; // Remove original connection state.connections = state.connections.filter(c => c !== orig); // Source → BUS_IN input[i] state.connections.push({ from: orig.from, fromPort: orig.fromPort, to: busIn.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_IN#${busIn.id} ↔ BUS_OUT#${busOut.id} with ${n} channels`); evaluateAll(); } /** * 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_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(outGate.id); pairs.push({ inGate: gate, outGate }); } return pairs; }