2026-01-29 00:24:41 -08:00
|
|
|
import { defineStore } from 'pinia'
|
2026-01-29 00:20:16 -08:00
|
|
|
import { shallowRef, computed } from 'vue'
|
|
|
|
|
import * as THREE from 'three'
|
|
|
|
|
|
|
|
|
|
export interface SceneObject {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
mesh: THREE.Mesh
|
|
|
|
|
visible: boolean
|
|
|
|
|
locked: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 00:24:41 -08:00
|
|
|
export const useSceneStore = defineStore('scene', () => {
|
|
|
|
|
// Use shallowRef to avoid Vue making THREE.Mesh objects deeply reactive
|
|
|
|
|
const objects = shallowRef<SceneObject[]>([])
|
2026-01-29 01:26:33 -08:00
|
|
|
const selectedIds = shallowRef<Set<string>>(new Set())
|
2026-01-29 00:24:41 -08:00
|
|
|
let threeScene: THREE.Scene | null = null
|
|
|
|
|
let nextId = 1
|
2026-01-29 00:20:16 -08:00
|
|
|
|
2026-01-29 00:24:41 -08:00
|
|
|
function generateId(): string {
|
|
|
|
|
return `obj_${nextId++}`
|
2026-01-29 00:20:16 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 00:24:41 -08:00
|
|
|
function generateName(baseName: string): string {
|
|
|
|
|
const existingNames = objects.value.map((o) => o.name)
|
|
|
|
|
if (!existingNames.includes(baseName)) {
|
|
|
|
|
return baseName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let counter = 1
|
|
|
|
|
while (existingNames.includes(`${baseName} ${counter}`)) {
|
|
|
|
|
counter++
|
|
|
|
|
}
|
|
|
|
|
return `${baseName} ${counter}`
|
2026-01-29 00:20:16 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 00:24:41 -08:00
|
|
|
function setScene(scene: THREE.Scene) {
|
|
|
|
|
threeScene = scene
|
2026-01-29 00:20:16 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addObject(mesh: THREE.Mesh, name?: string): SceneObject {
|
2026-01-29 00:24:41 -08:00
|
|
|
if (!threeScene) {
|
2026-01-29 00:20:16 -08:00
|
|
|
throw new Error('Scene not initialized. Call setScene() first.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const baseName = name ?? 'Object'
|
|
|
|
|
const objectName = generateName(baseName)
|
|
|
|
|
const sceneObject: SceneObject = {
|
|
|
|
|
id: generateId(),
|
|
|
|
|
name: objectName,
|
|
|
|
|
mesh,
|
|
|
|
|
visible: true,
|
|
|
|
|
locked: false,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 00:24:41 -08:00
|
|
|
threeScene.add(mesh)
|
2026-01-29 00:20:16 -08:00
|
|
|
objects.value = [...objects.value, sceneObject]
|
|
|
|
|
|
|
|
|
|
return sceneObject
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeObject(id: string): boolean {
|
2026-01-29 01:06:48 -08:00
|
|
|
const obj = objects.value.find((o) => o.id === id)
|
|
|
|
|
if (!obj) return false
|
2026-01-29 00:20:16 -08:00
|
|
|
|
2026-01-29 00:24:41 -08:00
|
|
|
if (threeScene) {
|
|
|
|
|
threeScene.remove(obj.mesh)
|
2026-01-29 00:20:16 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj.mesh.geometry.dispose()
|
|
|
|
|
if (obj.mesh.material instanceof THREE.Material) {
|
|
|
|
|
obj.mesh.material.dispose()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
objects.value = objects.value.filter((o) => o.id !== id)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearScene(): void {
|
|
|
|
|
const ids = objects.value.map((o) => o.id)
|
|
|
|
|
for (const id of ids) {
|
|
|
|
|
removeObject(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getObject(id: string): SceneObject | undefined {
|
|
|
|
|
return objects.value.find((o) => o.id === id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setObjectVisible(id: string, visible: boolean): void {
|
|
|
|
|
objects.value = objects.value.map((obj) => {
|
|
|
|
|
if (obj.id === id) {
|
|
|
|
|
obj.mesh.visible = visible
|
|
|
|
|
return { ...obj, visible }
|
|
|
|
|
}
|
|
|
|
|
return obj
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setObjectLocked(id: string, locked: boolean): void {
|
2026-01-29 16:26:27 -08:00
|
|
|
objects.value = objects.value.map((obj) => (obj.id === id ? { ...obj, locked } : obj))
|
2026-01-29 00:20:16 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renameObject(id: string, name: string): void {
|
2026-01-29 16:26:27 -08:00
|
|
|
objects.value = objects.value.map((obj) => (obj.id === id ? { ...obj, name } : obj))
|
2026-01-29 00:20:16 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 01:26:33 -08:00
|
|
|
function select(id: string): void {
|
|
|
|
|
const obj = objects.value.find((o) => o.id === id)
|
|
|
|
|
if (!obj || obj.locked) return
|
|
|
|
|
selectedIds.value = new Set([id])
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 01:29:25 -08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 01:26:33 -08:00
|
|
|
function clearSelection(): void {
|
|
|
|
|
selectedIds.value = new Set()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 00:20:16 -08:00
|
|
|
const objectList = computed(() => objects.value)
|
|
|
|
|
const objectCount = computed(() => objects.value.length)
|
2026-01-29 16:26:27 -08:00
|
|
|
const selectedObjects = computed(() => objects.value.filter((o) => selectedIds.value.has(o.id)))
|
2026-01-29 00:20:16 -08:00
|
|
|
|
|
|
|
|
return {
|
2026-01-29 00:24:41 -08:00
|
|
|
// State
|
|
|
|
|
objects,
|
2026-01-29 01:26:33 -08:00
|
|
|
selectedIds,
|
2026-01-29 00:24:41 -08:00
|
|
|
// Getters
|
|
|
|
|
objectList,
|
|
|
|
|
objectCount,
|
2026-01-29 01:26:33 -08:00
|
|
|
selectedObjects,
|
2026-01-29 00:24:41 -08:00
|
|
|
// Actions
|
2026-01-29 00:20:16 -08:00
|
|
|
setScene,
|
|
|
|
|
addObject,
|
|
|
|
|
removeObject,
|
|
|
|
|
clearScene,
|
|
|
|
|
getObject,
|
|
|
|
|
setObjectVisible,
|
|
|
|
|
setObjectLocked,
|
|
|
|
|
renameObject,
|
2026-01-29 01:26:33 -08:00
|
|
|
select,
|
2026-01-29 01:29:25 -08:00
|
|
|
addToSelection,
|
|
|
|
|
toggleSelection,
|
|
|
|
|
selectAll,
|
2026-01-29 01:26:33 -08:00
|
|
|
clearSelection,
|
2026-01-29 00:20:16 -08:00
|
|
|
}
|
2026-01-29 00:24:41 -08:00
|
|
|
})
|