Implement selection shortcut keys
This commit is contained in:
parent
aa7e588980
commit
3b42ce9f88
4 changed files with 116 additions and 5 deletions
6
TODO.md
6
TODO.md
|
|
@ -90,9 +90,9 @@ A step-by-step checklist for porting MatterControl's design features to a Vue +
|
|||
- [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
|
||||
- [x] Implement Shift+click for multi-select
|
||||
- [x] Implement Ctrl+click to toggle selection
|
||||
- [x] Add "Select All" (Ctrl+A) shortcut
|
||||
|
||||
### Selection Visualization
|
||||
- [ ] Add selection outline effect (OutlinePass or custom)
|
||||
|
|
|
|||
26
src/App.vue
26
src/App.vue
|
|
@ -1,17 +1,39 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
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) {
|
||||
function handlePick(object: SceneObject | null, event: MouseEvent) {
|
||||
if (object) {
|
||||
sceneStore.select(object.id)
|
||||
if (event.shiftKey) {
|
||||
sceneStore.addToSelection(object.id)
|
||||
} else if (event.ctrlKey || event.metaKey) {
|
||||
sceneStore.toggleSelection(object.id)
|
||||
} else {
|
||||
sceneStore.select(object.id)
|
||||
}
|
||||
} else {
|
||||
sceneStore.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
|
||||
event.preventDefault()
|
||||
sceneStore.selectAll()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -248,5 +248,68 @@ describe('useSceneStore', () => {
|
|||
expect(store.selectedObjects).toHaveLength(1)
|
||||
expect(store.selectedObjects[0]?.name).toBe('A')
|
||||
})
|
||||
|
||||
it('addToSelection() adds to existing selection', () => {
|
||||
const obj1 = store.addObject(createTestMesh(), 'A')
|
||||
const obj2 = store.addObject(createTestMesh(), 'B')
|
||||
|
||||
store.select(obj1.id)
|
||||
store.addToSelection(obj2.id)
|
||||
|
||||
expect(store.selectedObjects).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('addToSelection() ignores locked objects', () => {
|
||||
const obj1 = store.addObject(createTestMesh(), 'A')
|
||||
const obj2 = store.addObject(createTestMesh(), 'B')
|
||||
store.setObjectLocked(obj2.id, true)
|
||||
|
||||
store.select(obj1.id)
|
||||
store.addToSelection(obj2.id)
|
||||
|
||||
expect(store.selectedObjects).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('toggleSelection() adds unselected object', () => {
|
||||
const obj1 = store.addObject(createTestMesh(), 'A')
|
||||
const obj2 = store.addObject(createTestMesh(), 'B')
|
||||
|
||||
store.select(obj1.id)
|
||||
store.toggleSelection(obj2.id)
|
||||
|
||||
expect(store.selectedObjects).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('toggleSelection() removes selected object', () => {
|
||||
const obj1 = store.addObject(createTestMesh(), 'A')
|
||||
const obj2 = store.addObject(createTestMesh(), 'B')
|
||||
|
||||
store.select(obj1.id)
|
||||
store.addToSelection(obj2.id)
|
||||
store.toggleSelection(obj1.id)
|
||||
|
||||
expect(store.selectedObjects).toHaveLength(1)
|
||||
expect(store.selectedObjects[0]?.id).toBe(obj2.id)
|
||||
})
|
||||
|
||||
it('toggleSelection() ignores locked objects', () => {
|
||||
const obj = store.addObject(createTestMesh(), 'A')
|
||||
store.setObjectLocked(obj.id, true)
|
||||
|
||||
store.toggleSelection(obj.id)
|
||||
|
||||
expect(store.selectedObjects).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('selectAll() selects all unlocked objects', () => {
|
||||
store.addObject(createTestMesh(), 'A')
|
||||
store.addObject(createTestMesh(), 'B')
|
||||
const obj3 = store.addObject(createTestMesh(), 'C')
|
||||
store.setObjectLocked(obj3.id, true)
|
||||
|
||||
store.selectAll()
|
||||
|
||||
expect(store.selectedObjects).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -115,6 +115,29 @@ export const useSceneStore = defineStore('scene', () => {
|
|||
selectedIds.value = new Set([id])
|
||||
}
|
||||
|
||||
function addToSelection(id: string): void {
|
||||
const obj = objects.value.find((o) => o.id === id)
|
||||
if (!obj || obj.locked) return
|
||||
selectedIds.value = new Set([...selectedIds.value, id])
|
||||
}
|
||||
|
||||
function toggleSelection(id: string): void {
|
||||
const obj = objects.value.find((o) => o.id === id)
|
||||
if (!obj || obj.locked) return
|
||||
const newSet = new Set(selectedIds.value)
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id)
|
||||
} else {
|
||||
newSet.add(id)
|
||||
}
|
||||
selectedIds.value = newSet
|
||||
}
|
||||
|
||||
function selectAll(): void {
|
||||
const unlocked = objects.value.filter((o) => !o.locked).map((o) => o.id)
|
||||
selectedIds.value = new Set(unlocked)
|
||||
}
|
||||
|
||||
function clearSelection(): void {
|
||||
selectedIds.value = new Set()
|
||||
}
|
||||
|
|
@ -143,6 +166,9 @@ export const useSceneStore = defineStore('scene', () => {
|
|||
setObjectLocked,
|
||||
renameObject,
|
||||
select,
|
||||
addToSelection,
|
||||
toggleSelection,
|
||||
selectAll,
|
||||
clearSelection,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue