Skip to main content

Spatial Grid Manager

The spatialGridManager provides spatial indexing and collision detection for items placed on floors, walls, and ceilings. It maintains separate grids per level and validates item placement based on geometry overlap.

Import

import { spatialGridManager, pointInPolygon } from '@pascal-app/core'

spatialGridManager

Singleton instance of SpatialGridManager with a default cell size of 0.5 meters.
export const spatialGridManager = new SpatialGridManager()

Methods

handleNodeCreated

Called when a node is created. Registers the node in appropriate spatial grids.
handleNodeCreated(node: AnyNode, levelId: string): void
node
AnyNode
required
The created node
levelId
string
required
The level ID containing this node

handleNodeUpdated

Called when a node is updated. Re-indexes the node in spatial grids.
handleNodeUpdated(node: AnyNode, levelId: string): void
node
AnyNode
required
The updated node
levelId
string
required
The level ID containing this node

handleNodeDeleted

Called when a node is deleted. Removes the node from spatial grids.
handleNodeDeleted(nodeId: string, nodeType: string, levelId: string): string[]
nodeId
string
required
The deleted node ID
nodeType
string
required
The node type (e.g., ‘wall’, ‘item’, ‘slab’)
levelId
string
required
The level ID containing this node
returns
string[]
Array of item IDs that were removed (e.g., when deleting a wall removes all attached items)

canPlaceOnFloor

Checks if an item can be placed on the floor without collisions.
canPlaceOnFloor(
  levelId: string,
  position: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number],
  ignoreIds?: string[]
): { valid: boolean; conflictIds: string[] }
levelId
string
required
The level ID to check placement in
position
[number, number, number]
required
World position [x, y, z]
dimensions
[number, number, number]
required
Item dimensions [width, height, depth]
rotation
[number, number, number]
required
Euler rotation [x, y, z] in radians (typically only Y rotation is used)
ignoreIds
string[]
Optional array of item IDs to ignore during collision check (e.g., the item being moved)
returns
{ valid: boolean; conflictIds: string[] }
  • valid: true if placement is allowed, false if there are collisions
  • conflictIds: Array of item IDs that would overlap

canPlaceOnWall

Checks if an item can be placed on a wall without collisions.
canPlaceOnWall(
  levelId: string,
  wallId: string,
  localX: number,
  localY: number,
  dimensions: [number, number, number],
  attachType: 'wall' | 'wall-side' = 'wall',
  side?: 'front' | 'back',
  ignoreIds?: string[]
): { valid: boolean; conflictIds: string[] }
levelId
string
required
The level ID containing the wall
wallId
string
required
The wall ID to place the item on
localX
number
required
X position in wall-local space (distance from wall start in meters)
localY
number
required
Y position (height from floor in meters)
dimensions
[number, number, number]
required
Item dimensions [width, height, depth]
attachType
'wall' | 'wall-side'
default:"'wall'"
  • 'wall': Item needs both sides of the wall (goes through the wall)
  • 'wall-side': Item only needs one side of the wall (e.g., picture frame)
side
'front' | 'back'
Required if attachType is 'wall-side'. Specifies which side of the wall.
ignoreIds
string[]
Optional array of item IDs to ignore during collision check
returns
{ valid: boolean; conflictIds: string[] }
  • valid: true if placement is allowed
  • conflictIds: Array of conflicting item IDs

getSlabElevationAt

Gets the total slab elevation at a given (x, z) position on a level.
getSlabElevationAt(levelId: string, x: number, z: number): number
levelId
string
required
The level ID
x
number
required
X coordinate in world space
z
number
required
Z coordinate in world space
returns
number
The highest slab elevation at this point, or 0 if the point is not inside any slab (or is inside a hole)

getSlabElevationForItem

Gets the slab elevation for an item using its full footprint (bounding box).
getSlabElevationForItem(
  levelId: string,
  position: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number]
): number
levelId
string
required
The level ID
position
[number, number, number]
required
Item world position [x, y, z]
dimensions
[number, number, number]
required
Item dimensions [width, height, depth]
rotation
[number, number, number]
required
Item rotation [x, y, z] in radians
returns
number
The highest overlapping slab elevation, or 0 if no overlap

getSlabElevationForWall

Gets the slab elevation for a wall by checking if it overlaps with any slab polygon.
getSlabElevationForWall(
  levelId: string,
  start: [number, number],
  end: [number, number]
): number
levelId
string
required
The level ID
start
[number, number]
required
Wall start point [x, z]
end
[number, number]
required
Wall end point [x, z]
returns
number
The highest slab elevation, or 0 if the wall doesn’t overlap any slab

