Migrate scene state to Pinia store
This commit is contained in:
parent
862f31f6c4
commit
f2dc318f09
4 changed files with 81 additions and 78 deletions
2
TODO.md
2
TODO.md
|
|
@ -54,7 +54,7 @@ A step-by-step checklist for porting MatterControl's design features to a Vue +
|
||||||
- [x] Implement `addObject()` function
|
- [x] Implement `addObject()` function
|
||||||
- [x] Implement `removeObject()` function
|
- [x] Implement `removeObject()` function
|
||||||
- [x] Implement `clearScene()` function
|
- [x] Implement `clearScene()` function
|
||||||
- [ ] Add scene object list to Pinia store
|
- [x] Add scene object list to Pinia store
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
import { setActivePinia, createPinia } from 'pinia'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { useScene, type SceneObject } from './useScene'
|
import { useSceneStore } from './scene'
|
||||||
|
|
||||||
// Mock THREE.Scene
|
// Mock THREE.Scene
|
||||||
function createMockScene() {
|
function createMockScene() {
|
||||||
|
|
@ -17,29 +18,29 @@ function createTestMesh(): THREE.Mesh {
|
||||||
return new THREE.Mesh(geometry, material)
|
return new THREE.Mesh(geometry, material)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('useScene', () => {
|
describe('useSceneStore', () => {
|
||||||
|
let store: ReturnType<typeof useSceneStore>
|
||||||
let scene: THREE.Scene
|
let scene: THREE.Scene
|
||||||
let sceneApi: ReturnType<typeof useScene>
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Get a fresh composable instance
|
setActivePinia(createPinia())
|
||||||
sceneApi = useScene()
|
store = useSceneStore()
|
||||||
scene = createMockScene()
|
scene = createMockScene()
|
||||||
sceneApi.setScene(scene)
|
store.setScene(scene)
|
||||||
sceneApi.clearScene()
|
store.clearScene()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('addObject', () => {
|
describe('addObject', () => {
|
||||||
it('adds a mesh to the scene', () => {
|
it('adds a mesh to the scene', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
sceneApi.addObject(mesh, 'Test Cube')
|
store.addObject(mesh, 'Test Cube')
|
||||||
|
|
||||||
expect(scene.add).toHaveBeenCalledWith(mesh)
|
expect(scene.add).toHaveBeenCalledWith(mesh)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a SceneObject with correct properties', () => {
|
it('returns a SceneObject with correct properties', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
const obj = sceneApi.addObject(mesh, 'Test Cube')
|
const obj = store.addObject(mesh, 'Test Cube')
|
||||||
|
|
||||||
expect(obj.name).toBe('Test Cube')
|
expect(obj.name).toBe('Test Cube')
|
||||||
expect(obj.mesh).toBe(mesh)
|
expect(obj.mesh).toBe(mesh)
|
||||||
|
|
@ -51,15 +52,15 @@ describe('useScene', () => {
|
||||||
it('generates unique IDs', () => {
|
it('generates unique IDs', () => {
|
||||||
const mesh1 = createTestMesh()
|
const mesh1 = createTestMesh()
|
||||||
const mesh2 = createTestMesh()
|
const mesh2 = createTestMesh()
|
||||||
const obj1 = sceneApi.addObject(mesh1, 'Cube 1')
|
const obj1 = store.addObject(mesh1, 'Cube 1')
|
||||||
const obj2 = sceneApi.addObject(mesh2, 'Cube 2')
|
const obj2 = store.addObject(mesh2, 'Cube 2')
|
||||||
|
|
||||||
expect(obj1.id).not.toBe(obj2.id)
|
expect(obj1.id).not.toBe(obj2.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('auto-generates name when not provided', () => {
|
it('auto-generates name when not provided', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
const obj = sceneApi.addObject(mesh)
|
const obj = store.addObject(mesh)
|
||||||
|
|
||||||
expect(obj.name).toBe('Object')
|
expect(obj.name).toBe('Object')
|
||||||
})
|
})
|
||||||
|
|
@ -69,70 +70,67 @@ describe('useScene', () => {
|
||||||
const mesh2 = createTestMesh()
|
const mesh2 = createTestMesh()
|
||||||
const mesh3 = createTestMesh()
|
const mesh3 = createTestMesh()
|
||||||
|
|
||||||
sceneApi.addObject(mesh1, 'Cube')
|
store.addObject(mesh1, 'Cube')
|
||||||
const obj2 = sceneApi.addObject(mesh2, 'Cube')
|
const obj2 = store.addObject(mesh2, 'Cube')
|
||||||
const obj3 = sceneApi.addObject(mesh3, 'Cube')
|
const obj3 = store.addObject(mesh3, 'Cube')
|
||||||
|
|
||||||
expect(obj2.name).toBe('Cube 1')
|
expect(obj2.name).toBe('Cube 1')
|
||||||
expect(obj3.name).toBe('Cube 2')
|
expect(obj3.name).toBe('Cube 2')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws if scene not initialized', () => {
|
it('throws if scene not initialized', () => {
|
||||||
const freshApi = useScene()
|
setActivePinia(createPinia())
|
||||||
// Don't call setScene
|
const freshStore = useSceneStore()
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
|
|
||||||
// Clear the shared scene reference by setting it to a new scene then clearing
|
expect(() => freshStore.addObject(mesh)).toThrow('Scene not initialized')
|
||||||
freshApi.setScene(createMockScene())
|
|
||||||
freshApi.clearScene()
|
|
||||||
// This is tricky because useScene shares state - the test demonstrates the API
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('removeObject', () => {
|
describe('removeObject', () => {
|
||||||
it('removes object from scene', () => {
|
it('removes object from scene', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
const obj = sceneApi.addObject(mesh, 'Test')
|
const obj = store.addObject(mesh, 'Test')
|
||||||
|
|
||||||
const result = sceneApi.removeObject(obj.id)
|
const result = store.removeObject(obj.id)
|
||||||
|
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
expect(scene.remove).toHaveBeenCalledWith(mesh)
|
expect(scene.remove).toHaveBeenCalledWith(mesh)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns false for non-existent id', () => {
|
it('returns false for non-existent id', () => {
|
||||||
const result = sceneApi.removeObject('nonexistent')
|
const result = store.removeObject('nonexistent')
|
||||||
expect(result).toBe(false)
|
expect(result).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes object from objectList', () => {
|
it('removes object from objectList', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
const obj = sceneApi.addObject(mesh, 'Test')
|
const obj = store.addObject(mesh, 'Test')
|
||||||
|
|
||||||
expect(sceneApi.objectCount.value).toBe(1)
|
expect(store.objectCount).toBe(1)
|
||||||
sceneApi.removeObject(obj.id)
|
store.removeObject(obj.id)
|
||||||
expect(sceneApi.objectCount.value).toBe(0)
|
expect(store.objectCount).toBe(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('clearScene', () => {
|
describe('clearScene', () => {
|
||||||
it('removes all objects', () => {
|
it('removes all objects', () => {
|
||||||
sceneApi.addObject(createTestMesh(), 'Cube 1')
|
store.addObject(createTestMesh(), 'Cube 1')
|
||||||
sceneApi.addObject(createTestMesh(), 'Cube 2')
|
store.addObject(createTestMesh(), 'Cube 2')
|
||||||
sceneApi.addObject(createTestMesh(), 'Cube 3')
|
store.addObject(createTestMesh(), 'Cube 3')
|
||||||
|
|
||||||
expect(sceneApi.objectCount.value).toBe(3)
|
expect(store.objectCount).toBe(3)
|
||||||
sceneApi.clearScene()
|
store.clearScene()
|
||||||
expect(sceneApi.objectCount.value).toBe(0)
|
expect(store.objectCount).toBe(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getObject', () => {
|
describe('getObject', () => {
|
||||||
it('returns object by id', () => {
|
it('returns object by id', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
const added = sceneApi.addObject(mesh, 'Test')
|
const added = store.addObject(mesh, 'Test')
|
||||||
|
|
||||||
const found = sceneApi.getObject(added.id)
|
const found = store.getObject(added.id)
|
||||||
|
|
||||||
expect(found).toBeDefined()
|
expect(found).toBeDefined()
|
||||||
expect(found?.id).toBe(added.id)
|
expect(found?.id).toBe(added.id)
|
||||||
|
|
@ -141,7 +139,7 @@ describe('useScene', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns undefined for non-existent id', () => {
|
it('returns undefined for non-existent id', () => {
|
||||||
const found = sceneApi.getObject('nonexistent')
|
const found = store.getObject('nonexistent')
|
||||||
expect(found).toBeUndefined()
|
expect(found).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -149,11 +147,11 @@ describe('useScene', () => {
|
||||||
describe('setObjectVisible', () => {
|
describe('setObjectVisible', () => {
|
||||||
it('updates visibility on SceneObject and mesh', () => {
|
it('updates visibility on SceneObject and mesh', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
const obj = sceneApi.addObject(mesh, 'Test')
|
const obj = store.addObject(mesh, 'Test')
|
||||||
|
|
||||||
sceneApi.setObjectVisible(obj.id, false)
|
store.setObjectVisible(obj.id, false)
|
||||||
|
|
||||||
const updated = sceneApi.getObject(obj.id)
|
const updated = store.getObject(obj.id)
|
||||||
expect(updated?.visible).toBe(false)
|
expect(updated?.visible).toBe(false)
|
||||||
expect(mesh.visible).toBe(false)
|
expect(mesh.visible).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
@ -162,11 +160,11 @@ describe('useScene', () => {
|
||||||
describe('setObjectLocked', () => {
|
describe('setObjectLocked', () => {
|
||||||
it('updates locked state', () => {
|
it('updates locked state', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
const obj = sceneApi.addObject(mesh, 'Test')
|
const obj = store.addObject(mesh, 'Test')
|
||||||
|
|
||||||
sceneApi.setObjectLocked(obj.id, true)
|
store.setObjectLocked(obj.id, true)
|
||||||
|
|
||||||
const updated = sceneApi.getObject(obj.id)
|
const updated = store.getObject(obj.id)
|
||||||
expect(updated?.locked).toBe(true)
|
expect(updated?.locked).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -174,21 +172,21 @@ describe('useScene', () => {
|
||||||
describe('renameObject', () => {
|
describe('renameObject', () => {
|
||||||
it('updates object name', () => {
|
it('updates object name', () => {
|
||||||
const mesh = createTestMesh()
|
const mesh = createTestMesh()
|
||||||
const obj = sceneApi.addObject(mesh, 'Old Name')
|
const obj = store.addObject(mesh, 'Old Name')
|
||||||
|
|
||||||
sceneApi.renameObject(obj.id, 'New Name')
|
store.renameObject(obj.id, 'New Name')
|
||||||
|
|
||||||
const updated = sceneApi.getObject(obj.id)
|
const updated = store.getObject(obj.id)
|
||||||
expect(updated?.name).toBe('New Name')
|
expect(updated?.name).toBe('New Name')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('objectList', () => {
|
describe('objectList', () => {
|
||||||
it('returns all objects as computed', () => {
|
it('returns all objects as computed', () => {
|
||||||
sceneApi.addObject(createTestMesh(), 'A')
|
store.addObject(createTestMesh(), 'A')
|
||||||
sceneApi.addObject(createTestMesh(), 'B')
|
store.addObject(createTestMesh(), 'B')
|
||||||
|
|
||||||
const list = sceneApi.objectList.value
|
const list = store.objectList
|
||||||
|
|
||||||
expect(list).toHaveLength(2)
|
expect(list).toHaveLength(2)
|
||||||
expect(list[0].name).toBe('A')
|
expect(list[0].name).toBe('A')
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
import { shallowRef, computed } from 'vue'
|
import { shallowRef, computed } from 'vue'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
|
@ -9,35 +10,35 @@ export interface SceneObject {
|
||||||
locked: boolean
|
locked: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use shallowRef to avoid Vue making THREE.Mesh objects deeply reactive
|
export const useSceneStore = defineStore('scene', () => {
|
||||||
const objects = shallowRef<SceneObject[]>([])
|
// Use shallowRef to avoid Vue making THREE.Mesh objects deeply reactive
|
||||||
let scene: THREE.Scene | null = null
|
const objects = shallowRef<SceneObject[]>([])
|
||||||
let nextId = 1
|
let threeScene: THREE.Scene | null = null
|
||||||
|
let nextId = 1
|
||||||
|
|
||||||
function generateId(): string {
|
function generateId(): string {
|
||||||
return `obj_${nextId++}`
|
return `obj_${nextId++}`
|
||||||
}
|
|
||||||
|
|
||||||
function generateName(baseName: string): string {
|
|
||||||
const existingNames = objects.value.map((o) => o.name)
|
|
||||||
if (!existingNames.includes(baseName)) {
|
|
||||||
return baseName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let counter = 1
|
function generateName(baseName: string): string {
|
||||||
while (existingNames.includes(`${baseName} ${counter}`)) {
|
const existingNames = objects.value.map((o) => o.name)
|
||||||
counter++
|
if (!existingNames.includes(baseName)) {
|
||||||
}
|
return baseName
|
||||||
return `${baseName} ${counter}`
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function useScene() {
|
let counter = 1
|
||||||
function setScene(threeScene: THREE.Scene) {
|
while (existingNames.includes(`${baseName} ${counter}`)) {
|
||||||
scene = threeScene
|
counter++
|
||||||
|
}
|
||||||
|
return `${baseName} ${counter}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function setScene(scene: THREE.Scene) {
|
||||||
|
threeScene = scene
|
||||||
}
|
}
|
||||||
|
|
||||||
function addObject(mesh: THREE.Mesh, name?: string): SceneObject {
|
function addObject(mesh: THREE.Mesh, name?: string): SceneObject {
|
||||||
if (!scene) {
|
if (!threeScene) {
|
||||||
throw new Error('Scene not initialized. Call setScene() first.')
|
throw new Error('Scene not initialized. Call setScene() first.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +52,7 @@ export function useScene() {
|
||||||
locked: false,
|
locked: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.add(mesh)
|
threeScene.add(mesh)
|
||||||
objects.value = [...objects.value, sceneObject]
|
objects.value = [...objects.value, sceneObject]
|
||||||
|
|
||||||
return sceneObject
|
return sceneObject
|
||||||
|
|
@ -62,8 +63,8 @@ export function useScene() {
|
||||||
if (index === -1) return false
|
if (index === -1) return false
|
||||||
|
|
||||||
const obj = objects.value[index]
|
const obj = objects.value[index]
|
||||||
if (scene) {
|
if (threeScene) {
|
||||||
scene.remove(obj.mesh)
|
threeScene.remove(obj.mesh)
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.mesh.geometry.dispose()
|
obj.mesh.geometry.dispose()
|
||||||
|
|
@ -112,6 +113,12 @@ export function useScene() {
|
||||||
const objectCount = computed(() => objects.value.length)
|
const objectCount = computed(() => objects.value.length)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// State
|
||||||
|
objects,
|
||||||
|
// Getters
|
||||||
|
objectList,
|
||||||
|
objectCount,
|
||||||
|
// Actions
|
||||||
setScene,
|
setScene,
|
||||||
addObject,
|
addObject,
|
||||||
removeObject,
|
removeObject,
|
||||||
|
|
@ -120,7 +127,5 @@ export function useScene() {
|
||||||
setObjectVisible,
|
setObjectVisible,
|
||||||
setObjectLocked,
|
setObjectLocked,
|
||||||
renameObject,
|
renameObject,
|
||||||
objectList,
|
|
||||||
objectCount,
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue