draft item-system + elevation
This commit is contained in:
@@ -1,11 +1,30 @@
|
|||||||
import type { AnyNode, ItemNode, WallNode } from '../../schema'
|
import type { AnyNode, ItemNode, SlabNode, WallNode } from '../../schema'
|
||||||
import { SpatialGrid } from './spatial-grid'
|
import { SpatialGrid } from './spatial-grid'
|
||||||
import { WallSpatialGrid } from './wall-spatial-grid'
|
import { WallSpatialGrid } from './wall-spatial-grid'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point-in-polygon test using ray casting algorithm.
|
||||||
|
* Returns true if point (px, pz) is inside the polygon defined by vertices.
|
||||||
|
*/
|
||||||
|
function pointInPolygon(px: number, pz: number, polygon: Array<[number, number]>): boolean {
|
||||||
|
let inside = false
|
||||||
|
const n = polygon.length
|
||||||
|
for (let i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
const xi = polygon[i]![0], zi = polygon[i]![1]
|
||||||
|
const xj = polygon[j]![0], zj = polygon[j]![1]
|
||||||
|
|
||||||
|
if ((zi > pz) !== (zj > pz) && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
|
||||||
|
inside = !inside
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside
|
||||||
|
}
|
||||||
|
|
||||||
export class SpatialGridManager {
|
export class SpatialGridManager {
|
||||||
private floorGrids = new Map<string, SpatialGrid>() // levelId -> grid
|
private floorGrids = new Map<string, SpatialGrid>() // levelId -> grid
|
||||||
private wallGrids = new Map<string, WallSpatialGrid>() // levelId -> wall grid
|
private wallGrids = new Map<string, WallSpatialGrid>() // levelId -> wall grid
|
||||||
private walls = new Map<string, WallNode>() // wallId -> wall data (for length calculations)
|
private walls = new Map<string, WallNode>() // wallId -> wall data (for length calculations)
|
||||||
|
private slabsByLevel = new Map<string, Map<string, SlabNode>>() // levelId -> (slabId -> slab)
|
||||||
|
|
||||||
constructor(private cellSize = 0.5) {}
|
constructor(private cellSize = 0.5) {}
|
||||||
|
|
||||||
@@ -36,9 +55,18 @@ export class SpatialGridManager {
|
|||||||
return wall?.height ?? 2.5 // Default wall height
|
return wall?.height ?? 2.5 // Default wall height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSlabMap(levelId: string): Map<string, SlabNode> {
|
||||||
|
if (!this.slabsByLevel.has(levelId)) {
|
||||||
|
this.slabsByLevel.set(levelId, new Map())
|
||||||
|
}
|
||||||
|
return this.slabsByLevel.get(levelId)!
|
||||||
|
}
|
||||||
|
|
||||||
// Called when nodes change
|
// Called when nodes change
|
||||||
handleNodeCreated(node: AnyNode, levelId: string) {
|
handleNodeCreated(node: AnyNode, levelId: string) {
|
||||||
if (node.type === 'wall') {
|
if (node.type === 'slab') {
|
||||||
|
this.getSlabMap(levelId).set(node.id, node as SlabNode)
|
||||||
|
} else if (node.type === 'wall') {
|
||||||
const wall = node as WallNode
|
const wall = node as WallNode
|
||||||
this.walls.set(wall.id, wall)
|
this.walls.set(wall.id, wall)
|
||||||
} else if (node.type === 'item') {
|
} else if (node.type === 'item') {
|
||||||
@@ -79,7 +107,9 @@ export class SpatialGridManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNodeUpdated(node: AnyNode, levelId: string) {
|
handleNodeUpdated(node: AnyNode, levelId: string) {
|
||||||
if (node.type === 'wall') {
|
if (node.type === 'slab') {
|
||||||
|
this.getSlabMap(levelId).set(node.id, node as SlabNode)
|
||||||
|
} else if (node.type === 'wall') {
|
||||||
const wall = node as WallNode
|
const wall = node as WallNode
|
||||||
this.walls.set(wall.id, wall)
|
this.walls.set(wall.id, wall)
|
||||||
} else if (node.type === 'item') {
|
} else if (node.type === 'item') {
|
||||||
@@ -120,7 +150,9 @@ export class SpatialGridManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNodeDeleted(nodeId: string, nodeType: string, levelId: string) {
|
handleNodeDeleted(nodeId: string, nodeType: string, levelId: string) {
|
||||||
if (nodeType === 'wall') {
|
if (nodeType === 'slab') {
|
||||||
|
this.getSlabMap(levelId).delete(nodeId)
|
||||||
|
} else if (nodeType === 'wall') {
|
||||||
this.walls.delete(nodeId)
|
this.walls.delete(nodeId)
|
||||||
// Remove all items attached to this wall from the spatial grid
|
// Remove all items attached to this wall from the spatial grid
|
||||||
const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId)
|
const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId)
|
||||||
@@ -191,15 +223,37 @@ export class SpatialGridManager {
|
|||||||
return this.getWallGrid(levelId).getWallForItem(itemId)
|
return this.getWallGrid(levelId).getWallForItem(itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total slab elevation at a given (x, z) position on a level.
|
||||||
|
* Returns the highest slab elevation if the point is inside any slab polygon, otherwise 0.
|
||||||
|
*/
|
||||||
|
getSlabElevationAt(levelId: string, x: number, z: number): number {
|
||||||
|
const slabMap = this.slabsByLevel.get(levelId)
|
||||||
|
if (!slabMap) return 0
|
||||||
|
|
||||||
|
let maxElevation = 0
|
||||||
|
for (const slab of slabMap.values()) {
|
||||||
|
if (slab.polygon.length >= 3 && pointInPolygon(x, z, slab.polygon)) {
|
||||||
|
const elevation = slab.elevation ?? 0.05
|
||||||
|
if (elevation > maxElevation) {
|
||||||
|
maxElevation = elevation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxElevation
|
||||||
|
}
|
||||||
|
|
||||||
clearLevel(levelId: string) {
|
clearLevel(levelId: string) {
|
||||||
this.floorGrids.delete(levelId)
|
this.floorGrids.delete(levelId)
|
||||||
this.wallGrids.delete(levelId)
|
this.wallGrids.delete(levelId)
|
||||||
|
this.slabsByLevel.delete(levelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.floorGrids.clear()
|
this.floorGrids.clear()
|
||||||
this.wallGrids.clear()
|
this.wallGrids.clear()
|
||||||
this.walls.clear()
|
this.walls.clear()
|
||||||
|
this.slabsByLevel.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,10 +53,12 @@ export function initSpatialGridSync() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect updated nodes (items with position/rotation/parentId/side changes)
|
// Detect updated nodes (items with position/rotation/parentId/side changes, slabs with polygon/elevation changes)
|
||||||
for (const [id, node] of Object.entries(state.nodes)) {
|
for (const [id, node] of Object.entries(state.nodes)) {
|
||||||
const prev = prevState.nodes[id as AnyNode['id']]
|
const prev = prevState.nodes[id as AnyNode['id']]
|
||||||
if (prev && node.type === 'item' && prev.type === 'item') {
|
if (!prev) continue
|
||||||
|
|
||||||
|
if (node.type === 'item' && prev.type === 'item') {
|
||||||
if (
|
if (
|
||||||
!arraysEqual(node.position, prev.position) ||
|
!arraysEqual(node.position, prev.position) ||
|
||||||
!arraysEqual(node.rotation, prev.rotation) ||
|
!arraysEqual(node.rotation, prev.rotation) ||
|
||||||
@@ -66,6 +68,11 @@ export function initSpatialGridSync() {
|
|||||||
const levelId = resolveLevelId(node, state.nodes)
|
const levelId = resolveLevelId(node, state.nodes)
|
||||||
spatialGridManager.handleNodeUpdated(node, levelId)
|
spatialGridManager.handleNodeUpdated(node, levelId)
|
||||||
}
|
}
|
||||||
|
} else if (node.type === 'slab' && prev.type === 'slab') {
|
||||||
|
if (node.polygon !== prev.polygon || node.elevation !== prev.elevation) {
|
||||||
|
const levelId = resolveLevelId(node, state.nodes)
|
||||||
|
spatialGridManager.handleNodeUpdated(node, levelId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export * from './schema'
|
|||||||
export { default as useScene } from './store/use-scene'
|
export { default as useScene } from './store/use-scene'
|
||||||
// Systems
|
// Systems
|
||||||
export { CeilingSystem } from './systems/ceiling/ceiling-system'
|
export { CeilingSystem } from './systems/ceiling/ceiling-system'
|
||||||
|
export { ItemSystem } from './systems/item/item-system'
|
||||||
export { SlabSystem } from './systems/slab/slab-system'
|
export { SlabSystem } from './systems/slab/slab-system'
|
||||||
export { WallSystem } from './systems/wall/wall-system'
|
export { WallSystem } from './systems/wall/wall-system'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useFrame } from '@react-three/fiber'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
|
||||||
|
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager'
|
||||||
|
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync'
|
||||||
|
import type { AnyNodeId, ItemNode, WallNode } from '../../schema'
|
||||||
|
import useScene from '../../store/use-scene'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ITEM SYSTEM
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const ItemSystem = () => {
|
||||||
|
const { nodes, dirtyNodes, clearDirty } = useScene()
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (dirtyNodes.size === 0) return
|
||||||
|
|
||||||
|
dirtyNodes.forEach((id) => {
|
||||||
|
const node = nodes[id]
|
||||||
|
if (!node || node.type !== 'item') return
|
||||||
|
|
||||||
|
const item = node as ItemNode
|
||||||
|
const mesh = sceneRegistry.nodes.get(id) as THREE.Object3D
|
||||||
|
if (!mesh) return
|
||||||
|
|
||||||
|
if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
|
||||||
|
// Wall-attached item: offset Z by half the parent wall's thickness
|
||||||
|
const parentWall = item.parentId ? nodes[item.parentId as AnyNodeId] : undefined
|
||||||
|
if (parentWall && parentWall.type === 'wall') {
|
||||||
|
const wallThickness = (parentWall as WallNode).thickness ?? 0.1
|
||||||
|
mesh.position.z = wallThickness / 2
|
||||||
|
}
|
||||||
|
} else if (!item.asset.attachTo) {
|
||||||
|
// Floor item: elevate by slab height
|
||||||
|
const levelId = resolveLevelId(item, nodes)
|
||||||
|
const slabElevation = spatialGridManager.getSlabElevationAt(levelId, item.position[0], item.position[2])
|
||||||
|
mesh.position.y = slabElevation
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDirty(id as AnyNodeId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { CeilingSystem, SlabSystem, WallSystem } from '@pascal-app/core'
|
import { CeilingSystem, ItemSystem, SlabSystem, WallSystem } from '@pascal-app/core'
|
||||||
import { Bvh, Environment } from '@react-three/drei'
|
import { Bvh, Environment } from '@react-three/drei'
|
||||||
import { Canvas, extend, type ThreeToJSXElements } from '@react-three/fiber'
|
import { Canvas, extend, type ThreeToJSXElements } from '@react-three/fiber'
|
||||||
import * as THREE from 'three/webgpu'
|
import * as THREE from 'three/webgpu'
|
||||||
@@ -41,6 +41,7 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
|||||||
{/* Default Systems */}
|
{/* Default Systems */}
|
||||||
<LevelSystem />
|
<LevelSystem />
|
||||||
<CeilingSystem />
|
<CeilingSystem />
|
||||||
|
<ItemSystem />
|
||||||
<SlabSystem />
|
<SlabSystem />
|
||||||
<WallSystem />
|
<WallSystem />
|
||||||
<PostProcessing />
|
<PostProcessing />
|
||||||
|
|||||||
Reference in New Issue
Block a user