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', () => {