From aa7e588980c4f4bec89995f5432b1c74a3744236 Mon Sep 17 00:00:00 2001 From: Nettika Date: Thu, 29 Jan 2026 01:26:33 -0800 Subject: [PATCH] Implement object selection --- TODO.md | 6 ++--- src/App.vue | 13 +++++++++- src/stores/scene.spec.ts | 56 ++++++++++++++++++++++++++++++++++++++++ src/stores/scene.ts | 18 +++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index 2444a1caa..eaee38e68 100644 --- a/TODO.md +++ b/TODO.md @@ -87,9 +87,9 @@ A step-by-step checklist for porting MatterControl's design features to a Vue + ### Object Picking - [x] Add Raycaster for mouse picking -- [ ] Implement click-to-select single object -- [ ] Implement click-on-empty to deselect -- [ ] Add `selectedObjects` array to Pinia store +- [x] Implement click-to-select single object +- [x] Implement click-on-empty to deselect +- [x] Add `selectedObjects` array to Pinia store - [ ] Implement Shift+click for multi-select - [ ] Implement Ctrl+click to toggle selection - [ ] Add "Select All" (Ctrl+A) shortcut diff --git a/src/App.vue b/src/App.vue index 19bf8907d..6d0f0b101 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,22 @@ diff --git a/src/stores/scene.spec.ts b/src/stores/scene.spec.ts index e2deb906b..eb6f61131 100644 --- a/src/stores/scene.spec.ts +++ b/src/stores/scene.spec.ts @@ -193,4 +193,60 @@ describe('useSceneStore', () => { expect(list[1]?.name).toBe('B') }) }) + + describe('selection', () => { + it('select() selects a single object', () => { + const obj = store.addObject(createTestMesh(), 'Test') + + store.select(obj.id) + + expect(store.selectedObjects).toHaveLength(1) + expect(store.selectedObjects[0]?.id).toBe(obj.id) + }) + + it('select() replaces previous selection', () => { + const obj1 = store.addObject(createTestMesh(), 'A') + const obj2 = store.addObject(createTestMesh(), 'B') + + store.select(obj1.id) + store.select(obj2.id) + + expect(store.selectedObjects).toHaveLength(1) + expect(store.selectedObjects[0]?.id).toBe(obj2.id) + }) + + it('select() ignores locked objects', () => { + const obj = store.addObject(createTestMesh(), 'Test') + store.setObjectLocked(obj.id, true) + + store.select(obj.id) + + expect(store.selectedObjects).toHaveLength(0) + }) + + it('select() ignores non-existent ids', () => { + store.select('nonexistent') + + expect(store.selectedObjects).toHaveLength(0) + }) + + it('clearSelection() deselects all objects', () => { + const obj = store.addObject(createTestMesh(), 'Test') + store.select(obj.id) + + store.clearSelection() + + expect(store.selectedObjects).toHaveLength(0) + }) + + it('selectedObjects returns SceneObjects for selected ids', () => { + const obj1 = store.addObject(createTestMesh(), 'A') + store.addObject(createTestMesh(), 'B') + + store.select(obj1.id) + + expect(store.selectedObjects).toHaveLength(1) + expect(store.selectedObjects[0]?.name).toBe('A') + }) + }) }) diff --git a/src/stores/scene.ts b/src/stores/scene.ts index 56b132942..45b7b0f9b 100644 --- a/src/stores/scene.ts +++ b/src/stores/scene.ts @@ -13,6 +13,7 @@ export interface SceneObject { export const useSceneStore = defineStore('scene', () => { // Use shallowRef to avoid Vue making THREE.Mesh objects deeply reactive const objects = shallowRef([]) + const selectedIds = shallowRef>(new Set()) let threeScene: THREE.Scene | null = null let nextId = 1 @@ -108,15 +109,30 @@ export const useSceneStore = defineStore('scene', () => { ) } + function select(id: string): void { + const obj = objects.value.find((o) => o.id === id) + if (!obj || obj.locked) return + selectedIds.value = new Set([id]) + } + + function clearSelection(): void { + selectedIds.value = new Set() + } + const objectList = computed(() => objects.value) const objectCount = computed(() => objects.value.length) + const selectedObjects = computed(() => + objects.value.filter((o) => selectedIds.value.has(o.id)) + ) return { // State objects, + selectedIds, // Getters objectList, objectCount, + selectedObjects, // Actions setScene, addObject, @@ -126,5 +142,7 @@ export const useSceneStore = defineStore('scene', () => { setObjectVisible, setObjectLocked, renameObject, + select, + clearSelection, } })