Files
logic-gates/js/bus.js
Jose Luis 12d7331d2c refactor: bus now spawns two paired terminals with a bus cable
Instead of a single pass-through gate, shift+drag now creates two
BUS terminals (IN and OUT) connected by a thick bus cable. Internal
connections between terminals are hidden and rendered as a single
cable with /N notation and a diagonal slash. Each terminal is a
thin cyan bar that can be moved independently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 04:43:30 +01:00

223 lines
6.8 KiB
JavaScript

// 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)
const busIn = {
id: busInId,
type: `BUS:${n}`,
x: avgX - gap / 2 - 15,
y: avgY - busH / 2,
value: 0,
outputValues: new Array(n).fill(0),
busRole: 'in',
busPairId: busOutId
};
// 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 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_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,
fromPort: i,
to: orig.to,
toPort: orig.toPort
});
});
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;
}