Skip to main content

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.
rootNodeIds
AnyNodeId[]
Array of root-level node IDs (typically contains a single SiteNode ID).
dirtyNodes
Set<AnyNodeId>
Set of node IDs that have been modified and need revalidation by systems (wall physics, item collision, etc.).

Methods

Scene Management

loadScene

loadScene(): void
Loads the default scene if none exists. Creates a hierarchy: Site → Building → Level.

clearScene

clearScene(): void
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
rootNodeIds
AnyNodeId[]
required
Root node IDs for the new scene

Dirty Node Tracking

markDirty

markDirty(id: AnyNodeId): void
Marks a node as dirty, indicating it needs revalidation.
id
AnyNodeId
required
Node ID to mark dirty

clearDirty

clearDirty(id: AnyNodeId): void
Removes a node from the dirty set after revalidation.
id
AnyNodeId
required
Node ID to clear

Node Operations

createNode

createNode(node: AnyNode, parentId?: AnyNodeId): void
Creates a single node and optionally adds it to a parent’s children array.
node
AnyNode
required
The node to create (must have a unique id)
parentId
AnyNodeId
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.
id
AnyNodeId
required
ID of the node to update
data
Partial<AnyNode>
required
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.
id
AnyNodeId
required
ID of the node to delete

deleteNodes

deleteNodes(ids: AnyNodeId[]): void
Batch delete multiple nodes.
ids
AnyNodeId[]
required
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

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

Build docs developers (and LLMs) love