Files
llm-wiki/docs/spark/2026-06-25-sigma-overlay-refactor-perf-design.md
2026-07-12 21:26:08 +08:00

171 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sigma 覆盖层重构 + 性能改造 设计文档
- 日期:2026-06-25
- 分支:`feat/sigma-overlay-refactor-perf`
- 关联 issue#64 拆分 `sigma-global-renderer.ts`#63 第 2 条(DOM 重绘重建节点)
- 范围:本 spec 覆盖 Phase 1#64+ Phase 2#63.2)两阶段。#63 第 1 条(云层签名全量遍历)与 #65(无障碍)**不在本 spec 范围内**,后续单独处理。
---
## 1. 背景与动机
全局视角的知识图谱用 Sigma 在 canvas 上画点和边,再在上面盖一层 DOM/SVG 覆盖物(社区云团、节点命中框、社区标签)。这层覆盖物的全部逻辑集中在
`packages/graph-engine/src/render/sigma-global-renderer.ts`,当前 **1386 行**,存在两个已确认的问题:
1. **单文件过大(#64**:云层几何、SVG 渲染、凸包计算、坐标变换都堆在一个文件里。仓库已把 wash 逻辑拆到
`community-wash.ts` / `community-washes.ts`,本文件应顺势拆分。
2. **每帧重建 DOM#63 第 2 条)**`renderSigmaOverlays()` 绑在 Sigma 的 `afterRender` 事件上
`sigma-global-renderer.ts:426`),**平移/缩放的每一帧**都先 `overlayRoot.replaceChildren()` 清空
`:623`),再全量重建所有节点命中框、云层 SVG,并重新绑定 click / pointerdown / mousedown / dragstart
监听(`:621-715`)。但相机移动时节点集合并未改变,只有屏幕坐标变了——这是典型的 DOM 抖动热路径。
> 注:PR #60 的 1000 节点回归已通过,当前规模不卡。本改造目标是降低维护成本、消除架构坏味道,为更大规模做准备。
### 设计原则
- 两个 Phase 相互独立、可分别合并。**Phase 1 是纯搬迁(零行为变化),Phase 2 才动行为。**
- 先拆后优化:在 1000+ 行单文件里直接改性能热路径风险叠加;拆完后 Phase 2 改动面更小、更好测。
- 依赖单向:`sigma-coordinates``community-cloud-geometry``sigma-overlay-svg` ← 主文件(编排)。
- 不改对外接口(`SigmaGlobalRenderer`),workbench 侧零感知。
- 遵循 YAGNI:不做通用 diff 框架、不顺手重构无关代码。
---
## 2. Phase 1#64):按职责拆 3 个 helper
`sigma-global-renderer.ts` 抽出三组纯函数 / 无状态工厂。主文件只保留编排核心:
`createSigmaGlobalRenderer`、生命周期、拖拽会话、事件接线、Graphology 属性映射、hit projector、edge style。
| 新文件 | 搬入内容 | 性质 |
|---|---|---|
| `render/sigma-coordinates.ts` | `sigmaWorldPointToScreenPoint``sigmaScreenPointToWorldPoint``overlayPointerScreenPoint` | 坐标变换 |
| `render/community-cloud-geometry.ts` | `SigmaCommunityCloud` / `SigmaCommunityCloudBasis` 接口、`sigmaCommunityCloudBasisById` + `...WithReuse` + `...WithNodePoint``sigmaCommunityCloudSignature``sigmaProjectedCloudHullPoints``clampPointToWorldEllipse``clampPointToScreenEllipse``sigmaCommunityCloud``convexHull2d` | 几何 / 凸包,多为纯函数 |
| `render/sigma-overlay-svg.ts` | `SIGMA_OVERLAY_SVG_NS``nextSigmaCloudFilterSequence``sigmaSharedCloudFilterDef``sigmaCloudSvg`,及 overlay 元素工厂 `sigmaOverlayButton` / `sigmaOverlayPassiveElement` / `applyOverlayBox` / `createSigmaOverlayRoot` | DOM / SVG 构造 |
依赖说明:`community-cloud-geometry` 中的投影函数(`sigmaProjectedCloudHullPoints``sigmaCommunityCloud`
依赖坐标变换,故 import `sigma-coordinates`,单向依赖。
预计主文件从 1386 行降到约 1000 行。
### Phase 1 验收
- 行为零变化。
- 现有 8 个 graph-engine 测试全绿(`npm test`),`npm run typecheck` 通过。
- 几何纯函数补独立单测(凸包、签名、clamp、坐标往返)——这些此前埋在主文件内未单独测。
> 这些函数当前均为**模块私有、未对外导出**,搬到新 helper 后由新 helper 各自 `export`(供单测直接
> import),主文件再 import 回来使用。**公开 API`SigmaGlobalRenderer` 等)与 `src/index.ts`
> barrel 不变**workbench 侧零感知。模块级计数器 `sigmaCloudFilterSequence` 随 SVG 函数整组搬走,
> 仍是同一个计数器,行为不变。
---
## 3. Phase 2#63.2):拆"结构更新"与"位置更新"
把唯一的 `renderSigmaOverlays()`(每帧 `replaceChildren` 全量重建)拆成两条路径。
### `rebuildSigmaOverlays()` — 仅数据 / 选中变化时调用
- 按 id 做 diff:复用已存在元素,新增缺失的、移除多余的;维护三张
`Map<id, element>`(节点命中框、社区云层 region、社区标签各一张)。
- 监听器(click / pointerdown / mousedown / dragstart**只在元素创建时绑一次**。
- 处理由数据决定的状态:selected / dim / searchHit / pinned、label 文本等。
> 覆盖层只渲染**这 3 类** DOM 元素。aggregation 容器画在 Sigma canvas 上(走 Graphology
> 属性),不属于本覆盖层,本改造不涉及。
### `repositionSigmaOverlays()` — 绑 `afterRender` / resize / 拖拽移动,每帧调用
- 遍历已存在元素,只更新屏幕坐标(`applyOverlayBox`)与云层 SVG 的 `points` 属性。
- **绝不** `replaceChildren` / `createElement` / `addEventListener`
### 调用点改造
| 位置 | 现状 | 改为 |
|---|---|---|
| 初始渲染(`:336` | `renderSigmaOverlays()` | `rebuildSigmaOverlays()` |
| 数据更新(`:382` | `renderSigmaOverlays()` | `rebuildSigmaOverlays()` |
| `afterRender` 相机移动 | `renderSigmaOverlays()` | `repositionSigmaOverlays()` |
| resize raf | `renderSigmaOverlays()` | `repositionSigmaOverlays()` |
| 拖拽提交/取消(`applyNodeDragPoint` 终态) | `renderSigmaOverlays()` | `rebuildSigmaOverlays()` |
选中变化走数据更新路径(触发 rebuild),因此 dim / selected 不进每帧热路径。
> 实现修正(TDD 发现):原以为"拖拽 → reposition"。实际上拖拽**过程中**的每帧位置更新由
> `sigma.refresh()` → `afterRender` → reposition 负责;而 `applyNodeDragPoint` 里那处直接调用是
> 拖拽**提交/取消的终态**,此时 pin 状态与永久坐标已变(数据变化),必须 rebuild 才能刷新
> `dataset.pinned` 等属性。
### 状态归属与不变量(防止双路径打架)
覆盖层完全自封闭:生产代码里只有本文件操作 `overlayRoot` 与几何/SVG 函数,无第二处
监听 `afterRender`。因此冲突风险只在文件内部的 rebuild / reposition 之间,必须守住以下不变量:
- **rebuild 是元素生命周期的唯一权威**:只有它能增删元素、维护三张 `Map<id, element>`、绑监听。
- **reposition 对结构只读、且幂等**:只改已存在元素的位置/box 与云 hull 投影;遇到表里有、
但当前 `adapterData` 已无的 id,安全跳过(不报错、不创建)。
- **reposition 每帧从 `cloudBasisByCommunityId` 重新投影云 hull**:拖拽中 basis 冻结、松手才重算,
reposition 必须每次重投影才能让云层跟手(与现有行为一致)。
- **`update()` 的执行顺序安全前提**:现状 `sigma.refresh()``:381`)→ 真实 Sigma 触发 `afterRender`
→ reposition**早于** 紧随其后的 rebuild`:382`)。因 reposition 满足"只读+容忍不同步+幂等"
这次提前的 reposition 至多对旧元素做一次无害定位,rebuild 随后纠正为准。
- **`afterRender``cameraUpdated` 都绑同一处理器**`:425-426`),reposition 一帧可能跑 2 次;
因幂等无害,暂不做 rAF 去重(YAGNI,后续按需)。
### 关键风险点
云层每帧的 hull 投影点会变,所以 reposition 必须更新 polygon 的 `points` 属性和容器 box
同时保留同一个 shape 元素及其 click 监听。这是整个改造里最需要小心的一处,测试需专门覆盖。
---
## 4. 错误处理与边界
- 两条路径都保留 `destroyed` 守卫。
- 销毁时清空三张 `Map<id, element>`,避免悬挂引用。
- diff 按 id 增删;id 不变但内容变(如 label 文本)在 rebuild 内更新属性。
- 不改对外接口 `SigmaGlobalRenderer`
---
## 5. 测试方案
### Phase 1
- 现有回归全绿 + 几何纯函数新单测(凸包、签名、clamp、坐标往返)。
### Phase 2(验收标准:可量化性能基线)
`test/sigma-global-renderer.test.ts` 复用现有 `fakeContainer()` / `fakeRuntime()` / `sigma.emit()`
基础设施(fake 元素内部已用 Map 记录子节点与监听,无需引入 jsdom):
1. **量化基线断言**(核心):渲染后记录元素引用与 createElement / addEventListener 调用计数 →
多次 `sigma.emit("afterRender")` → 断言**元素实例不变、createElement 计数不增、监听计数不增、
`replaceChildren` 在 reposition 路径未被调用**。
2. **行为保持**:点击云层 → 社区选中、点击节点 → 节点选中、拖拽仍生效(沿用现有 emit 用例)。
3. **结构 diff**:数据更新增 / 删节点后,元素表正确增删。
4. **需更新的现有断言**:现有 resize 测试(`test/sigma-global-renderer.test.ts:574`)当前断言
resize 后覆盖层元素被**新建**(`assert.notEqual`)。Phase 2 后元素改为**复用**,这条须改成
`assert.equal`。这是**预期内的行为变更,不是回归**——它恰好验证了"reposition 不再重建元素"。
5. **第三层手动回归**:在 codex 跑 1000 节点平移 / 缩放,主观确认不卡 + 基线测试通过
(按 CLAUDE.md 推送前测试规则)。
---
## 6. 分 Phase 推进计划
- **Phase 0(本次 spark**:写本 spec,分支 `feat/sigma-overlay-refactor-perf`
- **Phase 1**:拆 3 个 helper → 跑回归 → 分步 commit → 可单独开 PR 合入(纯重构、低风险)。
- **Phase 2**:基于 Phase 1 改双路径 + 量化基线测试 → codex 回归 → commit → PR。
- **文档**:每个含功能改动的 Phase,按 CLAUDE.md 更新 CHANGELOG / README / 版本号。
---
## 7. 验收清单(汇总)
- [ ] Phase 1:主文件显著瘦身、行为零变化、现有测试与 typecheck 全绿、几何函数有独立单测。
- [ ] Phase 2`afterRender` 路径不再销毁 / 重建 DOM、不重绑监听(量化测试断言通过)。
- [ ] Phase 2:点选社区 / 点选节点 / 拖拽行为与改造前一致。
- [ ] Phase 21000 节点 codex 手动回归平移缩放不卡。
- [ ] 对外接口 `SigmaGlobalRenderer` 无变化,workbench 无需改动。