feat: drag selection box to select, move, and delete multiple gates
Click and drag on empty space to draw a selection rectangle. Gates inside the box get selected (cyan dashed outline). Drag any selected gate to move all of them together. Delete/Backspace removes all selected gates and their connections. Escape clears the selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
108
js/events.js
108
js/events.js
@@ -40,6 +40,35 @@ export function initEvents() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update selection box
|
||||
if (state.selectionBox) {
|
||||
state.selectionBox.endX = world.x;
|
||||
state.selectionBox.endY = world.y;
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-drag selected gates
|
||||
if (state.multiDrag) {
|
||||
if (dragStartPos && !dragMoved) {
|
||||
const dx = e.offsetX - dragStartPos.x;
|
||||
const dy = e.offsetY - dragStartPos.y;
|
||||
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) dragMoved = true;
|
||||
}
|
||||
if (dragMoved) {
|
||||
const dx = world.x - state.multiDrag.startX;
|
||||
const dy = world.y - state.multiDrag.startY;
|
||||
for (const orig of state.multiDrag.origins) {
|
||||
const gate = state.gates.find(g => g.id === orig.id);
|
||||
if (gate) {
|
||||
gate.x = orig.x + dx;
|
||||
gate.y = orig.y + dy;
|
||||
}
|
||||
}
|
||||
evaluateAll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.hoveredPort = findPortAt(world.x, world.y);
|
||||
state.hoveredGate = state.hoveredPort ? state.hoveredPort.gate : findGateAt(world.x, world.y);
|
||||
|
||||
@@ -60,6 +89,7 @@ export function initEvents() {
|
||||
}
|
||||
|
||||
canvas.style.cursor = state.placingGate ? 'crosshair'
|
||||
: state.selectionBox ? 'crosshair'
|
||||
: state.hoveredPort ? 'pointer'
|
||||
: state.hoveredGate ? 'grab'
|
||||
: 'default';
|
||||
@@ -147,10 +177,33 @@ export function initEvents() {
|
||||
// Drag any gate (including INPUT/CLOCK)
|
||||
const gate = findGateAt(world.x, world.y);
|
||||
if (gate) {
|
||||
// If clicking a selected gate → multi-drag all selected
|
||||
if (state.selectedGates.includes(gate.id)) {
|
||||
state.multiDrag = {
|
||||
startX: world.x,
|
||||
startY: world.y,
|
||||
origins: state.selectedGates.map(id => {
|
||||
const g = state.gates.find(g => g.id === id);
|
||||
return g ? { id: g.id, x: g.x, y: g.y } : null;
|
||||
}).filter(Boolean)
|
||||
};
|
||||
canvas.style.cursor = 'grabbing';
|
||||
return;
|
||||
}
|
||||
// Clicking an unselected gate → clear selection, drag just this one
|
||||
state.selectedGates = [];
|
||||
state.dragging = gate;
|
||||
state.dragOffset = { x: world.x - gate.x, y: world.y - gate.y };
|
||||
canvas.style.cursor = 'grabbing';
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on empty space → clear selection and start selection box
|
||||
state.selectedGates = [];
|
||||
state.selectionBox = {
|
||||
startX: world.x, startY: world.y,
|
||||
endX: world.x, endY: world.y
|
||||
};
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', e => {
|
||||
@@ -161,6 +214,41 @@ export function initEvents() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Finish selection box → select gates inside
|
||||
if (state.selectionBox) {
|
||||
const box = state.selectionBox;
|
||||
const x1 = Math.min(box.startX, box.endX);
|
||||
const y1 = Math.min(box.startY, box.endY);
|
||||
const x2 = Math.max(box.startX, box.endX);
|
||||
const y2 = Math.max(box.startY, box.endY);
|
||||
|
||||
// Only select if box is big enough (not just a click)
|
||||
if (Math.abs(x2 - x1) > 5 || Math.abs(y2 - y1) > 5) {
|
||||
state.selectedGates = state.gates
|
||||
.filter(g => {
|
||||
const isDynamic = g.type.startsWith('COMPONENT:') || g.type.startsWith('BUS:');
|
||||
const gw = isDynamic ? getComponentWidth(g) : GATE_W;
|
||||
const gh = isDynamic ? getComponentHeight(g) : GATE_H;
|
||||
// Gate overlaps selection box
|
||||
return g.x + gw > x1 && g.x < x2 && g.y + gh > y1 && g.y < y2;
|
||||
})
|
||||
.map(g => g.id);
|
||||
if (state.selectedGates.length > 0) {
|
||||
console.log(`[select] ${state.selectedGates.length} gate(s) selected`);
|
||||
}
|
||||
}
|
||||
state.selectionBox = null;
|
||||
dragStartPos = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Finish multi-drag
|
||||
if (state.multiDrag) {
|
||||
state.multiDrag = null;
|
||||
dragStartPos = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle INPUT/CLOCK only on click (no drag movement)
|
||||
if (state.dragging && !dragMoved) {
|
||||
const gate = state.dragging;
|
||||
@@ -168,7 +256,6 @@ export function initEvents() {
|
||||
gate.value = gate.value ? 0 : 1;
|
||||
console.log(`[toggle] ${gate.type}#${gate.id} → ${gate.value}`);
|
||||
evaluateAll(true); // record waveform on intentional toggle
|
||||
// Log all gate values after evaluation
|
||||
console.log('[state]', state.gates.map(g => `${g.type}#${g.id}=${g.value}`).join(', '));
|
||||
}
|
||||
}
|
||||
@@ -241,8 +328,22 @@ export function initEvents() {
|
||||
keysDown.add(e.key);
|
||||
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
if (state.hoveredGate && document.activeElement === document.body) {
|
||||
e.preventDefault();
|
||||
if (document.activeElement !== document.body) return;
|
||||
e.preventDefault();
|
||||
|
||||
// Delete all selected gates
|
||||
if (state.selectedGates.length > 0) {
|
||||
for (const gateId of state.selectedGates) {
|
||||
state.connections = state.connections.filter(c => c.from !== gateId && c.to !== gateId);
|
||||
state.gates = state.gates.filter(g => g.id !== gateId);
|
||||
delete state.waveData[gateId];
|
||||
}
|
||||
console.log(`[delete] removed ${state.selectedGates.length} gate(s)`);
|
||||
state.selectedGates = [];
|
||||
state.hoveredGate = null;
|
||||
evaluateAll();
|
||||
} else if (state.hoveredGate) {
|
||||
// Delete single hovered gate
|
||||
const gateId = state.hoveredGate.id;
|
||||
state.connections = state.connections.filter(c => c.from !== gateId && c.to !== gateId);
|
||||
state.gates = state.gates.filter(g => g.id !== gateId);
|
||||
@@ -254,6 +355,7 @@ export function initEvents() {
|
||||
if (e.key === 'Escape') {
|
||||
state.placingGate = null;
|
||||
state.connecting = null;
|
||||
state.selectedGates = [];
|
||||
}
|
||||
|
||||
// Pan with arrow keys
|
||||
|
||||
Reference in New Issue
Block a user