Implement object selection
This commit is contained in:
parent
08cdf0d41a
commit
aa7e588980
4 changed files with 89 additions and 4 deletions
6
TODO.md
6
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
|
||||
|
|
|
|||
13
src/App.vue
13
src/App.vue
|
|
@ -1,11 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import AppLayout from './components/AppLayout.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>
|
||||
|
||||
<template>
|
||||
<AppLayout>
|
||||
<Viewport />
|
||||
<Viewport @pick="handlePick" />
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<SceneObject[]>([])
|
||||
const selectedIds = shallowRef<Set<string>>(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,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue