> **本文回答**：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`）。

## 一、最小用法（实体级，推荐）

```js
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）

```js
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` 集合的批量判断

```js
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](../view/screen-bounds.md)）。

## 反例

- 直接改坐标字段却不 `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`
