Build custom systems to add behavior and automation to your 3D scene
Systems are the engine that powers dynamic behavior in Pascal Editor. They run every frame to process dirty nodes, update geometries, and synchronize the scene state with Three.js objects.
Create a React component that will house your system logic:
import { useFrame } from '@react-three/fiber'import { sceneRegistry, useScene } from '@pascal-app/core'import type { AnyNodeId } from '@pascal-app/core'export const CustomSystem = () => { const dirtyNodes = useScene((state) => state.dirtyNodes) const clearDirty = useScene((state) => state.clearDirty) useFrame(() => { // System logic runs here every frame }, 5) // Priority level (optional) return null}
System components don’t render anything - they’re purely for side effects. Always return null.
2
Subscribe to dirty nodes
Access the dirty nodes set to know which nodes need processing:
useFrame(() => { if (dirtyNodes.size === 0) return // Early exit if nothing to do const nodes = useScene.getState().nodes dirtyNodes.forEach((id) => { const node = nodes[id] if (!node) return // Filter for your node type if (node.type === 'custom-type') { // Process this node } })})
3
Access Three.js objects via registry
The scene registry maps node IDs to their Three.js representations:
const mesh = sceneRegistry.nodes.get(id) as THREE.Meshif (!mesh) return // Object not ready yet// Now you can manipulate the Three.js objectmesh.position.y = computedHeightmesh.rotation.y = computedRotation
If the mesh doesn’t exist yet, keep the node dirty so it’s processed in the next frame.
4
Implement your logic
Add your custom behavior. Here’s an example that animates items:
const item = node as ItemNodeconst mesh = sceneRegistry.nodes.get(id) as THREE.Object3Dif (!mesh) return// Example: Apply a floating animationconst time = Date.now() * 0.001const floatHeight = Math.sin(time + id.charCodeAt(0)) * 0.1mesh.position.y = item.position[1] + floatHeight// Don't clear dirty - we want to run every frame for animation// clearDirty(id)
5
Clear the dirty flag
When processing is complete, clear the dirty flag:
clearDirty(id as AnyNodeId)
Always clear the dirty flag unless you intentionally want the node to be reprocessed every frame (e.g., for animations).
6
Register your system
Add your system component to the scene:
// In your main Scene componentimport { CustomSystem } from './systems/custom-system'export function Scene() { return ( <Canvas> {/* Other scene components */} <CustomSystem /> </Canvas> )}
const mesh = sceneRegistry.nodes.get(id)if (!mesh) return // Don't clear dirty - try again next frame// Process meshmesh.position.set(...)clearDirty(id) // Only clear when successfully processed
import { sceneRegistry } from '@pascal-app/core'// Get a specific node's Three.js objectconst mesh = sceneRegistry.nodes.get(nodeId) as THREE.Mesh// Get all nodes of a specific typeconst allWallIds = sceneRegistry.byType.wall // Set<string>// Iterate through all wallsfor (const wallId of sceneRegistry.byType.wall) { const wallMesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh // Do something with wall mesh}