Add marquee selection: click and drag on empty space to select multiple symbols

- Blue dashed rectangle drawn while dragging
- Selects all visible symbols whose bounding box intersects the marquee
- Respects hidden symbols and hidden groups

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-21 18:42:21 +04:00
parent 3c532b8cfe
commit 18c0e03287
2 changed files with 42 additions and 0 deletions

View File

@ -548,6 +548,31 @@ function onMousemove(e: MouseEvent) {
} }
layout.markDirty(); layout.markDirty();
} }
if (dragState.type === 'marquee') {
const pos = screenToCanvas(e.clientX, e.clientY);
if (!dragState.dragActivated) {
if (!pastDragThreshold(pos.x, pos.y, dragState.startX!, dragState.startY!, DRAG_THRESHOLD)) return;
dragState.dragActivated = true;
}
const x1 = Math.min(dragState.startX!, pos.x);
const y1 = Math.min(dragState.startY!, pos.y);
const x2 = Math.max(dragState.startX!, pos.x);
const y2 = Math.max(dragState.startY!, pos.y);
marqueeRect = { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
// Select all visible symbols whose AABB intersects the marquee
const selected = new Set<number>();
for (const sym of layout.symbols) {
if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
const bb = getAABB(sym.x, sym.y, sym.w, sym.h, sym.rotation);
if (bb.x + bb.w >= x1 && bb.x <= x2 && bb.y + bb.h >= y1 && bb.y <= y2) {
selected.add(sym.id);
}
}
layout.selectedIds = selected;
layout.markDirty();
}
} }
function onMouseup(e: MouseEvent) { function onMouseup(e: MouseEvent) {
@ -643,6 +668,11 @@ function onMouseup(e: MouseEvent) {
layout.saveMcmState(); layout.saveMcmState();
} }
if (dragState.type === 'marquee') {
marqueeRect = null;
layout.markDirty();
}
dragState = null; dragState = null;
} }

View File

@ -1,6 +1,7 @@
import { layout } from '../stores/layout.svelte.js'; import { layout } from '../stores/layout.svelte.js';
import { getSymbolImage, isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, isPhotoeyeType, getCurveGeometry, getSymbolGroup, SPACING_EXEMPT, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG } from '../symbols.js'; import { getSymbolImage, isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, isPhotoeyeType, getCurveGeometry, getSymbolGroup, SPACING_EXEMPT, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG } from '../symbols.js';
import { checkSpacingViolation } from './collision.js'; import { checkSpacingViolation } from './collision.js';
import { marqueeRect } from './interactions.js';
import { THEME } from './render-theme.js'; import { THEME } from './render-theme.js';
import type { PlacedSymbol } from '../types.js'; import type { PlacedSymbol } from '../types.js';
@ -79,6 +80,17 @@ export function render() {
if (SPACING_EXEMPT.has(sym.symbolId)) drawSymbol(ctx, sym as PlacedSymbol); if (SPACING_EXEMPT.has(sym.symbolId)) drawSymbol(ctx, sym as PlacedSymbol);
} }
// Marquee selection rectangle
if (marqueeRect) {
ctx.strokeStyle = '#4a9eff';
ctx.lineWidth = 1;
ctx.setLineDash([4, 3]);
ctx.fillStyle = 'rgba(74, 158, 255, 0.1)';
ctx.fillRect(marqueeRect.x, marqueeRect.y, marqueeRect.w, marqueeRect.h);
ctx.strokeRect(marqueeRect.x, marqueeRect.y, marqueeRect.w, marqueeRect.h);
ctx.setLineDash([]);
}
ctx.restore(); ctx.restore();
} }