feat(paint-slots): persist scene materials + collections via autosave
The editor autosave/load is the single source of truth for what travels with a scene. Include document-level state (collections, materials) so it survives reload in every embedder (community cloud save + standalone localStorage): - SceneGraph carries optional collections + materials. - useAutoSave triggers on collections/materials reference changes (not just nodes) and writes them into the saved graph + the unload flush. - applySceneGraphToEditor restores them via setScene's extras arg on load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b3d840a39b
commit
2ac9f6762d
@@ -61,6 +61,12 @@ export function useAutoSave({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
|
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
|
||||||
let lastNodeCount = Object.keys(useScene.getState().nodes).length
|
let lastNodeCount = Object.keys(useScene.getState().nodes).length
|
||||||
|
// Collections + scene materials are document-level state that persists with
|
||||||
|
// the graph but lives outside `nodes`. Track them by reference (zustand
|
||||||
|
// hands out a new object on every mutation) so a material edit or a
|
||||||
|
// collection change still triggers a save.
|
||||||
|
let lastCollectionsRef = useScene.getState().collections
|
||||||
|
let lastMaterialsRef = useScene.getState().materials
|
||||||
|
|
||||||
async function executeSave() {
|
async function executeSave() {
|
||||||
if (isLoadingSceneRef.current || isVersionPreviewModeRef.current) {
|
if (isLoadingSceneRef.current || isVersionPreviewModeRef.current) {
|
||||||
@@ -69,8 +75,8 @@ export function useAutoSave({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nodes, rootNodeIds } = useScene.getState()
|
const { nodes, rootNodeIds, collections, materials } = useScene.getState()
|
||||||
const sceneGraph = { nodes, rootNodeIds } as SceneGraph
|
const sceneGraph = { nodes, rootNodeIds, collections, materials } as SceneGraph
|
||||||
|
|
||||||
// Guard: refuse to autosave if the scene went from populated to nearly empty.
|
// Guard: refuse to autosave if the scene went from populated to nearly empty.
|
||||||
// This catches accidental full deletions before they're persisted.
|
// This catches accidental full deletions before they're persisted.
|
||||||
@@ -118,19 +124,29 @@ export function useAutoSave({
|
|||||||
const unsubscribe = useScene.subscribe((state) => {
|
const unsubscribe = useScene.subscribe((state) => {
|
||||||
if (isLoadingSceneRef.current) {
|
if (isLoadingSceneRef.current) {
|
||||||
lastNodesSnapshot = JSON.stringify(state.nodes)
|
lastNodesSnapshot = JSON.stringify(state.nodes)
|
||||||
|
lastCollectionsRef = state.collections
|
||||||
|
lastMaterialsRef = state.materials
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVersionPreviewModeRef.current) {
|
if (isVersionPreviewModeRef.current) {
|
||||||
setSaveStatus('paused')
|
setSaveStatus('paused')
|
||||||
lastNodesSnapshot = JSON.stringify(state.nodes)
|
lastNodesSnapshot = JSON.stringify(state.nodes)
|
||||||
|
lastCollectionsRef = state.collections
|
||||||
|
lastMaterialsRef = state.materials
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentNodesSnapshot = JSON.stringify(state.nodes)
|
const currentNodesSnapshot = JSON.stringify(state.nodes)
|
||||||
if (currentNodesSnapshot === lastNodesSnapshot) return
|
const changed =
|
||||||
|
currentNodesSnapshot !== lastNodesSnapshot ||
|
||||||
|
state.collections !== lastCollectionsRef ||
|
||||||
|
state.materials !== lastMaterialsRef
|
||||||
|
if (!changed) return
|
||||||
|
|
||||||
lastNodesSnapshot = currentNodesSnapshot
|
lastNodesSnapshot = currentNodesSnapshot
|
||||||
|
lastCollectionsRef = state.collections
|
||||||
|
lastMaterialsRef = state.materials
|
||||||
hasDirtyChangesRef.current = true
|
hasDirtyChangesRef.current = true
|
||||||
onDirtyRef.current?.()
|
onDirtyRef.current?.()
|
||||||
setSaveStatus('pending')
|
setSaveStatus('pending')
|
||||||
@@ -156,8 +172,8 @@ export function useAutoSave({
|
|||||||
function flushOnExit() {
|
function flushOnExit() {
|
||||||
if (!hasDirtyChangesRef.current) return
|
if (!hasDirtyChangesRef.current) return
|
||||||
hasDirtyChangesRef.current = false
|
hasDirtyChangesRef.current = false
|
||||||
const { nodes, rootNodeIds } = useScene.getState()
|
const { nodes, rootNodeIds, collections, materials } = useScene.getState()
|
||||||
const sceneGraph = { nodes, rootNodeIds } as SceneGraph
|
const sceneGraph = { nodes, rootNodeIds, collections, materials } as SceneGraph
|
||||||
if (onSaveRef.current) {
|
if (onSaveRef.current) {
|
||||||
onSaveRef.current(sceneGraph, { keepalive: true }).catch(() => {})
|
onSaveRef.current(sceneGraph, { keepalive: true }).catch(() => {})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import useEditor, {
|
|||||||
export type SceneGraph = {
|
export type SceneGraph = {
|
||||||
nodes: Record<string, unknown>
|
nodes: Record<string, unknown>
|
||||||
rootNodeIds: string[]
|
rootNodeIds: string[]
|
||||||
|
// Document-level scene state that travels with the graph. Optional so older
|
||||||
|
// payloads (and callers that only build nodes) stay valid.
|
||||||
|
collections?: Record<string, unknown>
|
||||||
|
materials?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersistedSelectionPath = {
|
type PersistedSelectionPath = {
|
||||||
@@ -374,8 +378,11 @@ function hasUsableSceneGraph(sceneGraph?: SceneGraph | null): sceneGraph is Scen
|
|||||||
|
|
||||||
export function applySceneGraphToEditor(sceneGraph?: SceneGraph | null) {
|
export function applySceneGraphToEditor(sceneGraph?: SceneGraph | null) {
|
||||||
if (hasUsableSceneGraph(sceneGraph)) {
|
if (hasUsableSceneGraph(sceneGraph)) {
|
||||||
const { nodes, rootNodeIds } = sceneGraph
|
const { nodes, rootNodeIds, collections, materials } = sceneGraph
|
||||||
useScene.getState().setScene(nodes as any, rootNodeIds as any)
|
useScene.getState().setScene(nodes as any, rootNodeIds as any, {
|
||||||
|
collections: collections as any,
|
||||||
|
materials: materials as any,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
useScene.getState().clearScene()
|
useScene.getState().clearScene()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user