From 18c0e03287271aff4098dcb5a4b6ed16941f8381 Mon Sep 17 00:00:00 2001 From: igurielidze Date: Sat, 21 Mar 2026 18:42:21 +0400 Subject: [PATCH] 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) --- svelte-app/src/lib/canvas/interactions.ts | 30 +++++++++++++++++++++++ svelte-app/src/lib/canvas/renderer.ts | 12 +++++++++ 2 files changed, 42 insertions(+) diff --git a/svelte-app/src/lib/canvas/interactions.ts b/svelte-app/src/lib/canvas/interactions.ts index e5b0a7c..b0ba753 100644 --- a/svelte-app/src/lib/canvas/interactions.ts +++ b/svelte-app/src/lib/canvas/interactions.ts @@ -548,6 +548,31 @@ function onMousemove(e: MouseEvent) { } 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(); + 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) { @@ -643,6 +668,11 @@ function onMouseup(e: MouseEvent) { layout.saveMcmState(); } + if (dragState.type === 'marquee') { + marqueeRect = null; + layout.markDirty(); + } + dragState = null; } diff --git a/svelte-app/src/lib/canvas/renderer.ts b/svelte-app/src/lib/canvas/renderer.ts index 3dcb8d9..f6b633e 100644 --- a/svelte-app/src/lib/canvas/renderer.ts +++ b/svelte-app/src/lib/canvas/renderer.ts @@ -1,6 +1,7 @@ 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 { checkSpacingViolation } from './collision.js'; +import { marqueeRect } from './interactions.js'; import { THEME } from './render-theme.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); } + // 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(); }