remove community
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useLayoutEffect } from "react";
|
||||
import type * as THREE from "three";
|
||||
import { useLayoutEffect } from 'react'
|
||||
import type * as THREE from 'three'
|
||||
|
||||
export const sceneRegistry = {
|
||||
// Master lookup: ID -> Object3D
|
||||
@@ -22,7 +22,7 @@ export const sceneRegistry = {
|
||||
window: new Set<string>(),
|
||||
door: new Set<string>(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useRegistry(
|
||||
id: string,
|
||||
@@ -30,19 +30,19 @@ export function useRegistry(
|
||||
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,5 +1,5 @@
|
||||
import { getScaledDimensions } from '../../schema'
|
||||
import type { AnyNode, CeilingNode, ItemNode, SlabNode, WallNode } from '../../schema'
|
||||
import { getScaledDimensions } from '../../schema'
|
||||
import { SpatialGrid } from './spatial-grid'
|
||||
import { WallSpatialGrid } from './wall-spatial-grid'
|
||||
|
||||
@@ -14,10 +14,12 @@ export function pointInPolygon(px: number, pz: number, polygon: Array<[number, n
|
||||
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]
|
||||
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) {
|
||||
if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
|
||||
inside = !inside
|
||||
}
|
||||
}
|
||||
@@ -53,8 +55,14 @@ function getItemFootprint(
|
||||
* Test if two line segments (a1->a2) and (b1->b2) intersect.
|
||||
*/
|
||||
function segmentsIntersect(
|
||||
ax1: number, az1: number, ax2: number, az2: number,
|
||||
bx1: number, bz1: number, bx2: number, bz2: number,
|
||||
ax1: number,
|
||||
az1: number,
|
||||
ax2: number,
|
||||
az2: number,
|
||||
bx1: number,
|
||||
bz1: number,
|
||||
bx2: number,
|
||||
bz2: number,
|
||||
): boolean {
|
||||
const cross = (ox: number, oz: number, ax: number, az: number, bx: number, bz: number) =>
|
||||
(ax - ox) * (bz - oz) - (az - oz) * (bx - ox)
|
||||
@@ -64,15 +72,16 @@ function segmentsIntersect(
|
||||
const d3 = cross(ax1, az1, ax2, az2, bx1, bz1)
|
||||
const d4 = cross(ax1, az1, ax2, az2, bx2, bz2)
|
||||
|
||||
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
|
||||
((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
|
||||
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Collinear touching cases
|
||||
const onSeg = (px: number, pz: number, qx: number, qz: number, rx: number, rz: number) =>
|
||||
Math.min(px, qx) <= rx && rx <= Math.max(px, qx) &&
|
||||
Math.min(pz, qz) <= rz && rz <= Math.max(pz, qz)
|
||||
Math.min(px, qx) <= rx &&
|
||||
rx <= Math.max(px, qx) &&
|
||||
Math.min(pz, qz) <= rz &&
|
||||
rz <= Math.max(pz, qz)
|
||||
|
||||
if (d1 === 0 && onSeg(bx1, bz1, bx2, bz2, ax1, az1)) return true
|
||||
if (d2 === 0 && onSeg(bx1, bz1, bx2, bz2, ax2, az2)) return true
|
||||
@@ -86,16 +95,27 @@ function segmentsIntersect(
|
||||
* Test if a line segment intersects any edge of a polygon.
|
||||
*/
|
||||
function segmentIntersectsPolygon(
|
||||
sx1: number, sz1: number, sx2: number, sz2: number,
|
||||
sx1: number,
|
||||
sz1: number,
|
||||
sx2: number,
|
||||
sz2: number,
|
||||
polygon: Array<[number, number]>,
|
||||
): boolean {
|
||||
const n = polygon.length
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n
|
||||
if (segmentsIntersect(
|
||||
sx1, sz1, sx2, sz2,
|
||||
polygon[i]![0], polygon[i]![1], polygon[j]![0], polygon[j]![1],
|
||||
)) {
|
||||
if (
|
||||
segmentsIntersect(
|
||||
sx1,
|
||||
sz1,
|
||||
sx2,
|
||||
sz2,
|
||||
polygon[i]![0],
|
||||
polygon[i]![1],
|
||||
polygon[j]![0],
|
||||
polygon[j]![1],
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -129,10 +149,16 @@ export function itemOverlapsPolygon(
|
||||
// Check if any item edge intersects any polygon edge
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const j = (i + 1) % 4
|
||||
if (segmentIntersectsPolygon(
|
||||
corners[i]![0], corners[i]![1], corners[j]![0], corners[j]![1],
|
||||
polygon,
|
||||
)) return true
|
||||
if (
|
||||
segmentIntersectsPolygon(
|
||||
corners[i]![0],
|
||||
corners[i]![1],
|
||||
corners[j]![0],
|
||||
corners[j]![1],
|
||||
polygon,
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -144,8 +170,14 @@ export function itemOverlapsPolygon(
|
||||
* This prevents walls that just touch one point from being detected.
|
||||
*/
|
||||
function segmentsCollinearAndOverlap(
|
||||
ax1: number, az1: number, ax2: number, az2: number,
|
||||
bx1: number, bz1: number, bx2: number, bz2: number,
|
||||
ax1: number,
|
||||
az1: number,
|
||||
ax2: number,
|
||||
az2: number,
|
||||
bx1: number,
|
||||
bz1: number,
|
||||
bx2: number,
|
||||
bz2: number,
|
||||
): boolean {
|
||||
const EPSILON = 1e-6
|
||||
|
||||
@@ -159,8 +191,10 @@ function segmentsCollinearAndOverlap(
|
||||
|
||||
// Check if a point is on segment b
|
||||
const onSegment = (px: number, pz: number, qx: number, qz: number, rx: number, rz: number) =>
|
||||
Math.min(px, qx) - EPSILON <= rx && rx <= Math.max(px, qx) + EPSILON &&
|
||||
Math.min(pz, qz) - EPSILON <= rz && rz <= Math.max(pz, qz) + EPSILON
|
||||
Math.min(px, qx) - EPSILON <= rx &&
|
||||
rx <= Math.max(px, qx) + EPSILON &&
|
||||
Math.min(pz, qz) - EPSILON <= rz &&
|
||||
rz <= Math.max(pz, qz) + EPSILON
|
||||
|
||||
// BOTH endpoints of wall (a) must be on edge (b) for substantial overlap
|
||||
const a1OnB = onSegment(bx1, bz1, bx2, bz2, ax1, az1)
|
||||
@@ -314,7 +348,7 @@ export class SpatialGridManager {
|
||||
// position[1] is the bottom of the item
|
||||
this.getWallGrid(levelId).insert({
|
||||
itemId: item.id,
|
||||
wallId: wallId,
|
||||
wallId,
|
||||
tStart: t - halfW,
|
||||
tEnd: t + halfW,
|
||||
yStart: item.position[1],
|
||||
@@ -328,7 +362,12 @@ export class SpatialGridManager {
|
||||
// Ceiling item - use parentId as the ceiling ID
|
||||
const ceilingId = item.parentId
|
||||
if (ceilingId && this.ceilings.has(ceilingId)) {
|
||||
this.getCeilingGrid(ceilingId).insert(item.id, item.position, getScaledDimensions(item), item.rotation)
|
||||
this.getCeilingGrid(ceilingId).insert(
|
||||
item.id,
|
||||
item.position,
|
||||
getScaledDimensions(item),
|
||||
item.rotation,
|
||||
)
|
||||
this.itemCeilingMap.set(item.id, ceilingId)
|
||||
}
|
||||
} else if (!item.asset.attachTo) {
|
||||
@@ -367,7 +406,7 @@ export class SpatialGridManager {
|
||||
// position[1] is the bottom of the item
|
||||
this.getWallGrid(levelId).insert({
|
||||
itemId: item.id,
|
||||
wallId: wallId,
|
||||
wallId,
|
||||
tStart: t - halfW,
|
||||
tEnd: t + halfW,
|
||||
yStart: item.position[1],
|
||||
@@ -387,7 +426,12 @@ export class SpatialGridManager {
|
||||
// Insert into new ceiling grid
|
||||
const ceilingId = item.parentId
|
||||
if (ceilingId && this.ceilings.has(ceilingId)) {
|
||||
this.getCeilingGrid(ceilingId).insert(item.id, item.position, getScaledDimensions(item), item.rotation)
|
||||
this.getCeilingGrid(ceilingId).insert(
|
||||
item.id,
|
||||
item.position,
|
||||
getScaledDimensions(item),
|
||||
item.rotation,
|
||||
)
|
||||
this.itemCeilingMap.set(item.id, ceilingId)
|
||||
}
|
||||
} else if (!item.asset.attachTo) {
|
||||
@@ -530,9 +574,12 @@ export class SpatialGridManager {
|
||||
const slabMap = this.slabsByLevel.get(levelId)
|
||||
if (!slabMap) return 0
|
||||
|
||||
let maxElevation = -Infinity
|
||||
let maxElevation = Number.NEGATIVE_INFINITY
|
||||
for (const slab of slabMap.values()) {
|
||||
if (slab.polygon.length >= 3 && itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)) {
|
||||
if (
|
||||
slab.polygon.length >= 3 &&
|
||||
itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)
|
||||
) {
|
||||
// Check if item is entirely within a hole (if so, ignore this slab)
|
||||
// We consider it entirely in a hole if the item center is in the hole
|
||||
let inHole = false
|
||||
@@ -553,7 +600,7 @@ export class SpatialGridManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxElevation === -Infinity ? 0 : maxElevation
|
||||
return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -561,15 +608,11 @@ export class SpatialGridManager {
|
||||
* Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
|
||||
* Returns the highest slab elevation found, or 0 if none.
|
||||
*/
|
||||
getSlabElevationForWall(
|
||||
levelId: string,
|
||||
start: [number, number],
|
||||
end: [number, number],
|
||||
): number {
|
||||
getSlabElevationForWall(levelId: string, start: [number, number], end: [number, number]): number {
|
||||
const slabMap = this.slabsByLevel.get(levelId)
|
||||
if (!slabMap) return 0
|
||||
|
||||
let maxElevation = -Infinity
|
||||
let maxElevation = Number.NEGATIVE_INFINITY
|
||||
for (const slab of slabMap.values()) {
|
||||
if (slab.polygon.length < 3) continue
|
||||
if (!wallOverlapsPolygon(start, end, slab.polygon)) continue
|
||||
@@ -609,7 +652,7 @@ export class SpatialGridManager {
|
||||
if (elevation > maxElevation) maxElevation = elevation
|
||||
}
|
||||
}
|
||||
return maxElevation === -Infinity ? 0 : maxElevation
|
||||
return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { getScaledDimensions, type AnyNode, type AnyNodeId, type ItemNode, type SlabNode, type WallNode } from '../../schema'
|
||||
import {
|
||||
type AnyNode,
|
||||
type AnyNodeId,
|
||||
getScaledDimensions,
|
||||
type ItemNode,
|
||||
type SlabNode,
|
||||
type WallNode,
|
||||
} from '../../schema'
|
||||
import useScene from '../../store/use-scene'
|
||||
import { itemOverlapsPolygon, spatialGridManager, wallOverlapsPolygon } from './spatial-grid-manager'
|
||||
import {
|
||||
itemOverlapsPolygon,
|
||||
spatialGridManager,
|
||||
wallOverlapsPolygon,
|
||||
} from './spatial-grid-manager'
|
||||
|
||||
export function resolveLevelId(node: AnyNode, nodes: Record<string, AnyNode>): string {
|
||||
// If the node itself is a level
|
||||
@@ -13,10 +24,10 @@ export function resolveLevelId(node: AnyNode, nodes: Record<string, AnyNode>): s
|
||||
while (current) {
|
||||
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
|
||||
} else {
|
||||
if (current.parentId) {
|
||||
current = nodes[current.parentId]
|
||||
} else {
|
||||
current = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +82,11 @@ export function initSpatialGridSync() {
|
||||
|
||||
if (node.type === 'item' && prev.type === 'item') {
|
||||
if (
|
||||
!arraysEqual(node.position, prev.position) ||
|
||||
!arraysEqual(node.rotation, prev.rotation) ||
|
||||
!arraysEqual(node.scale, prev.scale) ||
|
||||
!(
|
||||
arraysEqual(node.position, prev.position) &&
|
||||
arraysEqual(node.rotation, prev.rotation) &&
|
||||
arraysEqual(node.scale, prev.scale)
|
||||
) ||
|
||||
node.parentId !== prev.parentId ||
|
||||
node.side !== prev.side
|
||||
) {
|
||||
@@ -85,7 +98,11 @@ export function initSpatialGridSync() {
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'slab' && prev.type === 'slab') {
|
||||
if (node.polygon !== prev.polygon || node.elevation !== prev.elevation || node.holes !== prev.holes) {
|
||||
if (
|
||||
node.polygon !== prev.polygon ||
|
||||
node.elevation !== prev.elevation ||
|
||||
node.holes !== prev.holes
|
||||
) {
|
||||
const levelId = resolveLevelId(node, state.nodes)
|
||||
spatialGridManager.handleNodeUpdated(node, levelId)
|
||||
|
||||
@@ -119,7 +136,15 @@ function markNodesOverlappingSlab(
|
||||
// Only floor items are affected by slabs
|
||||
if (item.asset.attachTo) continue
|
||||
if (resolveLevelId(node, nodes) !== slabLevelId) continue
|
||||
if (itemOverlapsPolygon(item.position, getScaledDimensions(item), item.rotation, slab.polygon, 0.01)) {
|
||||
if (
|
||||
itemOverlapsPolygon(
|
||||
item.position,
|
||||
getScaledDimensions(item),
|
||||
item.rotation,
|
||||
slab.polygon,
|
||||
0.01,
|
||||
)
|
||||
) {
|
||||
markDirty(node.id)
|
||||
}
|
||||
} else if (node.type === 'wall') {
|
||||
|
||||
@@ -49,7 +49,13 @@ export function useSpatialQuery() {
|
||||
rotation: [number, number, number],
|
||||
ignoreIds?: string[],
|
||||
) => {
|
||||
return spatialGridManager.canPlaceOnCeiling(ceilingId, position, dimensions, rotation, ignoreIds)
|
||||
return spatialGridManager.canPlaceOnCeiling(
|
||||
ceilingId,
|
||||
position,
|
||||
dimensions,
|
||||
rotation,
|
||||
ignoreIds,
|
||||
)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
@@ -140,7 +140,7 @@ export class WallSpatialGrid {
|
||||
|
||||
// Both are 'wall-side' - only conflict if they're on the same side
|
||||
// If either side is undefined, be conservative and assume conflict
|
||||
if (!newSide || !existing.side) {
|
||||
if (!(newSide && existing.side)) {
|
||||
return true
|
||||
}
|
||||
return newSide === existing.side
|
||||
|
||||
Reference in New Issue
Block a user