Implement object selection

This commit is contained in:
Nettika 2026-01-29 01:26:33 -08:00
parent 08cdf0d41a
commit aa7e588980
No known key found for this signature in database
4 changed files with 89 additions and 4 deletions

View file

@ -87,9 +87,9 @@ A step-by-step checklist for porting MatterControl's design features to a Vue +
### Object Picking ### Object Picking
- [x] Add Raycaster for mouse picking - [x] Add Raycaster for mouse picking
- [ ] Implement click-to-select single object - [x] Implement click-to-select single object
- [ ] Implement click-on-empty to deselect - [x] Implement click-on-empty to deselect
- [ ] Add `selectedObjects` array to Pinia store - [x] Add `selectedObjects` array to Pinia store
- [ ] Implement Shift+click for multi-select - [ ] Implement Shift+click for multi-select
- [ ] Implement Ctrl+click to toggle selection - [ ] Implement Ctrl+click to toggle selection
- [ ] Add "Select All" (Ctrl+A) shortcut - [ ] Add "Select All" (Ctrl+A) shortcut

View file

@ -1,11 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import AppLayout from './components/AppLayout.vue' import AppLayout from './components/AppLayout.vue'
import Viewport from './components/Viewport.vue' import Viewport from './components/Viewport.vue'
import { useSceneStore, type SceneObject } from './stores/scene'
const sceneStore = useSceneStore()
function handlePick(object: SceneObject | null) {
if (object) {
sceneStore.select(object.id)
} else {
sceneStore.clearSelection()
}
}
</script> </script>
<template> <template>
<AppLayout> <AppLayout>
<Viewport /> <Viewport @pick="handlePick" />
</AppLayout> </AppLayout>
</template> </template>

View file

@ -193,4 +193,60 @@ describe('useSceneStore', () => {
expect(list[1]?.name).toBe('B') 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')
})
})
}) })

View file

@ -13,6 +13,7 @@ export interface SceneObject {
export const useSceneStore = defineStore('scene', () => { export const useSceneStore = defineStore('scene', () => {
// Use shallowRef to avoid Vue making THREE.Mesh objects deeply reactive // Use shallowRef to avoid Vue making THREE.Mesh objects deeply reactive
const objects = shallowRef<SceneObject[]>([]) const objects = shallowRef<SceneObject[]>([])
const selectedIds = shallowRef<Set<string>>(new Set())
let threeScene: THREE.Scene | null = null let threeScene: THREE.Scene | null = null
let nextId = 1 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 objectList = computed(() => objects.value)
const objectCount = computed(() => objects.value.length) const objectCount = computed(() => objects.value.length)
const selectedObjects = computed(() =>
objects.value.filter((o) => selectedIds.value.has(o.id))
)
return { return {
// State // State
objects, objects,
selectedIds,
// Getters // Getters
objectList, objectList,
objectCount, objectCount,
selectedObjects,
// Actions // Actions
setScene, setScene,
addObject, addObject,
@ -126,5 +142,7 @@ export const useSceneStore = defineStore('scene', () => {
setObjectVisible, setObjectVisible,
setObjectLocked, setObjectLocked,
renameObject, renameObject,
select,
clearSelection,
} }
}) })