useScene
The useScene hook is the main state store for Pascal’s scene graph. It’s built with Zustand and includes middleware for undo/redo (temporal) and localStorage persistence.
Import
import useScene from '@pascal-app/core'
State Structure
SceneState
nodes
Record<AnyNodeId, AnyNode>
Flat dictionary of all nodes in the scene, keyed by node ID.
Array of root-level node IDs (typically contains a single SiteNode ID).
Set of node IDs that have been modified and need revalidation by systems (wall physics, item collision, etc.).
Methods
Scene Management
loadScene
Loads the default scene if none exists. Creates a hierarchy: Site → Building → Level.
Show Implementation Details
Checks if rootNodeIds is already populated
If empty, creates a default hierarchy with one level
Marks all existing nodes as dirty to trigger re-validation
clearScene
Clears all nodes and resets to default scene.
setScene
setScene ( nodes : Record < AnyNodeId , AnyNode > , rootNodeIds : AnyNodeId []): void
Replaces entire scene with new nodes.
nodes
Record<AnyNodeId, AnyNode>
required
Complete node dictionary
Root node IDs for the new scene
Show Backward Compatibility
Automatically patches ItemNode instances saved before the scale property was added by adding a default scale of [1, 1, 1].
Dirty Node Tracking
markDirty
markDirty ( id : AnyNodeId ): void
Marks a node as dirty, indicating it needs revalidation.
clearDirty
clearDirty ( id : AnyNodeId ): void
Removes a node from the dirty set after revalidation.
Node Operations
createNode
createNode ( node : AnyNode , parentId ?: AnyNodeId ): void
Creates a single node and optionally adds it to a parent’s children array.
The node to create (must have a unique id)
Optional parent node ID. If provided, the node will be added to the parent’s children array.
createNodes
createNodes ( ops : { node: AnyNode ; parentId ?: AnyNodeId }[]): void
Batch create multiple nodes in a single operation.
ops
{ node: AnyNode; parentId?: AnyNodeId }[]
required
Array of node creation operations
updateNode
updateNode ( id : AnyNodeId , data : Partial < AnyNode > ): void
Updates a single node with partial data.
Partial node data to merge with existing node
updateNodes
updateNodes ( updates : { id: AnyNodeId ; data : Partial < AnyNode > }[]): void
Batch update multiple nodes in a single operation.
updates
{ id: AnyNodeId; data: Partial<AnyNode> }[]
required
Array of update operations
deleteNode
deleteNode ( id : AnyNodeId ): void
Deletes a single node and its descendants.
deleteNodes
deleteNodes ( ids : AnyNodeId []): void
Batch delete multiple nodes.
Array of node IDs to delete
Middleware
Temporal (Undo/Redo)
Provided by zundo. Access undo/redo state via:
const undo = useScene . temporal . getState (). undo
const redo = useScene . temporal . getState (). redo
const pastStates = useScene . temporal . getState (). pastStates
const futureStates = useScene . temporal . getState (). futureStates
Configuration:
Partialized state : Only nodes and rootNodeIds are tracked in history
Limit : Last 50 actions
Auto re-validation : After undo/redo, all nodes are marked dirty to trigger system revalidation
Persist (LocalStorage)
Provided by zustand/middleware/persist.
Configuration:
Storage key : editor-storage
Version : 1
Partialized state : Only nodes (excluding transient nodes) and rootNodeIds are persisted
Transient nodes : Nodes with metadata.isTransient === true are excluded from persistence
ItemNode scale migration : Adds default scale: [1, 1, 1] to items saved before scale was added
SiteNode wrapper migration : Wraps old scenes (where root is not a SiteNode) in a new SiteNode on hydration
Usage Examples
Basic Usage
import useScene from '@pascal-app/core'
function MyComponent () {
// Select specific state
const nodes = useScene (( state ) => state . nodes )
const createNode = useScene (( state ) => state . createNode )
// Access all state
const scene = useScene ()
return < div > Node count: { Object . keys ( nodes ). length } </ div >
}
Creating Nodes
import { WallNode } from '@pascal-app/core'
import useScene from '@pascal-app/core'
const createNode = useScene (( state ) => state . createNode )
const levelId = 'level-123'
// Create a wall
const wall = WallNode . parse ({
start: [ 0 , 0 ],
end: [ 5 , 0 ],
height: 2.5 ,
thickness: 0.2 ,
})
createNode ( wall , levelId )
Batch Operations
const createNodes = useScene (( state ) => state . createNodes )
const walls = [
{ node: wall1 , parentId: levelId },
{ node: wall2 , parentId: levelId },
{ node: wall3 , parentId: levelId },
]
createNodes ( walls )
Updating Nodes
const updateNode = useScene (( state ) => state . updateNode )
// Move a wall endpoint
updateNode ( wallId , { end: [ 10 , 0 ] })
Undo/Redo
function UndoRedoButtons () {
const undo = () => useScene . temporal . getState (). undo ()
const redo = () => useScene . temporal . getState (). redo ()
const canUndo = useScene . temporal (( state ) => state . pastStates . length > 0 )
const canRedo = useScene . temporal (( state ) => state . futureStates . length > 0 )
return (
<>
< button onClick = { undo } disabled = { ! canUndo } > Undo </ button >
< button onClick = { redo } disabled = { ! canRedo } > Redo </ button >
</>
)
}
Dirty Node System
// Mark node for revalidation after modification
const markDirty = useScene (( state ) => state . markDirty )
const clearDirty = useScene (( state ) => state . clearDirty )
// Update wall and mark dirty
updateNode ( wallId , { end: [ 10 , 0 ] })
markDirty ( wallId )
// In a system component (runs each frame)
function WallSystem () {
const dirtyNodes = useScene (( state ) => state . dirtyNodes )
const clearDirty = useScene (( state ) => state . clearDirty )
useFrame (() => {
dirtyNodes . forEach (( nodeId ) => {
// Validate/update node
processWall ( nodeId )
// Clear dirty flag
clearDirty ( nodeId )
})
})
return null
}
Type Definitions
export type SceneState = {
// Data
nodes : Record < AnyNodeId , AnyNode >
rootNodeIds : AnyNodeId []
dirtyNodes : Set < AnyNodeId >
// Scene management
loadScene : () => void
clearScene : () => void
setScene : ( nodes : Record < AnyNodeId , AnyNode >, rootNodeIds : AnyNodeId []) => void
// Dirty tracking
markDirty : ( id : AnyNodeId ) => void
clearDirty : ( id : AnyNodeId ) => void
// Node operations
createNode : ( node : AnyNode , parentId ?: AnyNodeId ) => void
createNodes : ( ops : { node : AnyNode ; parentId ?: AnyNodeId }[]) => void
updateNode : ( id : AnyNodeId , data : Partial < AnyNode >) => void
updateNodes : ( updates : { id : AnyNodeId ; data : Partial < AnyNode > }[]) => void
deleteNode : ( id : AnyNodeId ) => void
deleteNodes : ( ids : AnyNodeId []) => void
}
type UseSceneStore = UseBoundStore < StoreApi < SceneState >> & {
temporal : StoreApi < TemporalState < Pick < SceneState , 'nodes' | 'rootNodeIds' >>>
}
See Also