You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
5.5 KiB
200 lines
5.5 KiB
import * as THREE from 'three'
|
|
import rbush from 'rbush'
|
|
import { OBB } from 'three/examples/jsm/math/OBB'
|
|
// import { Octree } from 'three/examples/jsm/math/Octree.js'
|
|
// import { QuadTreeNode } from '@/core/QuadTree.ts'
|
|
// import { convexHull } from '@/core/ModelUtils.ts'
|
|
|
|
interface ItemEntry extends rbush.BBox {
|
|
id: string
|
|
obb: OBB
|
|
}
|
|
|
|
// 主管理器类
|
|
export default class ItemFindManager {
|
|
private spatialIndex = new rbush<ItemEntry>()
|
|
private items = new Map<string, ItemEntry>()
|
|
|
|
dispose() {
|
|
this.spatialIndex.clear()
|
|
this.items.clear()
|
|
}
|
|
|
|
constructor() {
|
|
}
|
|
|
|
// 添加或更新物品
|
|
addOrUpdate(...items: ItemMetrix[]): void {
|
|
for (const item of items) {
|
|
const aabb = itemToAABB(item)
|
|
const obb = itemToOBB(item)
|
|
|
|
if (this.items.has(item.id)) {
|
|
this.remove(item.id)
|
|
}
|
|
|
|
const entry: ItemEntry = {
|
|
id: item.id,
|
|
obb,
|
|
...aabb
|
|
}
|
|
|
|
this.items.set(item.id, entry)
|
|
this.spatialIndex.insert(entry)
|
|
}
|
|
}
|
|
|
|
// 移除物品
|
|
remove(...ids: string[]): void {
|
|
for (const id of ids) {
|
|
const entry = this.items.get(id)
|
|
if (entry) {
|
|
this.spatialIndex.remove(entry)
|
|
this.items.delete(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 位置查询
|
|
getItemsByPosition(x: number, z: number): string[] {
|
|
const candidates = this.spatialIndex.search({
|
|
minX: x,
|
|
minY: z,
|
|
maxX: x,
|
|
maxY: z
|
|
})
|
|
|
|
const point = new THREE.Vector3(x, 0, z)
|
|
return candidates.filter((item) => pointIntersectsOBB(point, item.obb)).map((item) => item.id)
|
|
}
|
|
|
|
// 距离查询
|
|
getItemsByDistance(x: number, z: number, distance: number): string[] {
|
|
const sphere = new THREE.Sphere(new THREE.Vector3(x, 0, z), distance)
|
|
|
|
const candidates = this.spatialIndex.search({
|
|
minX: x - distance,
|
|
minY: z - distance,
|
|
maxX: x + distance,
|
|
maxY: z + distance
|
|
})
|
|
|
|
return candidates.filter((item) => sphereIntersectsOBB(sphere, item.obb)).map((item) => item.id)
|
|
}
|
|
|
|
// 矩形区域查询(有交集)
|
|
getItemsByRect(x1: number, z1: number, x2: number, z2: number): string[] {
|
|
const rect = {
|
|
minX: Math.min(x1, x2),
|
|
maxX: Math.max(x1, x2),
|
|
minY: Math.min(z1, z2),
|
|
maxY: Math.max(z1, z2)
|
|
}
|
|
|
|
const candidates = this.spatialIndex.search(rect)
|
|
return candidates.filter((item) => rectIntersectsOBB(rect, item.obb)).map((item) => item.id)
|
|
}
|
|
}
|
|
|
|
function pointIntersectsOBB(point: THREE.Vector3, obb: OBB): boolean {
|
|
const box = new THREE.Box3()
|
|
setBox3FromOBB(box, obb)
|
|
return box.containsPoint(point)
|
|
}
|
|
|
|
function sphereIntersectsOBB(sphere: THREE.Sphere, obb: OBB): boolean {
|
|
const box = new THREE.Box3()
|
|
setBox3FromOBB(box, obb)
|
|
return box.intersectsSphere(sphere)
|
|
}
|
|
|
|
function rectIntersectsOBB(rect: { minX: number; minY: number; maxX: number; maxY: number }, obb: OBB): boolean {
|
|
// 简化判断:用 AABB 投影到 XZ 平面做矩形交叉检测
|
|
const aabb = new THREE.Box3()
|
|
setBox3FromOBB(aabb, obb)
|
|
const aabb2D = {
|
|
minX: aabb.min.x,
|
|
minY: aabb.min.z,
|
|
maxX: aabb.max.x,
|
|
maxY: aabb.max.z
|
|
}
|
|
|
|
return !(
|
|
rect.maxX < aabb2D.minX ||
|
|
rect.minX > aabb2D.maxX ||
|
|
rect.maxY < aabb2D.minY ||
|
|
rect.minY > aabb2D.maxY
|
|
)
|
|
}
|
|
|
|
export function itemToAABB(item: ItemMetrix): { minX: number; minY: number; maxX: number; maxY: number } {
|
|
// 假设所有物品都是立方体,尺寸为 scale.x × scale.z
|
|
const x = item.tf[0][0]
|
|
const z = item.tf[0][2]
|
|
const halfWidth = item.tf[2][0] / 2
|
|
const halfDepth = item.tf[2][2] / 2
|
|
|
|
return {
|
|
minX: x - halfWidth,
|
|
maxX: x + halfWidth,
|
|
minY: z - halfDepth,
|
|
maxY: z + halfDepth
|
|
}
|
|
}
|
|
|
|
export function itemToOBB(item: ItemMetrix): OBB {
|
|
const position = new THREE.Vector3(...item.tf[0])
|
|
const rotation = new THREE.Euler(
|
|
THREE.MathUtils.degToRad(item.tf[1][0]),
|
|
THREE.MathUtils.degToRad(item.tf[1][1]),
|
|
THREE.MathUtils.degToRad(item.tf[1][2]),
|
|
'XYZ'
|
|
)
|
|
const scale = new THREE.Vector3(...item.tf[2])
|
|
|
|
const matrix = new THREE.Matrix4()
|
|
.makeRotationFromEuler(rotation)
|
|
.premultiply(new THREE.Matrix4().makeTranslation(position.x, position.y, position.z))
|
|
.premultiply(new THREE.Matrix4().makeScale(scale.x, scale.y, scale.z))
|
|
|
|
const obb = new OBB(
|
|
new THREE.Vector3(),
|
|
new THREE.Vector3(0.5, 0.5, 0.5),
|
|
new THREE.Matrix3().setFromMatrix4(matrix)
|
|
)
|
|
|
|
return obb
|
|
}
|
|
|
|
function setBox3FromOBB(box: THREE.Box3, obb: OBB): THREE.Box3 {
|
|
const center = obb.center
|
|
const halfSize = new THREE.Vector3().copy(obb.halfSize)
|
|
const rotation = obb.rotation
|
|
|
|
// 8 个局部顶点
|
|
const vertices = [
|
|
new THREE.Vector3().copy(halfSize).multiply(new THREE.Vector3(1, 1, 1)),
|
|
new THREE.Vector3().copy(halfSize).multiply(new THREE.Vector3(-1, 1, 1)),
|
|
new THREE.Vector3().copy(halfSize).multiply(new THREE.Vector3(-1, -1, 1)),
|
|
new THREE.Vector3().copy(halfSize).multiply(new THREE.Vector3(1, -1, 1)),
|
|
new THREE.Vector3().copy(halfSize).multiply(new THREE.Vector3(1, 1, -1)),
|
|
new THREE.Vector3().copy(halfSize).multiply(new THREE.Vector3(-1, 1, -1)),
|
|
new THREE.Vector3().copy(halfSize).multiply(new THREE.Vector3(-1, -1, -1)),
|
|
new THREE.Vector3().copy(halfSize).multiply(new THREE.Vector3(1, -1, -1))
|
|
]
|
|
|
|
// 应用旋转和平移到每个顶点
|
|
const worldVertices = vertices.map((v) => {
|
|
return v.applyMatrix3(rotation).add(center)
|
|
})
|
|
|
|
// 构造包围盒
|
|
box.min.set(Infinity, Infinity, Infinity)
|
|
box.max.set(-Infinity, -Infinity, -Infinity)
|
|
|
|
for (const v of worldVertices) {
|
|
box.expandByPoint(v)
|
|
}
|
|
|
|
return box
|
|
}
|
|
|