唯杰地图 logo唯杰地图
VJMAP
EN

spatial-index

vjcad/references/geometry/spatial-index.md 4.8 KB84 行
原文

本文回答: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