From 2eead4c1aabe6a8195d4422afbd2036aa3161e05 Mon Sep 17 00:00:00 2001 From: A0 Orchestrator Date: Wed, 1 Jul 2026 23:46:57 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20selection=20bugs=20=E2=80=94=20arrow=20t?= =?UTF-8?q?ool=20click=20select,=20box-select=20on=20empty=20drag,=20prese?= =?UTF-8?q?rve=20selection=20on=20tool=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/canvas/SelectionEngine.ts | 9 ++++++++ frontend/src/interaction/index.ts | 32 ++++++++++++++++++++++---- frontend/tests/SelectionEngine.test.ts | 15 ++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/frontend/src/canvas/SelectionEngine.ts b/frontend/src/canvas/SelectionEngine.ts index 77f9afd..00bc40e 100644 --- a/frontend/src/canvas/SelectionEngine.ts +++ b/frontend/src/canvas/SelectionEngine.ts @@ -294,4 +294,13 @@ export class SelectionEngine { isBoxSelecting(): boolean { return this.boxStart !== null; } + + /** + * Returns true when the box selection drag actually moved away from the start point. + * Used to distinguish a click (no drag) from a drag-based box selection. + */ + hasBoxMoved(): boolean { + if (!this.boxStart || !this.boxEnd) return false; + return this.boxStart.x !== this.boxEnd.x || this.boxStart.y !== this.boxEnd.y; + } } diff --git a/frontend/src/interaction/index.ts b/frontend/src/interaction/index.ts index 2b2a5fc..b0c017e 100644 --- a/frontend/src/interaction/index.ts +++ b/frontend/src/interaction/index.ts @@ -131,7 +131,8 @@ export class InteractionEngine { this.state.phase = 'idle'; this.state.points = []; this.state.previewElement = null; - if (tool !== 'select' && tool !== 'pan') { + const drawingTools: ToolType[] = ['line', 'rect', 'circle', 'arc', 'polyline', 'polygon', 'revcloud', 'text', 'dimension', 'leader', 'chair', 'seating-row', 'seating-block', 'table', 'stage', 'seating-template', 'hatch']; + if (drawingTools.includes(tool)) { this.selectionEngine.clearSelection(); this.emitSelectionChange(); } @@ -368,8 +369,13 @@ export class InteractionEngine { } if (this.state.activeTool === 'select' && this.selectionEngine.isBoxSelecting()) { - this.selectionEngine.finishBoxSelect(this.allElements); - this.emitSelectionChange(); + if (this.selectionEngine.hasBoxMoved()) { + this.selectionEngine.finishBoxSelect(this.allElements); + this.emitSelectionChange(); + } else { + // No drag movement — cancel box select (it was just a click, already handled by clickSelect) + this.selectionEngine.cancelBoxSelect(); + } this.requestRender(); return; } @@ -510,8 +516,24 @@ export class InteractionEngine { this.selectionEngine.setOptions({ additive, subtractive }); if (!additive && !subtractive) { - // Start potential box select - this.selectionEngine.startBoxSelect(world.x, world.y); + // Hit test first: if an object is clicked, select it directly + const hit = this.renderEngine.hitTest(world.x, world.y, 5); + if (hit) { + // Group-aware click selection: if clicked element is in a group, select all group members + if (this.groupManager) { + const groupElements = this.groupManager.getGroupElements(hit.id); + if (groupElements.length > 1) { + this.selectionEngine.selectByIds(groupElements, false); + this.emitSelectionChange(); + return; + } + } + this.selectionEngine.clickSelect(world.x, world.y, this.allElements); + this.emitSelectionChange(); + } else { + // No object hit: start potential box select for drag + this.selectionEngine.startBoxSelect(world.x, world.y); + } } else { // Group-aware click selection: if clicked element is in a group, select all group members const hit = this.renderEngine.hitTest(world.x, world.y, 5); diff --git a/frontend/tests/SelectionEngine.test.ts b/frontend/tests/SelectionEngine.test.ts index 76a9d34..95cbdeb 100644 --- a/frontend/tests/SelectionEngine.test.ts +++ b/frontend/tests/SelectionEngine.test.ts @@ -272,6 +272,21 @@ describe('SelectionEngine', () => { s.selectionEngine.cancelBoxSelect(); expect(s.selectionEngine.isBoxSelecting()).toBe(false); }); + + it('hasBoxMoved should return false when box not started', () => { + expect(s.selectionEngine.hasBoxMoved()).toBe(false); + }); + + it('hasBoxMoved should return false when start equals end (no drag)', () => { + s.selectionEngine.startBoxSelect(50, 50); + expect(s.selectionEngine.hasBoxMoved()).toBe(false); + }); + + it('hasBoxMoved should return true when box end differs from start', () => { + s.selectionEngine.startBoxSelect(50, 50); + s.selectionEngine.updateBoxSelect(100, 100, []); + expect(s.selectionEngine.hasBoxMoved()).toBe(true); + }); }); describe('clearSelection', () => {