canPlaceOnCeiling

Checks if an item can be placed on a ceiling without collisions.
canPlaceOnCeiling(
  ceilingId: string,
  position: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number],
  ignoreIds?: string[]
): { valid: boolean; conflictIds: string[] }
ceilingId
string
required
The ceiling ID
position
[number, number, number]
required
Item position in ceiling-local space [x, y, z]
dimensions
[number, number, number]
required
Item dimensions [width, height, depth]
rotation
[number, number, number]
required
Item rotation [x, y, z] in radians
ignoreIds
string[]
Optional array of item IDs to ignore during collision check
returns
{ valid: boolean; conflictIds: string[] }
  • valid: true if placement is allowed
  • conflictIds: Array of conflicting item IDs

clearLevel

Clears all spatial grids for a level.
clearLevel(levelId: string): void
levelId
string
required
The level ID to clear

clear

Clears all spatial grids.
clear(): void

Utility Functions

pointInPolygon

Point-in-polygon test using ray casting algorithm.
export function pointInPolygon(
  px: number,
  pz: number,
  polygon: Array<[number, number]>
): boolean
px
number
required
Point X coordinate
pz
number
required
Point Z coordinate
polygon
Array<[number, number]>
required
Polygon vertices as [x, z] pairs
returns
boolean
true if the point is inside the polygon, false otherwise

Usage Examples

Floor Item Placement

import { spatialGridManager } from '@pascal-app/core'

function tryPlaceItem(
  levelId: string,
  position: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number]
) {
  const result = spatialGridManager.canPlaceOnFloor(
    levelId,
    position,
    dimensions,
    rotation
  )
  
  if (result.valid) {
    console.log('Item can be placed!')
    // Create the item node
  } else {
    console.log('Collision detected with:', result.conflictIds)
  }
}

Wall Item Placement

import { spatialGridManager } from '@pascal-app/core'

function placeWallArt(
  levelId: string,
  wallId: string,
  localX: number,
  localY: number
) {
  const dimensions: [number, number, number] = [0.8, 0.6, 0.05] // width, height, depth
  
  const result = spatialGridManager.canPlaceOnWall(
    levelId,
    wallId,
    localX,
    localY,
    dimensions,
    'wall-side', // Art only needs one side
    'front'
  )
  
  if (result.valid) {
    // Create wall-attached item
  }
}

Slab Elevation Query

import { spatialGridManager } from '@pascal-app/core'

function getFloorHeight(levelId: string, x: number, z: number) {
  const elevation = spatialGridManager.getSlabElevationAt(levelId, x, z)
  return elevation // 0 if on ground level, > 0 if on raised slab
}

// For an item with rotation
function getItemFloorHeight(
  levelId: string,
  position: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number]
) {
  return spatialGridManager.getSlabElevationForItem(
    levelId,
    position,
    dimensions,
    rotation
  )
}

Moving an Item

import { spatialGridManager } from '@pascal-app/core'
import useScene from '@pascal-app/core'

function moveItem(
  itemId: string,
  levelId: string,
  newPosition: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number]
) {
  // Check if new position is valid (ignore the item being moved)
  const result = spatialGridManager.canPlaceOnFloor(
    levelId,
    newPosition,
    dimensions,
    rotation,
    [itemId] // Ignore self
  )
  
  if (result.valid) {
    // Update item position
    const updateNode = useScene.getState().updateNode
    updateNode(itemId, { position: newPosition })
  } else {
    console.warn('Cannot move item - would collide with:', result.conflictIds)
  }
}

Syncing with Scene Store

import { spatialGridManager } from '@pascal-app/core'
import useScene from '@pascal-app/core'

// Initialize sync (called once at app startup)
import { initSpatialGridSync } from '@pascal-app/core'
initSpatialGridSync()

// The sync automatically handles:
// - Node creation → handleNodeCreated
// - Node updates → handleNodeUpdated
// - Node deletion → handleNodeDeleted

Performance Characteristics

  • Cell-based spatial indexing: O(1) average case for insertion and queries
  • Separate grids per level: Queries only check relevant level
  • Bounding box culling: Only detailed geometry checks for nearby items
  • Efficient polygon tests: Ray casting algorithm is O(n) where n = polygon vertices

See Also

Build docs developers (and LLMs) love