chore(road-editor): plan direct-edit task tree and add client test infra

Planning: parent design.md becomes the single authoritative contract
(constraint model with 6 kinds, handle manifest, coordinate/unit
layering, preview sequencing, storage layout and lazy migration, area
config snapshot, API contract). Work is split into four independently
verifiable child tasks with per-step gates and rollback points.

Test infra: pin vitest 4.1.11, add test:client:unit for client pure
logic, extend prettier globs to root *.ts so vitest.config.ts is checked.

Add .gitignore: the repo had none, so inputs/, outputs/, workbench-data/
and the client build output were untracked rather than ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 17:33:42 +08:00
parent 7dc7ede2b8
commit 93f09e399e
41 changed files with 2001 additions and 4 deletions

View File

@@ -0,0 +1,108 @@
# Canvas 与控制柄工具方案
## OpenLayers 能力确认
当前项目使用 `ol@10.10.0`。OpenLayers `VectorLayer` 默认由 Canvas renderer 绘制,当前工作台正是这种模式。类型声明可确认:
- `ol/interaction/Modify` 支持对 source/feature collection 的顶点修改,带 `modifystart` / `modifyend`、自定义 vertex style、pixel tolerance 与 hit detection。
- `ol/interaction/Snap` 支持顶点、边、交点吸附,且会改写交互事件的 coordinate/pixel供其他 pointer interaction 使用。
- `ol/interaction/Translate` 支持 feature 集合的拖动和 start/move/end 事件。
因此OL 具备 Canvas 渲染、命中测试、投影换算、视口同步与低层 pointer 交互所需的基础能力。它没有白板编辑器的命令历史、语义控制柄、约束求解、工具状态或多对象选择模型。
## 不使用原生 `Modify` 直接编辑道路面
`Modify` 会直接改变传入 feature 的 geometry 坐标,也允许插入/删除顶点。这适合通用 GIS 几何编辑,但不适合本项目:道路面和路口面是编译产物,顶点没有稳定的编辑语义,直接修改会使车道、标线和 connector 脱节。
它可被借鉴的只是事件生命周期、Canvas 命中与样式机制;道路源图层必须保持只读。
## 建议架构
```text
OpenLayers Map / VectorLayer (Canvas)
- 基线与预览 GeoJSON 图层:只读、由编译器/preview solver 提供
- editHandles VectorLayerPoint/LineString featureCanvas 画柄与影响区间
- EditPointerInteraction命中 handle -> 投影拖拽方向 -> 更新 draft command
- Snap只对批准的语义锚点/网格 source 生效
|
v
React EditSessionselection、draft、undo/redo
|
v
constraint preview solver -> 仅更新受影响 source
```
控制柄是独立的、可丢弃的 UI feature它的 properties 只携带 `handleId`,并通过 manifest 反查 semantic anchor。任何鼠标移动都先被投影为结构化约束值再重算预览绝不把 handle 的地理坐标直接写为道路 polygon 坐标。
## 引入独立 Canvas 白板库的判断
将 tldraw、Fabric、Konva 等作为覆盖层或替换 OL均会引入第二个 viewport、平移/缩放手势、坐标系与 hit-test 系统。与地理坐标、OL hit detection、原有图层开关和地图导航同步的成本很高且不能自动提供道路领域的约束模型。
除非“Quickdraw”是一个能在既有地图 Canvas 内以 OpenLayers coordinate/pointer API 运行的明确库,否则首选是继续把 OL 作为唯一 Canvas 和视口,使用它成熟的 interaction primitives加一个小而领域化的 `EditPointerInteraction`。这不是手写画布;渲染、坐标、事件和吸附都由 OL 提供,新增代码只负责道路语义。
## 成熟的 OpenLayers 扩展ol-ext
`ol-ext` 是当前最匹配的成熟扩展候选npm 最新版为 `4.0.38`2026-02-23BSD-3-Clause仓库仍持续维护。其 `interaction/Transform` 明确提供:
- 单独的 Canvas overlay layer 与可定制的 transform handles
-`Select` 交互同步选择;
- feature translate / scale / stretch / rotate
- `translatestart``translating``translateend``scalestart``scaling``scaleend` 等生命周期事件;
- 通过 `PointerInteraction` 实现,因此共享 OpenLayers 的地图坐标、事件分发与视口。
这能承担通用控制柄的视觉和 pointer 生命周期,消除自行实现 hover、命中、capture、拖拽状态机和控制柄绘制的大部分工作。
### 不能委托给 ol-ext 的部分
`Transform` 的最终行为仍是缩放、旋转或平移传入 feature 几何。其控制柄是矩形 bounding box而道路编辑需要沿道路法线的 edge offset、沿中心线的范围控制和路口特定的 cutback/corner radius这些语义并非该扩展的能力。
推荐将 ol-ext 只绑定到**短生命周期 proxy feature**
```text
语义 handle manifest -> proxy feature / ol-ext Transform
-> transform event -> 约束值法向偏移、station interval、cutback、radius
-> preview solver -> 新预览几何与新的 proxy
```
真实编译产物图层不可传给 `Transform`。对于不能表达为标准 translate/scale 的角部圆角和区间范围控制,仍需一个很小的领域 adapter但它只做“事件到约束”的投影不维护自己的 Canvas 或通用手势系统。
## 建议的验证闸门
在正式实现前完成一个隔离技术验证:将 `ol-ext` 的 Transform 绑定到单道路的临时 proxy确认它能在当前 `ol@10.10.0`、React map 生命周期下稳定工作,并验证一次拖拽只更新 proxy/preview source、不会改写基线 source、不会重建地图。通过后再将它固定为依赖不通过则保留 OpenLayers 原生 interaction 方案,避免在主分支承诺未经验证的扩展。
## 有限探针与回退策略
`ol-ext` 不是架构前提。探针只允许解决一个问题:复用通用 handle 的命中、pointer lifecycle 和视觉反馈。它有明确的时间上限和退出条件:
| 结果 | 动作 |
| --- | --- |
| 可在临时 proxy 上平稳拖拽,且不写基线 source、不重建 Map | 作为标准 transform 代理的可选依赖 |
| 与 `ol@10.10.0` 或 React 生命周期不兼容 | 删除探针,不迁移现有 MapCanvas走原生 OL 方案 |
| 能运行但矩形 scale/rotate 模型妨碍道路法线/路口约束 | 不把它用于道路手柄;仅保留可复用部分或删除 |
| 探针超过预设时间仍无法达到以上条件 | 停止排障,按回退方案推进 |
回退方案按优先顺序:
1. **OpenLayers 原生 proxy + 小型 `PointerInteraction` adapter**Point/LineString proxy 仍由 OL Canvas 绘制和命中,使用 `Snap` 处理吸附adapter 只处理已命中的少量语义 handle 到 constraint 的投影。它不重写渲染、相机、图层或通用选择,工作量受限。
2. **DOM handle overlay + 成熟手势库**:仅在当前选中道路/路口上放置少数固定像素的 React/HTML controls用成熟拖拽手势库处理 pointer capture每个地图 postrender 将它们用 `map.getPixelFromCoordinate()` 对齐。地图仍是唯一视口,未命中 handle 的事件继续交给 OL。适合复杂的范围、数值标签和路口控制但不是 Canvas 风格。
3. **不采用的方向**:迁移到 MapLibre/Leaflet 仅为获取绘制插件,或把 Quickdraw 叠到 OL 上。这会更换地图/视口基础设施,成本远高于有限 adapter且不能解决道路语义。
无论哪个输入 adapter 运行,`HandleManifest -> RoadEditOperation -> RoadConstraint -> preview solver` 的数据合同保持不变。因此探针失败最多替换输入层,不会推翻 schema、求解器或已保存编辑。
## Drawtonomy 的关系
公开 checkout 未包含 `Canvas.tsx`,也未在 package manifests 或 lockfile 中暴露 `tldraw`、Konva、Fabric、Excalidraw 或名为 Quickdraw 的依赖。因此不能从该源码确认其编辑器具体使用了哪个成熟 Canvas 库,只能借鉴其公开的对象图原则。
## Quickdraw 核查结论
检查 `/tmp/quickdraw` 后,确认它是 MIT 许可、零运行时依赖的完整无限白板 SDK而不是地图编辑器控制柄库
- `packages/core/src/editor.js` 在传入 container 内创建自己的 scene canvas 和 overlay canvas并实现独立 camera`x/y/z`、平移、缩放、pinch、pointer capture、hit test、selection 和 resize/rotate/arrow handles。
- `packages/core/src/store.js` 使用不可变 record、transaction、diff、batch undo/redo同一手势的连续更新会合并为一个 history entry。
- 内建形状和工具是封闭集合(笔、箭头、几何形状、文字等),公开资料中没有可把 OpenLayers feature 注册为原生 shape/handle 或让它共享外部 map camera 的扩展接口。
### 结论
不将 Quickdraw 作为覆盖层或替代 OpenLayers二者都会处理 pointer、wheel、pinch、屏幕到世界坐标转换和 camera接入后要持续同步两套视口且会破坏当前 OL 地图导航和图层命中。也不将 Quickdraw 的 flat drawing document 作为道路约束的存储格式。
可直接借鉴的成熟实现原则是固定像素尺寸的控制柄、overlay 与场景分离、pointer capture、一个 gesture 对应一个 transaction、不可变 diff 的 undo/redo、以及局部渲染。项目自身的 `EditSession` 应采用相同语义,但以 `RoadEditOperation` / `RoadConstraint` 为数据而非 Quickdraw shape。