> **本文回答**：选集事件 —— `selection:changed` / `boxselect:started` / `boxselect:ended`。

## 一、事件列表

| 常量 | 字符串 | payload |
|---|---|---|
| `SelectionChanged` | `selection:changed` | `{ selectedIds: string[] }` |
| `BoxSelectStarted` | `boxselect:started` | `{ mode }`——**恒为 `"window"`**（开始时不分左右拖向） |
| `BoxSelectEnded` | `boxselect:ended` | `{ mode, count, entityIds }`——`mode` 这里才是终态：`"window"`（左→右）或 `"crossing"`（右→左） |

## 二、selection:changed

**全量快照** payload（运行时核实：payload 是 `{ selectedIds: Array.from(this._selectedIds) }`——即**当前全集**，不是增量）：

```js
mapcadLayer.eventBus.on(VjmapextEvents.SelectionChanged, ({ selectedIds }) => {
  console.log('当前选中：', selectedIds);   // 全集；与 store.getSelectedIds() 一致
});
```

要拿"本次新增/移除"的增量，自己 diff 上一次的集合。

## 三、属性面板联动

```js
mapcadLayer.eventBus.on(VjmapextEvents.SelectionChanged, () => {
  const ids = mapcadLayer.store.getSelectedIds();
  if (ids.length === 1) {
    showProp(mapcadLayer.store.get(ids[0]));
  } else if (ids.length > 1) {
    showBatchProp(ids);
  } else {
    hideProp();
  }
});
```

## 四、选中高亮

默认 vjmapext 选中后实体会有描边 / 夹点；不需要额外代码。

如果要自定义选中颜色：

```js
let prev = new Set();
mapcadLayer.eventBus.on(VjmapextEvents.SelectionChanged, ({ selectedIds }) => {
  const now = new Set(selectedIds);
  // 新增选中
  for (const id of now) if (!prev.has(id)) {
    const e = mapcadLayer.store.get(id);
    e._origColor = e.color; e.color = 0xffff00; e.styleChanged();
  }
  // 取消选中
  for (const id of prev) if (!now.has(id)) {
    const e = mapcadLayer.store.get(id);
    if (e && e._origColor !== undefined) { e.color = e._origColor; e.styleChanged(); }
  }
  prev = now;
});
```

## 五、框选事件

触发条件：
- **空闲态**：必须 **按住 Shift 从空白处拖**才框选，且结果是 **toggle**（框内已选的取消、未选的加入）；不按 Shift 的普通拖拽是平移地图
- `getSelections` 等待中：从空白处直接拖即框选（追加；按 Shift 则 toggle）
- `getEntity({ multiple: true })` 等待中：从空白处拖即框选（toggle，受 `filter` 过滤）

方向：从左 → 右：窗口选（完全在框内）；从右 → 左：交叉选（与框相交即可）。只选 `editable` 的实体。

```js
mapcadLayer.eventBus.on(VjmapextEvents.BoxSelectStarted, () => {
  statusBar.textContent = '正在框选…';
});

mapcadLayer.eventBus.on(VjmapextEvents.BoxSelectEnded, () => {
  const ids = mapcadLayer.store.getSelectedIds();
  statusBar.textContent = `选中 ${ids.length} 个`;
});
```

顺序：**先** `selection:changed`（选集已改完），**后** `boxselect:ended`——所以在 `boxselect:ended` 里读 `getSelectedIds()` 拿到的已是框选后的结果。

## 六、编程修改选集

```js
mapcadLayer.store.setSelection([id1, id2]);     // 覆盖全集
mapcadLayer.store.addToSelection(id3);        // 追加（单个 id）
mapcadLayer.store.removeFromSelection(id1);   // 取消（单个 id）
mapcadLayer.store.clearSelection();            // 清空
```

每次都会 fire `selection:changed`。

## 七、监听单击选中 + 空白取消

vjmapext 默认：

- 单击实体 → 选中（`setSelection([id])` **替换**整个选集，不是追加）
- 单击空白 → 清空选集（按住 Shift 点空白不清空）
- Shift + 单击 → 追加 / 取消（toggleSelection；**是 Shift 不是 Ctrl**）
- Shift + 从空白处拖 → 框选（toggle）；不按 Shift 拖 = 平移地图
- 编辑模式下 `editable === false` 的实体普通点选不中；需 **Ctrl+Shift+单击** 强制选中（浏览模式则看 `selectableInReadonly`）

这些都是内置逻辑，不用自己写；但**组合 Shift** 或"单选模式"需要自定义 → 覆写 click 处理。

## 八、批量删除按钮

```js
mapcadLayer.eventBus.on(VjmapextEvents.SelectionChanged, () => {
  const ids = mapcadLayer.store.getSelectedIds();
  delBtn.disabled = ids.length === 0;
  delBtn.textContent = `删除 (${ids.length})`;
});

delBtn.onclick = () => {
  const ids = mapcadLayer.store.getSelectedIds();
  mapcadLayer.undoManager.beginTransaction();  // 删除选中
  ids.forEach(id => mapcadLayer.store.delete(id));
  mapcadLayer.undoManager.endTransaction();
};
```

## 九、过滤选集

```js
// 只保留 LINE 类型
const ids = mapcadLayer.store.getSelectedIds()
  .filter(id => mapcadLayer.store.get(id).type === 'LINE');
mapcadLayer.store.setSelection(ids);
```

## 十、常见坑

1. **payload 是全集不是增量**：`selectedIds` 是选后全集快照；要"新加/移除"得自己 diff。
2. **频繁选集变化 + handler 昂贵**：点击一片区域可能触发十几次；handler 加 debounce。
3. **程序设选集触发事件**：如果 handler 又 setSelection → 死循环；加 guard。
4. **框选没结束**：鼠标按下拖出去没抬起；BoxSelectEnded 不 fire → 检查 mouseup。
5. **框选跨不同图层**：只会选 mapcadLayer 里的实体，不选底图要素。

## 相关

- 实体事件 → [bus-entity](bus-entity.md)
- 命令事件 → [bus-command-mode](bus-command-mode.md)
- 输入选集 → [drawing/input-manager](../drawing/input-manager.md)
