diff --git a/TODO.md b/TODO.md
index eaee38e68..b26c925a5 100644
--- a/TODO.md
+++ b/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)
diff --git a/src/App.vue b/src/App.vue
index 6d0f0b101..a55fe55e5 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,17 +1,39 @@
diff --git a/src/stores/scene.spec.ts b/src/stores/scene.spec.ts
index eb6f61131..7fa844f44 100644
--- a/src/stores/scene.spec.ts
+++ b/src/stores/scene.spec.ts
@@ -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)
+ })
})
})
diff --git a/src/stores/scene.ts b/src/stores/scene.ts
index 45b7b0f9b..b2a699d25 100644
--- a/src/stores/scene.ts
+++ b/src/stores/scene.ts
@@ -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,
}
})