Skip to main content

Overview

The ItemSystem is a React component that processes dirty item nodes and calculates their position based on attachment type:
  • Wall-side items: Offset by wall thickness with correct side orientation
  • Floor items: Elevated by slab height using spatial grid
  • Ceiling items: Positioned at ceiling height
  • Surface-placed items: Positioned on other items via hierarchy
The system runs in a useFrame hook at priority 2, ensuring items update after slabs but before walls.

Location

~/workspace/source/packages/core/src/systems/item/item-system.tsx

How It Works

Processing Pipeline

1

Monitor dirty nodes

The system watches the dirtyNodes set for nodes with type === 'item'.
2

Determine attachment type

Check the item’s asset.attachTo property:
  • 'wall-side': Wall-attached items (art, TVs, switches)
  • 'wall': Wall-embedded items (windows, doors)
  • 'ceiling': Ceiling-attached items (lights, fans)
  • undefined: Floor or surface-placed items
3

Calculate position

Based on attachment type:
  • Wall-side: Offset Z by (wallThickness / 2) * side
  • Floor: Query spatial grid for slab elevation
  • Surface: Let React Three Fiber hierarchy handle it
4

Clear dirty flag

Once the mesh is positioned, the node is removed from dirtyNodes.

Props

The ItemSystem component has no props. It reads state from the Zustand store:
dirtyNodes
Set<AnyNodeId>
Set of node IDs that need position updates
nodes
Record<AnyNodeId, AnyNode>
All nodes in the scene graph

Dependencies

ItemNode Schema

The system processes ItemNode objects with these properties:
id
string
required
Unique item identifier
position
[number, number, number]
default:"[0, 0, 0]"
Position in level (or parent) coordinates [x, y, z]
rotation
[number, number, number]
default:"[0, 0, 0]"
Euler rotation in radians [x, y, z]
scale
[number, number, number]
default:"[1, 1, 1]"
Scale multiplier [x, y, z]
side
'front' | 'back'
Which side of the wall the item is on (for attachTo: 'wall-side')
parentId
string
Parent node ID (wall, ceiling, level, or another item)
asset
Asset
required
Asset definition containing dimensions, model URL, and attachment type

Asset Schema

asset.attachTo
'wall' | 'wall-side' | 'ceiling' | undefined
How the item attaches to the scene:
  • 'wall-side': On wall surface (art, TVs)
  • 'wall': Embedded in wall (windows, doors)
  • 'ceiling': Hangs from ceiling (lights, fans)
  • undefined: Sits on floor or surface
asset.dimensions
[number, number, number]
Base dimensions in meters [width, height, depth]
asset.surface
{ height: number }
Optional surface definition for items that support placement on top

External Systems

  • Scene Registry (sceneRegistry.nodes): Maps node IDs to THREE.js meshes
  • Spatial Grid Manager (spatialGridManager): Provides slab elevation for floor items

Positioning Logic

Wall-Side Items

Items attached to wall surfaces are offset perpendicular to the wall:
if (item.asset.attachTo === 'wall-side') {
  const parentWall = nodes[item.parentId] as WallNode
  const wallThickness = parentWall.thickness ?? 0.1
  const side = item.side === 'front' ? 1 : -1
  mesh.position.z = (wallThickness / 2) * side
}
  • Front side: z = +wallThickness/2 (outside wall)
  • Back side: z = -wallThickness/2 (inside wall)

Floor Items

Items placed on the floor query the spatial grid for slab elevation:
const levelId = resolveLevelId(item, nodes)
const slabElevation = spatialGridManager.getSlabElevationForItem(
  levelId,
  item.position,
  getScaledDimensions(item),
  item.rotation,
)
mesh.position.y = slabElevation + item.position[1]
The spatial grid uses the item’s full footprint to determine which slab it sits on.

Surface-Placed Items

Items placed on surfaces (tables, desks) inherit position from their parent:
const parentNode = nodes[item.parentId]
if (parentNode?.type === 'item') {
  // React Three Fiber hierarchy handles positioning
  // No manual position calculation needed
}

Slab Elevation Calculation

The spatial grid manager calculates slab elevation by:
  1. Transforming item footprint to world coordinates
  2. Querying spatial grid cells that overlap the footprint
  3. Finding highest slab that the item sits on
  4. Returning elevation (or 0 if no slab found)
This ensures items:
  • Sit on raised platforms
  • Don’t fall through floors
  • Rest on the correct slab when overlapping multiple floors

Usage Example

import { ItemSystem } from '@pascal/core/systems/item/item-system'
import { useScene } from '@pascal/core/store/use-scene'

function placeChairOnFloor() {
  const { addNode, markDirty } = useScene.getState()
  
  const chair = {
    id: 'item_chair_1',
    type: 'item',
    parentId: 'level_1',
    position: [2, 0, 3],
    rotation: [0, Math.PI / 4, 0],
    scale: [1, 1, 1],
    asset: {
      id: 'chair_modern',
      name: 'Modern Chair',
      src: '/models/chair.glb',
      dimensions: [0.5, 0.8, 0.5],
      // No attachTo = floor placement
    },
  }
  
  addNode(chair)
  markDirty(chair.id)
}

function hangArtOnWall() {
  const { addNode, markDirty } = useScene.getState()
  
  const art = {
    id: 'item_art_1',
    type: 'item',
    parentId: 'wall_1',
    position: [2.5, 1.5, 0], // Along wall, height, (z offset handled by system)
    rotation: [0, 0, 0],
    scale: [1, 1, 1],
    side: 'front',
    asset: {
      id: 'art_frame',
      name: 'Picture Frame',
      src: '/models/frame.glb',
      dimensions: [0.8, 0.6, 0.05],
      attachTo: 'wall-side',
    },
  }
  
  addNode(art)
  markDirty(art.id)
}

function Viewer() {
  return (
    <Canvas>
      {/* Other scene content */}
      <ItemSystem />
    </Canvas>
  )
}

Coordinate Systems

Wall-Attached Items

Wall-local coordinates:
  • X-axis: Along wall length
  • Y-axis: Height (up)
  • Z-axis: Perpendicular to wall (thickness direction)

Floor Items

Level coordinates:
  • X-axis: Horizontal (left-right)
  • Y-axis: Vertical (height)
  • Z-axis: Horizontal (forward-back)

Ceiling Items

Ceiling-local coordinates:
  • X-axis: Horizontal (left-right)
  • Y-axis: Vertical (down from ceiling)
  • Z-axis: Horizontal (forward-back)

Surface Placement

Items with asset.surface can have other items placed on them:
const desk = {
  asset: {
    id: 'desk',
    dimensions: [1.2, 0.75, 0.6],
    surface: {
      height: 0.75, // Top of desk
    },
  },
}

const monitor = {
  parentId: desk.id,
  position: [0, 0.75, 0], // On top of desk
}
The parent-child hierarchy in React Three Fiber automatically handles the transform.

Performance Notes

  • Only updates dirty items (not all items every frame)
  • Runs at priority 2 (after slabs update their geometry)
  • Uses cached spatial grid queries for slab elevation
  • No geometry generation (just position updates)

Integration with Other Systems

Wall System

Walls create cutouts for wall-attached items with a 'cutout' mesh using CSG subtraction.

Spatial Grid

The spatial grid tracks slab boundaries and elevations to correctly position floor items, even on complex multi-level floors.

Scene Registry

All item meshes are registered in sceneRegistry.nodes for quick lookup by ID.

Build docs developers (and LLMs) love