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:
137
js/bus.js
137
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 { 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.
|
* 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) {
|
function sampleBezier(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, steps = 24) {
|
||||||
const points = [];
|
const points = [];
|
||||||
@@ -23,8 +22,6 @@ function sampleBezier(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, steps = 24) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test intersection between two line segments.
|
* 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) {
|
function segmentIntersect(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) {
|
||||||
const dx1 = ax2 - ax1, dy1 = ay2 - ay1;
|
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;
|
const u = ((bx1 - ax1) * dy1 - (by1 - ay1) * dx1) / d;
|
||||||
|
|
||||||
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
|
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
|
||||||
return {
|
return { x: ax1 + t * dx1, y: ay1 + t * dy1, t };
|
||||||
x: ax1 + t * dx1,
|
|
||||||
y: ay1 + t * dy1,
|
|
||||||
t // position along the CUT line (for sorting)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return null;
|
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) {
|
function getConnectionBezier(conn) {
|
||||||
const fromGate = state.gates.find(g => g.id === conn.from);
|
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
|
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++) {
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
const hit = segmentIntersect(
|
const hit = segmentIntersect(
|
||||||
startX, startY, endX, endY,
|
startX, startY, endX, endY,
|
||||||
@@ -90,18 +82,25 @@ export function findIntersectingConnections(startX, startY, endX, endY) {
|
|||||||
);
|
);
|
||||||
if (hit) {
|
if (hit) {
|
||||||
results.push({ conn, hitPoint: 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);
|
results.sort((a, b) => a.hitPoint.t - b.hitPoint.t);
|
||||||
return results;
|
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() {
|
export function createBusFromCut() {
|
||||||
const cut = state.busCutting;
|
const cut = state.busCutting;
|
||||||
@@ -116,46 +115,108 @@ export function createBusFromCut() {
|
|||||||
const n = hits.length;
|
const n = hits.length;
|
||||||
console.log(`[bus] cut intersected ${n} connection(s)`);
|
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 avgX = hits.reduce((s, h) => s + h.hitPoint.x, 0) / n;
|
||||||
const avgY = hits.reduce((s, h) => s + h.hitPoint.y, 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
|
// Reserve IDs
|
||||||
const busType = `BUS:${n}`;
|
const busInId = state.nextId++;
|
||||||
const busGate = {
|
const busOutId = state.nextId++;
|
||||||
id: state.nextId++,
|
|
||||||
type: busType,
|
// Create BUS_IN terminal (left — collects wires into bus)
|
||||||
x: avgX - 15, // half of bus width (30)
|
const busIn = {
|
||||||
y: avgY - Math.max(40, (n + 1) * 22) / 2,
|
id: busInId,
|
||||||
|
type: `BUS:${n}`,
|
||||||
|
x: avgX - gap / 2 - 15,
|
||||||
|
y: avgY - busH / 2,
|
||||||
value: 0,
|
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) => {
|
hits.forEach((hit, i) => {
|
||||||
const origConn = hit.conn;
|
const orig = hit.conn;
|
||||||
|
|
||||||
// Remove the original connection
|
// Remove original connection
|
||||||
state.connections = state.connections.filter(c => c !== origConn);
|
state.connections = state.connections.filter(c => c !== orig);
|
||||||
|
|
||||||
// Source → BUS input port i
|
// Source → BUS_IN input[i]
|
||||||
state.connections.push({
|
state.connections.push({
|
||||||
from: origConn.from,
|
from: orig.from,
|
||||||
fromPort: origConn.fromPort,
|
fromPort: orig.fromPort,
|
||||||
to: busGate.id,
|
to: busIn.id,
|
||||||
toPort: i
|
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({
|
state.connections.push({
|
||||||
from: busGate.id,
|
from: busIn.id,
|
||||||
fromPort: i,
|
fromPort: i,
|
||||||
to: origConn.to,
|
to: busOut.id,
|
||||||
toPort: origConn.toPort
|
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();
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { GATE_W, GATE_H, COMP_W, PORT_R, GATE_COLORS } from './constants.js';
|
|||||||
import { state } from './state.js';
|
import { state } from './state.js';
|
||||||
import { getInputPorts, getOutputPorts, getComponentWidth, getComponentHeight } from './gates.js';
|
import { getInputPorts, getOutputPorts, getComponentWidth, getComponentHeight } from './gates.js';
|
||||||
import { getGateLabel, drawWaveLabels, drawWaveforms } from './waveform.js';
|
import { getGateLabel, drawWaveLabels, drawWaveforms } from './waveform.js';
|
||||||
|
import { isBusInternalConnection, getBusPairs } from './bus.js';
|
||||||
|
|
||||||
let canvas, ctx;
|
let canvas, ctx;
|
||||||
|
|
||||||
@@ -157,12 +158,13 @@ function drawBusGate(gate) {
|
|||||||
ctx.lineTo(gate.x + w / 2, gate.y + h - 6);
|
ctx.lineTo(gate.x + w / 2, gate.y + h - 6);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Bus size label
|
// Bus size label + role indicator
|
||||||
ctx.font = 'bold 9px monospace';
|
ctx.font = 'bold 9px monospace';
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
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)
|
// Input ports (left)
|
||||||
getInputPorts(gate).forEach(p => {
|
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) {
|
function drawComponentGate(gate) {
|
||||||
const isHovered = state.hoveredGate === gate;
|
const isHovered = state.hoveredGate === gate;
|
||||||
const isActive = gate.value === 1;
|
const isActive = gate.value === 1;
|
||||||
@@ -309,6 +367,9 @@ function drawComponentGate(gate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function drawConnection(conn) {
|
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 fromGate = state.gates.find(g => g.id === conn.from);
|
||||||
const toGate = state.gates.find(g => g.id === conn.to);
|
const toGate = state.gates.find(g => g.id === conn.to);
|
||||||
if (!fromGate || !toGate) return;
|
if (!fromGate || !toGate) return;
|
||||||
@@ -507,6 +568,7 @@ function draw() {
|
|||||||
|
|
||||||
drawGrid();
|
drawGrid();
|
||||||
state.connections.forEach(drawConnection);
|
state.connections.forEach(drawConnection);
|
||||||
|
drawBusCables();
|
||||||
state.gates.forEach(drawGate);
|
state.gates.forEach(drawGate);
|
||||||
drawConnectingWire();
|
drawConnectingWire();
|
||||||
drawBusCutLine();
|
drawBusCutLine();
|
||||||
|
|||||||
Reference in New Issue
Block a user