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>
This commit is contained in:
Jose Luis
2026-03-20 04:43:30 +01:00
parent 99f0fefe5c
commit 12d7331d2c
2 changed files with 163 additions and 40 deletions

137
js/bus.js
View File

@@ -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;
}