draft item-system + elevation

This commit is contained in:
wass08
2026-01-27 19:05:08 +09:00
parent 150a0a4ca1
commit 11aba2af36
5 changed files with 116 additions and 7 deletions
@@ -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 { 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 {
private floorGrids = new Map<string, SpatialGrid>() // levelId -> grid
private wallGrids = new Map<string, WallSpatialGrid>() // levelId -> wall grid
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) {}
@@ -36,9 +55,18 @@ export class SpatialGridManager {
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
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
this.walls.set(wall.id, wall)
} else if (node.type === 'item') {
@@ -79,7 +107,9 @@ export class SpatialGridManager {
}
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
this.walls.set(wall.id, wall)
} else if (node.type === 'item') {
@@ -120,7 +150,9 @@ export class SpatialGridManager {
}
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)
// Remove all items attached to this wall from the spatial grid
const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId)
@@ -191,15 +223,37 @@ export class SpatialGridManager {
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) {
this.floorGrids.delete(levelId)
this.wallGrids.delete(levelId)
this.slabsByLevel.delete(levelId)
}
clear() {
this.floorGrids.clear()
this.wallGrids.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)) {
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 (
!arraysEqual(node.position, prev.position) ||
!arraysEqual(node.rotation, prev.rotation) ||
@@ -66,6 +68,11 @@ export function initSpatialGridSync() {
const levelId = resolveLevelId(node, state.nodes)
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)
}
}
}
})
+1
View File
@@ -29,6 +29,7 @@ export * from './schema'
export { default as useScene } from './store/use-scene'
// Systems
export { CeilingSystem } from './systems/ceiling/ceiling-system'
export { ItemSystem } from './systems/item/item-system'
export { SlabSystem } from './systems/slab/slab-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'
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 { Canvas, extend, type ThreeToJSXElements } from '@react-three/fiber'
import * as THREE from 'three/webgpu'
@@ -41,6 +41,7 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
{/* Default Systems */}
<LevelSystem />
<CeilingSystem />
<ItemSystem />
<SlabSystem />
<WallSystem />
<PostProcessing />