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
The level ID containing this node
SlabNode : Stored for elevation queries
CeilingNode : Stored for ceiling item placement
WallNode : Stored for wall item placement calculations
ItemNode : Inserted into floor, wall, or ceiling grid based on asset.attachTo
handleNodeUpdated
Called when a node is updated. Re-indexes the node in spatial grids.
handleNodeUpdated ( node : AnyNode , levelId : string ): void
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 []
The node type (e.g., ‘wall’, ‘item’, ‘slab’)
The level ID containing this node
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 [] }
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)
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 [] }
The level ID containing the wall
The wall ID to place the item on
X position in wall-local space (distance from wall start in meters)
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)
Required if attachType is 'wall-side'. Specifies which side of the wall.
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
X coordinate in world space
Z coordinate in world space
The highest slab elevation at this point, or 0 if the point is not inside any slab (or is inside a hole)
Iterates through all slabs on the level
Uses pointInPolygon to check if the point is inside the slab polygon
Checks if the point is inside any holes in the slab
Returns the highest elevation found (slabs can stack)
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
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
The highest overlapping slab elevation, or 0 if no overlap
Computes the item’s rotated footprint (4 corners)
Checks if any part of the footprint overlaps with slab polygons
Excludes slabs where the item center is inside a hole
Returns the highest elevation found
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
The highest slab elevation, or 0 if the wall doesn’t overlap any slab
Uses wallOverlapsPolygon to check overlap with slab polygons
Samples multiple points along the wall (0%, 25%, 50%, 75%, 100%) to handle holes
Returns the highest elevation where at least one sample point is on solid slab (not in a hole)
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 [] }
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
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
Checks that the ceiling polygon exists and has at least 3 vertices
Validates all item footprint corners are inside the ceiling polygon
Checks that the item center is not inside any ceiling holes
Checks for overlaps with other ceiling items
clearLevel
Clears all spatial grids for a level.
clearLevel ( levelId : string ): void
clear
Clears all spatial grids.
Utility Functions
pointInPolygon
Point-in-polygon test using ray casting algorithm.
export function pointInPolygon (
px : number ,
pz : number ,
polygon : Array <[ number , number ]>
) : boolean
polygon
Array<[number, number]>
required
Polygon vertices as [x, z] pairs
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
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