- Rename all DB tables: properties → projects, properties_addresses → projects_addresses, properties_models → projects_models, property_likes → projects_likes - Add projects_likes as proper Drizzle schema table (was SQL-only before) - Rename ID prefixes: property_xxx → project_xxx - Rename auth_sessions column: active_property_id → active_project_id - Simplify project creation: address is now optional (no longer required to get started) - Update all actions, stores, types, components, and routes - Rename route: /editor/[propertyId] → /editor/[projectId] - Add SQL migration for table/column/index/policy/function renames - Update RPC functions: increment_project_views, get_project_like_count - Update storage bucket reference: project-thumbnails - All types/exports follow existing Drizzle patterns (pgTable, relations, zod schemas)
143 lines
3.8 KiB
TypeScript
143 lines
3.8 KiB
TypeScript
/**
|
|
* Hooks for project model (scene) loading and auto-saving
|
|
*/
|
|
|
|
'use client'
|
|
|
|
import { useScene } from '@pascal-app/core'
|
|
import { useViewer } from '@pascal-app/viewer'
|
|
import { useEffect, useRef } from 'react'
|
|
import useEditor from '@/store/use-editor'
|
|
import { useProjectStore } from '../projects/store'
|
|
import { getProjectModel, saveProjectModel } from './actions'
|
|
|
|
/**
|
|
* Load the scene when a project becomes active
|
|
* Saves changes automatically with debouncing
|
|
*/
|
|
export function useProjectScene() {
|
|
// Subscribe to project store
|
|
const activeProject = useProjectStore((state) => state.activeProject)
|
|
const isLoadingProject = useProjectStore((state) => state.isLoading)
|
|
|
|
const lastProjectIdRef = useRef<string | null>(null)
|
|
const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
|
const isSavingRef = useRef(false)
|
|
const currentProjectIdRef = useRef<string | null>(null)
|
|
|
|
// Extract project ID for dependency tracking
|
|
const projectId = activeProject?.id ?? null
|
|
const projectName = activeProject?.name ?? null
|
|
|
|
// Load scene when active project changes
|
|
useEffect(() => {
|
|
if (isLoadingProject) {
|
|
return
|
|
}
|
|
|
|
if (!projectId) {
|
|
return
|
|
}
|
|
|
|
// Skip if same project
|
|
if (lastProjectIdRef.current === projectId) {
|
|
return
|
|
}
|
|
|
|
lastProjectIdRef.current = projectId
|
|
|
|
// Load the project's scene
|
|
async function loadScene() {
|
|
try {
|
|
const result = await getProjectModel(projectId || '')
|
|
|
|
if (result.success && result.data?.scene_graph) {
|
|
// Load the scene graph into the store
|
|
const { nodes, rootNodeIds } = result.data.scene_graph
|
|
useScene.getState().setScene(nodes, rootNodeIds)
|
|
} else {
|
|
// No scene found - clear the scene
|
|
useScene.getState().clearScene()
|
|
}
|
|
} catch (error) {
|
|
// Fall back to clear scene
|
|
useScene.getState().clearScene()
|
|
}
|
|
|
|
// Reset editor state after loading/clearing scene
|
|
useEditor.getState().setPhase('site')
|
|
useViewer.getState().setSelection({
|
|
buildingId: null,
|
|
levelId: null,
|
|
selectedIds: [],
|
|
zoneId: null,
|
|
})
|
|
}
|
|
|
|
loadScene()
|
|
}, [projectId, isLoadingProject])
|
|
|
|
// Auto-save scene changes with debouncing
|
|
useEffect(() => {
|
|
if (!projectId) {
|
|
currentProjectIdRef.current = null
|
|
return
|
|
}
|
|
|
|
currentProjectIdRef.current = projectId
|
|
|
|
// Subscribe to any scene changes
|
|
// Use JSON stringification to detect any node changes, not just count
|
|
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
|
|
|
|
const unsubscribe = useScene.subscribe((state) => {
|
|
const currentNodesSnapshot = JSON.stringify(state.nodes)
|
|
|
|
// Only trigger save if nodes actually changed
|
|
if (currentNodesSnapshot === lastNodesSnapshot) {
|
|
return
|
|
}
|
|
|
|
lastNodesSnapshot = currentNodesSnapshot
|
|
const nodes = state.nodes
|
|
|
|
// Skip if currently saving
|
|
if (isSavingRef.current) {
|
|
return
|
|
}
|
|
|
|
// Clear existing timeout
|
|
if (saveTimeoutRef.current) {
|
|
clearTimeout(saveTimeoutRef.current)
|
|
}
|
|
|
|
// Debounce save by 2 seconds
|
|
saveTimeoutRef.current = setTimeout(async () => {
|
|
// Get the current project ID at save time (not the captured value)
|
|
const currentProjectId = currentProjectIdRef.current
|
|
if (!currentProjectId) {
|
|
return
|
|
}
|
|
|
|
const rootNodeIds = useScene.getState().rootNodeIds
|
|
const sceneGraph = { nodes, rootNodeIds }
|
|
|
|
isSavingRef.current = true
|
|
|
|
try {
|
|
await saveProjectModel(currentProjectId, sceneGraph)
|
|
} finally {
|
|
isSavingRef.current = false
|
|
}
|
|
}, 2000)
|
|
})
|
|
|
|
return () => {
|
|
if (saveTimeoutRef.current) {
|
|
clearTimeout(saveTimeoutRef.current)
|
|
}
|
|
unsubscribe()
|
|
}
|
|
}, [projectId])
|
|
}
|