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,
}
})