feat: shift+drag to cut wires and create bus connectors
Hold Shift and drag across wires to create a BUS gate that groups them together. The cut line shows a live preview with wire count. BUS gates are pass-through (each input maps to its output) and render as a thin cyan bar with ports on each side. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
162
js/renderer.js
162
js/renderer.js
@@ -32,10 +32,9 @@ export function screenToWorld(sx, sy) {
|
||||
}
|
||||
|
||||
function drawGate(gate) {
|
||||
// Component gates have different rendering
|
||||
if (gate.type.startsWith('COMPONENT:')) {
|
||||
return drawComponentGate(gate);
|
||||
}
|
||||
// Special gate types have different rendering
|
||||
if (gate.type.startsWith('BUS:')) return drawBusGate(gate);
|
||||
if (gate.type.startsWith('COMPONENT:')) return drawComponentGate(gate);
|
||||
|
||||
const color = GATE_COLORS[gate.type];
|
||||
const isHovered = state.hoveredGate === gate;
|
||||
@@ -127,6 +126,80 @@ function drawGate(gate) {
|
||||
});
|
||||
}
|
||||
|
||||
function drawBusGate(gate) {
|
||||
const isHovered = state.hoveredGate === gate;
|
||||
const w = getComponentWidth(gate); // 30
|
||||
const h = getComponentHeight(gate);
|
||||
const color = '#44ddff';
|
||||
const n = parseInt(gate.type.substring(4)) || 1;
|
||||
const hasActive = gate.outputValues?.some(v => v === 1);
|
||||
|
||||
if (hasActive) {
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 12 * state.zoom;
|
||||
}
|
||||
|
||||
// Main bus bar
|
||||
ctx.fillStyle = hasActive ? color + '22' : '#14141e';
|
||||
ctx.strokeStyle = isHovered ? '#fff' : color;
|
||||
ctx.lineWidth = isHovered ? 2.5 : 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(gate.x, gate.y, w, h, 4);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Thick center line (bus bar visual)
|
||||
ctx.strokeStyle = isHovered ? '#fff' : color;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(gate.x + w / 2, gate.y + 6);
|
||||
ctx.lineTo(gate.x + w / 2, gate.y + h - 6);
|
||||
ctx.stroke();
|
||||
|
||||
// Bus size label
|
||||
ctx.font = 'bold 9px monospace';
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(`${n}`, gate.x + w / 2, gate.y + h + 10);
|
||||
|
||||
// Input ports (left)
|
||||
getInputPorts(gate).forEach(p => {
|
||||
const isPortHovered = state.hoveredPort &&
|
||||
state.hoveredPort.gate === gate &&
|
||||
state.hoveredPort.index === p.index &&
|
||||
state.hoveredPort.type === 'input';
|
||||
const conn = state.connections.find(c => c.to === gate.id && c.toPort === p.index);
|
||||
const portActive = conn ? state.gates.find(g => g.id === conn.from)?.value : 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, PORT_R - 1, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e');
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// Output ports (right)
|
||||
getOutputPorts(gate).forEach(p => {
|
||||
const isPortHovered = state.hoveredPort &&
|
||||
state.hoveredPort.gate === gate &&
|
||||
state.hoveredPort.index === p.index &&
|
||||
state.hoveredPort.type === 'output';
|
||||
const portVal = gate.outputValues ? (gate.outputValues[p.index] || 0) : 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, PORT_R - 1, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isPortHovered ? '#fff' : (portVal ? '#00ff88' : '#1a1a2e');
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
function drawComponentGate(gate) {
|
||||
const isHovered = state.hoveredGate === gate;
|
||||
const isActive = gate.value === 1;
|
||||
@@ -305,6 +378,86 @@ function drawConnectingWire() {
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
function drawBusCutLine() {
|
||||
if (!state.busCutting) return;
|
||||
const cut = state.busCutting;
|
||||
|
||||
// Dashed cyan line showing the cut
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cut.startX, cut.startY);
|
||||
ctx.lineTo(cut.endX, cut.endY);
|
||||
ctx.strokeStyle = '#44ddff';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.setLineDash([8, 5]);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Small circles at endpoints
|
||||
ctx.beginPath();
|
||||
ctx.arc(cut.startX, cut.startY, 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#44ddff';
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cut.endX, cut.endY, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Highlight intersecting wires
|
||||
// Import is circular so we compute inline: sample bezier + test intersection
|
||||
const hits = countCutIntersections(cut);
|
||||
if (hits > 0) {
|
||||
ctx.save();
|
||||
ctx.resetTransform();
|
||||
ctx.fillStyle = '#44ddff';
|
||||
ctx.font = 'bold 12px "Segoe UI", system-ui, sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`✂ ${hits} wire${hits > 1 ? 's' : ''}`, 10, canvas.height - 10);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick inline intersection count for preview (avoids circular import from bus.js)
|
||||
*/
|
||||
function countCutIntersections(cut) {
|
||||
let count = 0;
|
||||
for (const conn of state.connections) {
|
||||
const fromGate = state.gates.find(g => g.id === conn.from);
|
||||
const toGate = state.gates.find(g => g.id === conn.to);
|
||||
if (!fromGate || !toGate) continue;
|
||||
const fp = getOutputPorts(fromGate)[conn.fromPort];
|
||||
const tp = getInputPorts(toGate)[conn.toPort];
|
||||
if (!fp || !tp) continue;
|
||||
|
||||
const midX = (fp.x + tp.x) / 2;
|
||||
// Sample bezier at 16 points
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const t1 = i / 16, t2 = (i + 1) / 16;
|
||||
const bx1 = bezAt(fp.x, midX, midX, tp.x, t1);
|
||||
const by1 = bezAt(fp.y, fp.y, tp.y, tp.y, t1);
|
||||
const bx2 = bezAt(fp.x, midX, midX, tp.x, t2);
|
||||
const by2 = bezAt(fp.y, fp.y, tp.y, tp.y, t2);
|
||||
if (segsHit(cut.startX, cut.startY, cut.endX, cut.endY, bx1, by1, bx2, by2)) {
|
||||
count++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function bezAt(p0, p1, p2, p3, t) {
|
||||
const mt = 1 - t;
|
||||
return mt * mt * mt * p0 + 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t * p3;
|
||||
}
|
||||
|
||||
function segsHit(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) {
|
||||
const d = (ax2 - ax1) * (by2 - by1) - (ay2 - ay1) * (bx2 - bx1);
|
||||
if (Math.abs(d) < 1e-10) return false;
|
||||
const t = ((bx1 - ax1) * (by2 - by1) - (by1 - ay1) * (bx2 - bx1)) / d;
|
||||
const u = ((bx1 - ax1) * (ay2 - ay1) - (by1 - ay1) * (ax2 - ax1)) / d;
|
||||
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
|
||||
}
|
||||
|
||||
function drawPlacingGhost() {
|
||||
if (!state.placingGate) return;
|
||||
ctx.globalAlpha = 0.5;
|
||||
@@ -356,6 +509,7 @@ function draw() {
|
||||
state.connections.forEach(drawConnection);
|
||||
state.gates.forEach(drawGate);
|
||||
drawConnectingWire();
|
||||
drawBusCutLine();
|
||||
drawPlacingGhost();
|
||||
|
||||
ctx.restore();
|
||||
|
||||
Reference in New Issue
Block a user