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>
109 lines
8.5 KiB
Markdown
109 lines
8.5 KiB
Markdown
# 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 VectorLayer:Point/LineString feature,Canvas 画柄与影响区间
|
||
- EditPointerInteraction:命中 handle -> 投影拖拽方向 -> 更新 draft command
|
||
- Snap:只对批准的语义锚点/网格 source 生效
|
||
|
|
||
v
|
||
React EditSession(selection、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-23),BSD-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。
|