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 <noreply@anthropic.com>
198 lines
6.1 KiB
JavaScript
198 lines
6.1 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, 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;
|
|
}
|