spatial-index
本文回答:vjcad 空间查询有两层:实体级
Engine.spatialIndexManager(自动维护索引,search(bounds)/collides(bounds)/insert|remove|update(entity)/load(entities?));底层SpatialIndex是 RBush 风格裸索引(insert(item)/search(bbox)/load(items)/remove(item),item 需含minX/minY/maxX/maxY)。
一、最小用法(实体级,推荐)
import { Engine, BoundingBox, Point2D } from 'vjcad';
const idx = Engine.spatialIndexManager;
const bb = new BoundingBox(new Point2D(0, 0), new Point2D(100, 100));
const candidates = idx.search(bb); // 可能相交的实体数组(粗筛);未初始化时自动 load
const anyHit = idx.collides(bb); // 是否有实体相交(⚠️ 不懒加载:索引未初始化时直接返回 false)
// 一般无需手动维护;以下仅作兜底
idx.load(); // 整表重建(空参 = 用 Engine.currentSpace.items 全量加载)
idx.update(entity); // 强制刷新单个实体
自动维护:
Engine.addEntities/pushEntity入库模型空间时自动insert,erase时自动remove;search/collides前会扫描索引项,把isDirty或包围盒缓存已失效(setModified()会清缓存)的实体重新计算入树、把已删除实体移出。所以改几何后只要调了setModified()(实体自带的 move/rotate 等都会调),通常不用手动update。少数例外:改完几何已regen(清了isDirty)且包围盒缓存又被别处(如getScreenEntities)重算过,此时 dirty 检查看不出变化,需idx.update(ent)兜底。
坑位:
load(entities)/insert(entity)有静默门槛——实体必须isAlive === true且id != null,即先入文档(Engine.addEntities之后);手工new LineEnt未入文档的实体被跳过且无任何报错,索引/搜索结果为空。BoundingBox构造是两个Point2D顶点(new BoundingBox(p1, p2)),传new BoundingBox(x1,y1,x2,y2)四个数字会在构造器内对数字调clone()抛TypeError。
MCP 参考:vjcad/example/geometry/09spatialindex.js、types.d.ts SpatialIndexManager。
二、底层 SpatialIndex(RBush)
import { SpatialIndex } from 'vjcad';
const tree = new SpatialIndex(); // 构造参数 = 每节点最大条目数(默认 9)
tree.load([{ minX: 0, minY: 0, maxX: 10, maxY: 10, data: 'a' }]);
tree.insert({ minX: 5, minY: 5, maxX: 15, maxY: 15 });
tree.search({ minX: 0, minY: 0, maxX: 8, maxY: 8 }); // 相交项数组
tree.collides({ minX: 0, minY: 0, maxX: 8, maxY: 8 }); // boolean
tree.remove(item); tree.clear(); tree.all();
SpatialIndex存的是任意带 bbox 字段的数据项(不是实体对象),要自己维护item → entity映射。查询实体请优先用Engine.spatialIndexManager,它已做了 entity→index 的映射、dirty 检查与按文档隔离。
三、为什么需要
| 场景 | 直接 filter | SpatialIndex |
|---|---|---|
| 实体 < 1000 | 够用 | 没必要 |
| 实体 ~ 几万 | 偏慢 | 推荐 |
| 实体 ~ 百万 | 不可接受 | 必须 |
SpatialIndex 内部是 RBush(R-Tree 批量装载实现),SpatialIndexManager 对每个文档各持一棵树并把"先按 bbox 粗筛"做成了 O(log n) 查询。
四、配合 Layers 集合的批量判断
const layers = Engine.currentDoc.layers;
const visibleEntities = idx
.search(Engine.getScreenBoundsWcs())
.filter(e => layers.isLayerVisible(e.layerId));
MCP 参考:vjcad/doc/layer/layers.md、vjcad/example/geometry/09spatialindex.js。
五、与 Engine.getScreenEntities() 的区别
| API | 范围 | 性能 |
|---|---|---|
Engine.getScreenEntities(expandFactor?) |
当前屏幕(WCS)可见的实体 | 内部已用空间索引 / 视椎裁剪 |
spatialIndexManager.search(bb) |
任意 bbox | 你完全可控 |
如果只是查屏幕里有什么,直接用 Engine.getScreenEntities()(view/screen-bounds)。
反例
- 直接改坐标字段却不
setModified():索引的 dirty 检查发现不了,粗筛结果过期(改完调setModified(),或idx.update(ent)兜底)。 - 入库 / 删除后手动
idx.insert/idx.remove:重复劳动——pushEntity/erase已自动处理。 - 依赖
collides而索引从未初始化:直接返回 false;先调一次search或load()。 - 在每次画面
regen都重新 load 空间索引:构建本身就O(n log n),得不偿失。 - 拿
search(bb)的结果当"精确位于 bb 内":只是粗筛,需要再做精确判断。
参考(官方文档 MCP)
- 文档:
vjcad/doc/geometry/index.md、vjcad/doc/layer/layers.md - 示例:
vjcad/example/geometry/09spatialindex.js