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
|
### 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
|
||||||
|
|
|
||||||
13
src/App.vue
13
src/App.vue
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue