biome
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import { useLayoutEffect } from "react";
|
||||
import { useLayoutEffect } from 'react'
|
||||
import type * as THREE from 'three'
|
||||
|
||||
export const sceneRegistry = {
|
||||
// Master lookup: ID -> Object3D
|
||||
@@ -15,27 +14,27 @@ export const sceneRegistry = {
|
||||
item: new Set<string>(),
|
||||
slab: new Set<string>(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useRegistry(
|
||||
id: string,
|
||||
type: keyof typeof sceneRegistry.byType,
|
||||
ref: React.RefObject<THREE.Object3D>
|
||||
ref: React.RefObject<THREE.Object3D>,
|
||||
) {
|
||||
useLayoutEffect(() => {
|
||||
const obj = ref.current;
|
||||
if (!obj) return;
|
||||
const obj = ref.current
|
||||
if (!obj) return
|
||||
|
||||
// 1. Add to master map
|
||||
sceneRegistry.nodes.set(id, obj);
|
||||
sceneRegistry.nodes.set(id, obj)
|
||||
|
||||
// 2. Add to type-specific set
|
||||
sceneRegistry.byType[type].add(id);
|
||||
sceneRegistry.byType[type].add(id)
|
||||
|
||||
// 4. Cleanup when component unmounts
|
||||
return () => {
|
||||
sceneRegistry.nodes.delete(id);
|
||||
sceneRegistry.byType[type].delete(id);
|
||||
};
|
||||
}, [id, type, ref]);
|
||||
sceneRegistry.nodes.delete(id)
|
||||
sceneRegistry.byType[type].delete(id)
|
||||
}
|
||||
}, [id, type, ref])
|
||||
}
|
||||
|
||||
@@ -1,64 +1,58 @@
|
||||
import { AnyNode, ItemNode, WallNode } from "../../schema";
|
||||
import { SpatialGrid } from "./spatial-grid";
|
||||
import { WallSpatialGrid } from "./wall-spatial-grid";
|
||||
import type { AnyNode, ItemNode, WallNode } from '../../schema'
|
||||
import { SpatialGrid } from './spatial-grid'
|
||||
import { WallSpatialGrid } from './wall-spatial-grid'
|
||||
|
||||
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 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)
|
||||
|
||||
constructor(private cellSize = 0.5) {}
|
||||
|
||||
private getFloorGrid(levelId: string): SpatialGrid {
|
||||
if (!this.floorGrids.has(levelId)) {
|
||||
this.floorGrids.set(
|
||||
levelId,
|
||||
new SpatialGrid({ cellSize: this.cellSize }),
|
||||
);
|
||||
this.floorGrids.set(levelId, new SpatialGrid({ cellSize: this.cellSize }))
|
||||
}
|
||||
return this.floorGrids.get(levelId)!;
|
||||
return this.floorGrids.get(levelId)!
|
||||
}
|
||||
|
||||
private getWallGrid(levelId: string): WallSpatialGrid {
|
||||
if (!this.wallGrids.has(levelId)) {
|
||||
this.wallGrids.set(levelId, new WallSpatialGrid());
|
||||
this.wallGrids.set(levelId, new WallSpatialGrid())
|
||||
}
|
||||
return this.wallGrids.get(levelId)!;
|
||||
return this.wallGrids.get(levelId)!
|
||||
}
|
||||
|
||||
private getWallLength(wallId: string): number {
|
||||
const wall = this.walls.get(wallId);
|
||||
if (!wall) return 0;
|
||||
const dx = wall.end[0] - wall.start[0];
|
||||
const dy = wall.end[1] - wall.start[1];
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
const wall = this.walls.get(wallId)
|
||||
if (!wall) return 0
|
||||
const dx = wall.end[0] - wall.start[0]
|
||||
const dy = wall.end[1] - wall.start[1]
|
||||
return Math.sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
|
||||
private getWallHeight(wallId: string): number {
|
||||
const wall = this.walls.get(wallId);
|
||||
return wall?.height ?? 2.5; // Default wall height
|
||||
const wall = this.walls.get(wallId)
|
||||
return wall?.height ?? 2.5 // Default wall height
|
||||
}
|
||||
|
||||
// Called when nodes change
|
||||
handleNodeCreated(node: AnyNode, levelId: string) {
|
||||
if (node.type === "wall") {
|
||||
const wall = node as WallNode;
|
||||
this.walls.set(wall.id, wall);
|
||||
} else if (node.type === "item") {
|
||||
const item = node as ItemNode;
|
||||
if (
|
||||
item.asset.attachTo === "wall" ||
|
||||
item.asset.attachTo === "wall-side"
|
||||
) {
|
||||
if (node.type === 'wall') {
|
||||
const wall = node as WallNode
|
||||
this.walls.set(wall.id, wall)
|
||||
} else if (node.type === 'item') {
|
||||
const item = node as ItemNode
|
||||
if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
|
||||
// Wall-attached item - use parentId as the wall ID
|
||||
const wallId = item.parentId;
|
||||
const wallId = item.parentId
|
||||
if (wallId && this.walls.has(wallId)) {
|
||||
const wallLength = this.getWallLength(wallId);
|
||||
const wallLength = this.getWallLength(wallId)
|
||||
if (wallLength > 0) {
|
||||
const [width, height] = item.asset.dimensions;
|
||||
const halfW = width / wallLength / 2;
|
||||
const [width, height] = item.asset.dimensions
|
||||
const halfW = width / wallLength / 2
|
||||
// Calculate t from local X position (position[0] is distance along wall)
|
||||
const t = item.position[0] / wallLength;
|
||||
const t = item.position[0] / wallLength
|
||||
// position[1] is the bottom of the item
|
||||
this.getWallGrid(levelId).insert({
|
||||
itemId: item.id,
|
||||
@@ -67,7 +61,7 @@ export class SpatialGridManager {
|
||||
tEnd: t + halfW,
|
||||
yStart: item.position[1],
|
||||
yEnd: item.position[1] + height,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (!item.asset.attachTo) {
|
||||
@@ -77,31 +71,28 @@ export class SpatialGridManager {
|
||||
item.position,
|
||||
item.asset.dimensions,
|
||||
item.rotation,
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNodeUpdated(node: AnyNode, levelId: string) {
|
||||
if (node.type === "wall") {
|
||||
const wall = node as WallNode;
|
||||
this.walls.set(wall.id, wall);
|
||||
} else if (node.type === "item") {
|
||||
const item = node as ItemNode;
|
||||
if (
|
||||
item.asset.attachTo === "wall" ||
|
||||
item.asset.attachTo === "wall-side"
|
||||
) {
|
||||
if (node.type === 'wall') {
|
||||
const wall = node as WallNode
|
||||
this.walls.set(wall.id, wall)
|
||||
} else if (node.type === 'item') {
|
||||
const item = node as ItemNode
|
||||
if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
|
||||
// Remove old placement and re-insert
|
||||
this.getWallGrid(levelId).removeByItemId(item.id);
|
||||
const wallId = item.parentId;
|
||||
this.getWallGrid(levelId).removeByItemId(item.id)
|
||||
const wallId = item.parentId
|
||||
if (wallId && this.walls.has(wallId)) {
|
||||
const wallLength = this.getWallLength(wallId);
|
||||
const wallLength = this.getWallLength(wallId)
|
||||
if (wallLength > 0) {
|
||||
const [width, height] = item.asset.dimensions;
|
||||
const halfW = width / wallLength / 2;
|
||||
const [width, height] = item.asset.dimensions
|
||||
const halfW = width / wallLength / 2
|
||||
// Calculate t from local X position (position[0] is distance along wall)
|
||||
const t = item.position[0] / wallLength;
|
||||
const t = item.position[0] / wallLength
|
||||
// position[1] is the bottom of the item
|
||||
this.getWallGrid(levelId).insert({
|
||||
itemId: item.id,
|
||||
@@ -110,7 +101,7 @@ export class SpatialGridManager {
|
||||
tEnd: t + halfW,
|
||||
yStart: item.position[1],
|
||||
yEnd: item.position[1] + height,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (!item.asset.attachTo) {
|
||||
@@ -119,22 +110,22 @@ export class SpatialGridManager {
|
||||
item.position,
|
||||
item.asset.dimensions,
|
||||
item.rotation,
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNodeDeleted(nodeId: string, nodeType: string, levelId: string) {
|
||||
if (nodeType === "wall") {
|
||||
this.walls.delete(nodeId);
|
||||
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);
|
||||
return removedItemIds; // Caller can use this to delete the items from scene
|
||||
} else if (nodeType === "item") {
|
||||
this.getFloorGrid(levelId).remove(nodeId);
|
||||
this.getWallGrid(levelId).removeByItemId(nodeId);
|
||||
const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId)
|
||||
return removedItemIds // Caller can use this to delete the items from scene
|
||||
} else if (nodeType === 'item') {
|
||||
this.getFloorGrid(levelId).remove(nodeId)
|
||||
this.getWallGrid(levelId).removeByItemId(nodeId)
|
||||
}
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
// Query methods
|
||||
@@ -145,8 +136,8 @@ export class SpatialGridManager {
|
||||
rotation: [number, number, number],
|
||||
ignoreIds?: string[],
|
||||
) {
|
||||
const grid = this.getFloorGrid(levelId);
|
||||
return grid.canPlace(position, dimensions, rotation, ignoreIds);
|
||||
const grid = this.getFloorGrid(levelId)
|
||||
return grid.canPlace(position, dimensions, rotation, ignoreIds)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,14 +157,14 @@ export class SpatialGridManager {
|
||||
dimensions: [number, number, number],
|
||||
ignoreIds?: string[],
|
||||
) {
|
||||
const wallLength = this.getWallLength(wallId);
|
||||
const wallLength = this.getWallLength(wallId)
|
||||
if (wallLength === 0) {
|
||||
return { valid: false, conflictIds: [] };
|
||||
return { valid: false, conflictIds: [] }
|
||||
}
|
||||
const wallHeight = this.getWallHeight(wallId);
|
||||
const wallHeight = this.getWallHeight(wallId)
|
||||
// Convert local X position to parametric t (0-1)
|
||||
const tCenter = localX / wallLength;
|
||||
const [itemWidth, itemHeight] = dimensions;
|
||||
const tCenter = localX / wallLength
|
||||
const [itemWidth, itemHeight] = dimensions
|
||||
return this.getWallGrid(levelId).canPlaceOnWall(
|
||||
wallId,
|
||||
wallLength,
|
||||
@@ -183,24 +174,24 @@ export class SpatialGridManager {
|
||||
localY,
|
||||
itemHeight,
|
||||
ignoreIds,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
getWallForItem(levelId: string, itemId: string): string | undefined {
|
||||
return this.getWallGrid(levelId).getWallForItem(itemId);
|
||||
return this.getWallGrid(levelId).getWallForItem(itemId)
|
||||
}
|
||||
|
||||
clearLevel(levelId: string) {
|
||||
this.floorGrids.delete(levelId);
|
||||
this.wallGrids.delete(levelId);
|
||||
this.floorGrids.delete(levelId)
|
||||
this.wallGrids.delete(levelId)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.floorGrids.clear();
|
||||
this.wallGrids.clear();
|
||||
this.walls.clear();
|
||||
this.floorGrids.clear()
|
||||
this.wallGrids.clear()
|
||||
this.walls.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const spatialGridManager = new SpatialGridManager();
|
||||
export const spatialGridManager = new SpatialGridManager()
|
||||
|
||||
@@ -1,70 +1,67 @@
|
||||
import { AnyNode } from "../../schema";
|
||||
import useScene from "../../store/use-scene";
|
||||
import { spatialGridManager } from "./spatial-grid-manager";
|
||||
import type { AnyNode } from '../../schema'
|
||||
import useScene from '../../store/use-scene'
|
||||
import { spatialGridManager } from './spatial-grid-manager'
|
||||
|
||||
export function resolveLevelId(
|
||||
node: AnyNode,
|
||||
nodes: Record<string, AnyNode>,
|
||||
): string {
|
||||
export function resolveLevelId(node: AnyNode, nodes: Record<string, AnyNode>): string {
|
||||
// If the node itself is a level
|
||||
if (node.type === "level") return node.id;
|
||||
if (node.type === 'level') return node.id
|
||||
|
||||
// Walk up parent chain to find level
|
||||
// This assumes you track parentId or can derive it
|
||||
let current: AnyNode | undefined = node;
|
||||
let current: AnyNode | undefined = node
|
||||
|
||||
while (current) {
|
||||
if (current.type === "level") return current.id;
|
||||
if (current.type === 'level') return current.id
|
||||
// Find parent (you might need to add parentId to your schema or derive it)
|
||||
if (!current.parentId) {
|
||||
current = undefined;
|
||||
current = undefined
|
||||
} else {
|
||||
current = nodes[current.parentId];
|
||||
current = nodes[current.parentId]
|
||||
}
|
||||
}
|
||||
|
||||
return "default"; // fallback for orphaned items
|
||||
return 'default' // fallback for orphaned items
|
||||
}
|
||||
|
||||
// Call this once at app initialization
|
||||
export function initSpatialGridSync() {
|
||||
const store = useScene;
|
||||
const store = useScene
|
||||
|
||||
// Subscribe to all changes
|
||||
store.subscribe((state, prevState) => {
|
||||
// Detect added nodes
|
||||
for (const [id, node] of Object.entries(state.nodes)) {
|
||||
if (!prevState.nodes[id as AnyNode["id"]]) {
|
||||
const levelId = resolveLevelId(node, state.nodes);
|
||||
spatialGridManager.handleNodeCreated(node, levelId);
|
||||
if (!prevState.nodes[id as AnyNode['id']]) {
|
||||
const levelId = resolveLevelId(node, state.nodes)
|
||||
spatialGridManager.handleNodeCreated(node, levelId)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect removed nodes
|
||||
for (const [id, node] of Object.entries(prevState.nodes)) {
|
||||
if (!state.nodes[id as AnyNode["id"]]) {
|
||||
const levelId = resolveLevelId(node, prevState.nodes);
|
||||
spatialGridManager.handleNodeDeleted(id, node.type, levelId);
|
||||
if (!state.nodes[id as AnyNode['id']]) {
|
||||
const levelId = resolveLevelId(node, prevState.nodes)
|
||||
spatialGridManager.handleNodeDeleted(id, node.type, levelId)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect updated nodes (items with position/rotation/parentId 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") {
|
||||
const prev = prevState.nodes[id as AnyNode['id']]
|
||||
if (prev && node.type === 'item' && prev.type === 'item') {
|
||||
if (
|
||||
!arraysEqual(node.position, prev.position) ||
|
||||
!arraysEqual(node.rotation, prev.rotation) ||
|
||||
node.parentId !== prev.parentId
|
||||
) {
|
||||
const levelId = resolveLevelId(node, state.nodes);
|
||||
spatialGridManager.handleNodeUpdated(node, levelId);
|
||||
const levelId = resolveLevelId(node, state.nodes)
|
||||
spatialGridManager.handleNodeUpdated(node, levelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function arraysEqual(a: number[], b: number[]): boolean {
|
||||
return a.length === b.length && a.every((v, i) => v === b[i]);
|
||||
return a.length === b.length && a.every((v, i) => v === b[i])
|
||||
}
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
type CellKey = `${number},${number}`;
|
||||
type CellKey = `${number},${number}`
|
||||
|
||||
interface GridCell {
|
||||
itemIds: Set<string>;
|
||||
itemIds: Set<string>
|
||||
}
|
||||
|
||||
interface SpatialGridConfig {
|
||||
cellSize: number; // e.g., 0.5 meters = Sims-style half-tile
|
||||
cellSize: number // e.g., 0.5 meters = Sims-style half-tile
|
||||
}
|
||||
|
||||
export class SpatialGrid {
|
||||
private cells = new Map<CellKey, GridCell>();
|
||||
private itemCells = new Map<string, Set<CellKey>>(); // reverse lookup
|
||||
private cells = new Map<CellKey, GridCell>()
|
||||
private itemCells = new Map<string, Set<CellKey>>() // reverse lookup
|
||||
|
||||
constructor(private config: SpatialGridConfig) {}
|
||||
|
||||
private posToCell(x: number, z: number): [number, number] {
|
||||
return [
|
||||
Math.floor(x / this.config.cellSize),
|
||||
Math.floor(z / this.config.cellSize),
|
||||
];
|
||||
return [Math.floor(x / this.config.cellSize), Math.floor(z / this.config.cellSize)]
|
||||
}
|
||||
|
||||
private cellKey(cx: number, cz: number): CellKey {
|
||||
return `${cx},${cz}`;
|
||||
return `${cx},${cz}`
|
||||
}
|
||||
|
||||
// Get all cells an item occupies based on its AABB
|
||||
@@ -33,34 +30,34 @@ export class SpatialGrid {
|
||||
): CellKey[] {
|
||||
// Simplified: axis-aligned bounding box
|
||||
// For full rotation support, compute rotated corners
|
||||
const [x, , z] = position;
|
||||
const [w, , d] = dimensions;
|
||||
const yRot = rotation[1]; // Y-axis rotation
|
||||
const [x, , z] = position
|
||||
const [w, , d] = dimensions
|
||||
const yRot = rotation[1] // Y-axis rotation
|
||||
|
||||
// Compute rotated footprint (simplified for 90° increments)
|
||||
const cos = Math.abs(Math.cos(yRot));
|
||||
const sin = Math.abs(Math.sin(yRot));
|
||||
const rotatedW = w * cos + d * sin;
|
||||
const rotatedD = w * sin + d * cos;
|
||||
const cos = Math.abs(Math.cos(yRot))
|
||||
const sin = Math.abs(Math.sin(yRot))
|
||||
const rotatedW = w * cos + d * sin
|
||||
const rotatedD = w * sin + d * cos
|
||||
|
||||
const minX = x - rotatedW / 2;
|
||||
const maxX = x + rotatedW / 2;
|
||||
const minZ = z - rotatedD / 2;
|
||||
const maxZ = z + rotatedD / 2;
|
||||
const minX = x - rotatedW / 2
|
||||
const maxX = x + rotatedW / 2
|
||||
const minZ = z - rotatedD / 2
|
||||
const maxZ = z + rotatedD / 2
|
||||
|
||||
const [minCx, minCz] = this.posToCell(minX, minZ);
|
||||
const [minCx, minCz] = this.posToCell(minX, minZ)
|
||||
// Use exclusive upper bound: subtract epsilon so exact boundaries don't overlap
|
||||
// This allows adjacent items (touching but not overlapping) to not conflict
|
||||
const epsilon = 1e-6;
|
||||
const [maxCx, maxCz] = this.posToCell(maxX - epsilon, maxZ - epsilon);
|
||||
const epsilon = 1e-6
|
||||
const [maxCx, maxCz] = this.posToCell(maxX - epsilon, maxZ - epsilon)
|
||||
|
||||
const keys: CellKey[] = [];
|
||||
const keys: CellKey[] = []
|
||||
for (let cx = minCx; cx <= maxCx; cx++) {
|
||||
for (let cz = minCz; cz <= maxCz; cz++) {
|
||||
keys.push(this.cellKey(cx, cz));
|
||||
keys.push(this.cellKey(cx, cz))
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
return keys
|
||||
}
|
||||
|
||||
// Register an item
|
||||
@@ -70,33 +67,33 @@ export class SpatialGrid {
|
||||
dimensions: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
) {
|
||||
const cellKeys = this.getItemCells(position, dimensions, rotation);
|
||||
const cellKeys = this.getItemCells(position, dimensions, rotation)
|
||||
|
||||
this.itemCells.set(itemId, new Set(cellKeys));
|
||||
this.itemCells.set(itemId, new Set(cellKeys))
|
||||
|
||||
for (const key of cellKeys) {
|
||||
if (!this.cells.has(key)) {
|
||||
this.cells.set(key, { itemIds: new Set() });
|
||||
this.cells.set(key, { itemIds: new Set() })
|
||||
}
|
||||
this.cells.get(key)!.itemIds.add(itemId);
|
||||
this.cells.get(key)!.itemIds.add(itemId)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove an item
|
||||
remove(itemId: string) {
|
||||
const cellKeys = this.itemCells.get(itemId);
|
||||
if (!cellKeys) return;
|
||||
const cellKeys = this.itemCells.get(itemId)
|
||||
if (!cellKeys) return
|
||||
|
||||
for (const key of cellKeys) {
|
||||
const cell = this.cells.get(key);
|
||||
const cell = this.cells.get(key)
|
||||
if (cell) {
|
||||
cell.itemIds.delete(itemId);
|
||||
cell.itemIds.delete(itemId)
|
||||
if (cell.itemIds.size === 0) {
|
||||
this.cells.delete(key);
|
||||
this.cells.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.itemCells.delete(itemId);
|
||||
this.itemCells.delete(itemId)
|
||||
}
|
||||
|
||||
// Update = remove + insert
|
||||
@@ -106,8 +103,8 @@ export class SpatialGrid {
|
||||
dimensions: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
) {
|
||||
this.remove(itemId);
|
||||
this.insert(itemId, position, dimensions, rotation);
|
||||
this.remove(itemId)
|
||||
this.insert(itemId, position, dimensions, rotation)
|
||||
}
|
||||
|
||||
// Query: is this placement valid?
|
||||
@@ -117,16 +114,16 @@ export class SpatialGrid {
|
||||
rotation: [number, number, number],
|
||||
ignoreIds: string[] = [],
|
||||
): { valid: boolean; conflictIds: string[] } {
|
||||
const cellKeys = this.getItemCells(position, dimensions, rotation);
|
||||
const ignoreSet = new Set(ignoreIds);
|
||||
const conflicts = new Set<string>();
|
||||
const cellKeys = this.getItemCells(position, dimensions, rotation)
|
||||
const ignoreSet = new Set(ignoreIds)
|
||||
const conflicts = new Set<string>()
|
||||
|
||||
for (const key of cellKeys) {
|
||||
const cell = this.cells.get(key);
|
||||
const cell = this.cells.get(key)
|
||||
if (cell) {
|
||||
for (const id of cell.itemIds) {
|
||||
if (!ignoreSet.has(id)) {
|
||||
conflicts.add(id);
|
||||
conflicts.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,29 +132,29 @@ export class SpatialGrid {
|
||||
return {
|
||||
valid: conflicts.size === 0,
|
||||
conflictIds: [...conflicts],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Query: get all items near a point (for snapping, selection, etc.)
|
||||
queryRadius(x: number, z: number, radius: number): string[] {
|
||||
const cellRadius = Math.ceil(radius / this.config.cellSize);
|
||||
const [cx, cz] = this.posToCell(x, z);
|
||||
const found = new Set<string>();
|
||||
const cellRadius = Math.ceil(radius / this.config.cellSize)
|
||||
const [cx, cz] = this.posToCell(x, z)
|
||||
const found = new Set<string>()
|
||||
|
||||
for (let dx = -cellRadius; dx <= cellRadius; dx++) {
|
||||
for (let dz = -cellRadius; dz <= cellRadius; dz++) {
|
||||
const cell = this.cells.get(this.cellKey(cx + dx, cz + dz));
|
||||
const cell = this.cells.get(this.cellKey(cx + dx, cz + dz))
|
||||
if (cell) {
|
||||
for (const id of cell.itemIds) {
|
||||
found.add(id);
|
||||
found.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...found];
|
||||
return [...found]
|
||||
}
|
||||
|
||||
getItemCount(): number {
|
||||
return this.itemCells.size;
|
||||
return this.itemCells.size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import { useCallback } from "react";
|
||||
import { LevelNode, WallNode } from "../../schema";
|
||||
import { spatialGridManager } from "./spatial-grid-manager";
|
||||
import { useCallback } from 'react'
|
||||
import type { LevelNode, WallNode } from '../../schema'
|
||||
import { spatialGridManager } from './spatial-grid-manager'
|
||||
|
||||
export function useSpatialQuery() {
|
||||
const canPlaceOnFloor = useCallback(
|
||||
(
|
||||
levelId: LevelNode["id"],
|
||||
levelId: LevelNode['id'],
|
||||
position: [number, number, number],
|
||||
dimensions: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
ignoreIds?: string[],
|
||||
) => {
|
||||
return spatialGridManager.canPlaceOnFloor(
|
||||
levelId,
|
||||
position,
|
||||
dimensions,
|
||||
rotation,
|
||||
ignoreIds,
|
||||
);
|
||||
return spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds)
|
||||
},
|
||||
[],
|
||||
);
|
||||
)
|
||||
|
||||
const canPlaceOnWall = useCallback(
|
||||
(
|
||||
levelId: LevelNode["id"],
|
||||
wallId: WallNode["id"],
|
||||
levelId: LevelNode['id'],
|
||||
wallId: WallNode['id'],
|
||||
localX: number,
|
||||
localY: number,
|
||||
dimensions: [number, number, number],
|
||||
@@ -38,10 +32,10 @@ export function useSpatialQuery() {
|
||||
localY,
|
||||
dimensions,
|
||||
ignoreIds,
|
||||
);
|
||||
)
|
||||
},
|
||||
[],
|
||||
);
|
||||
)
|
||||
|
||||
return { canPlaceOnFloor, canPlaceOnWall };
|
||||
return { canPlaceOnFloor, canPlaceOnWall }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
interface WallItemPlacement {
|
||||
itemId: string;
|
||||
wallId: string;
|
||||
tStart: number; // 0-1 parametric position along wall
|
||||
tEnd: number;
|
||||
yStart: number; // height range
|
||||
yEnd: number;
|
||||
itemId: string
|
||||
wallId: string
|
||||
tStart: number // 0-1 parametric position along wall
|
||||
tEnd: number
|
||||
yStart: number // height range
|
||||
yEnd: number
|
||||
}
|
||||
|
||||
export class WallSpatialGrid {
|
||||
private wallItems = new Map<string, WallItemPlacement[]>(); // wallId -> placements
|
||||
private itemToWall = new Map<string, string>(); // itemId -> wallId (reverse lookup)
|
||||
private wallItems = new Map<string, WallItemPlacement[]>() // wallId -> placements
|
||||
private itemToWall = new Map<string, string>() // itemId -> wallId (reverse lookup)
|
||||
|
||||
canPlaceOnWall(
|
||||
wallId: string,
|
||||
@@ -21,82 +21,82 @@ export class WallSpatialGrid {
|
||||
itemHeight: number,
|
||||
ignoreIds: string[] = [],
|
||||
): { valid: boolean; conflictIds: string[] } {
|
||||
const halfW = itemWidth / wallLength / 2;
|
||||
const tStart = tCenter - halfW;
|
||||
const tEnd = tCenter + halfW;
|
||||
const halfW = itemWidth / wallLength / 2
|
||||
const tStart = tCenter - halfW
|
||||
const tEnd = tCenter + halfW
|
||||
// yBottom is the bottom of the item, so yEnd = yBottom + itemHeight
|
||||
const yStart = yBottom;
|
||||
const yEnd = yBottom + itemHeight;
|
||||
const yStart = yBottom
|
||||
const yEnd = yBottom + itemHeight
|
||||
|
||||
// Check wall boundaries
|
||||
if (tStart < 0 || tEnd > 1 || yStart < 0 || yEnd > wallHeight) {
|
||||
return { valid: false, conflictIds: [] };
|
||||
return { valid: false, conflictIds: [] }
|
||||
}
|
||||
|
||||
const existing = this.wallItems.get(wallId) ?? [];
|
||||
const ignoreSet = new Set(ignoreIds);
|
||||
const conflicts: string[] = [];
|
||||
const existing = this.wallItems.get(wallId) ?? []
|
||||
const ignoreSet = new Set(ignoreIds)
|
||||
const conflicts: string[] = []
|
||||
|
||||
for (const placement of existing) {
|
||||
if (ignoreSet.has(placement.itemId)) continue;
|
||||
if (ignoreSet.has(placement.itemId)) continue
|
||||
|
||||
const tOverlap = tStart < placement.tEnd && tEnd > placement.tStart;
|
||||
const yOverlap = yStart < placement.yEnd && yEnd > placement.yStart;
|
||||
const tOverlap = tStart < placement.tEnd && tEnd > placement.tStart
|
||||
const yOverlap = yStart < placement.yEnd && yEnd > placement.yStart
|
||||
|
||||
if (tOverlap && yOverlap) {
|
||||
conflicts.push(placement.itemId);
|
||||
conflicts.push(placement.itemId)
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: conflicts.length === 0, conflictIds: conflicts };
|
||||
return { valid: conflicts.length === 0, conflictIds: conflicts }
|
||||
}
|
||||
|
||||
insert(placement: WallItemPlacement) {
|
||||
const { wallId, itemId } = placement;
|
||||
const { wallId, itemId } = placement
|
||||
|
||||
if (!this.wallItems.has(wallId)) {
|
||||
this.wallItems.set(wallId, []);
|
||||
this.wallItems.set(wallId, [])
|
||||
}
|
||||
this.wallItems.get(wallId)!.push(placement);
|
||||
this.itemToWall.set(itemId, wallId);
|
||||
this.wallItems.get(wallId)!.push(placement)
|
||||
this.itemToWall.set(itemId, wallId)
|
||||
}
|
||||
|
||||
remove(wallId: string, itemId: string) {
|
||||
const items = this.wallItems.get(wallId);
|
||||
const items = this.wallItems.get(wallId)
|
||||
if (items) {
|
||||
const idx = items.findIndex((p) => p.itemId === itemId);
|
||||
if (idx !== -1) items.splice(idx, 1);
|
||||
const idx = items.findIndex((p) => p.itemId === itemId)
|
||||
if (idx !== -1) items.splice(idx, 1)
|
||||
}
|
||||
this.itemToWall.delete(itemId);
|
||||
this.itemToWall.delete(itemId)
|
||||
}
|
||||
|
||||
removeByItemId(itemId: string) {
|
||||
const wallId = this.itemToWall.get(itemId);
|
||||
const wallId = this.itemToWall.get(itemId)
|
||||
if (wallId) {
|
||||
this.remove(wallId, itemId);
|
||||
this.remove(wallId, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
// Useful for when a wall is deleted - remove all items on it
|
||||
removeWall(wallId: string): string[] {
|
||||
const items = this.wallItems.get(wallId) ?? [];
|
||||
const removedIds = items.map((p) => p.itemId);
|
||||
const items = this.wallItems.get(wallId) ?? []
|
||||
const removedIds = items.map((p) => p.itemId)
|
||||
|
||||
for (const itemId of removedIds) {
|
||||
this.itemToWall.delete(itemId);
|
||||
this.itemToWall.delete(itemId)
|
||||
}
|
||||
this.wallItems.delete(wallId);
|
||||
this.wallItems.delete(wallId)
|
||||
|
||||
return removedIds; // Return removed item IDs in case you need to delete them from scene
|
||||
return removedIds // Return removed item IDs in case you need to delete them from scene
|
||||
}
|
||||
|
||||
// Get which wall an item is on
|
||||
getWallForItem(itemId: string): string | undefined {
|
||||
return this.itemToWall.get(itemId);
|
||||
return this.itemToWall.get(itemId)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.wallItems.clear();
|
||||
this.itemToWall.clear();
|
||||
this.wallItems.clear()
|
||||
this.itemToWall.clear()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user