chore(task): archive 08-26-direct-edit-map-editor
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "核对坐标分层(禁止用 3857 坐标差当米)、预览时序仲裁位置与开关关闭时的零残留要求"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/canvas-integration.md", "reason": "核对基线图层保持只读、手柄只带 handleId、未把地理坐标直接写成道路 polygon"}
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "检查客户端是否出现第二份约束模型或对 manifest 字段的局部 cast"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "检查是否重复实现图层更新、选择高亮或米制换算"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "核对 Map 单例未被重建、既有图层可见性与选择行为未回归"}
|
||||
@@ -0,0 +1,22 @@
|
||||
# 设计
|
||||
|
||||
技术合约不在本文重复定义。权威定义见父任务 `.trellis/tasks/08-26-direct-manipulation-road-editor/design.md` 的以下小节:
|
||||
|
||||
- 「Handle manifest」— 客户端读取的字段与 `reserves` 禁入区语义。
|
||||
- 「坐标与单位分层」— 客户端渲染用 EPSG:3857,指针位移换米必须先回 4326 再用球面距离。
|
||||
- 「预览时序与延迟预算」— 80ms 防抖、`pointerup` 最终请求、`previewSeq` 乱序丢弃、`degraded` 处理。
|
||||
- 「交互与预览」「编辑所有权」— 只读基线图层、三个独立 source、道路区间与 junction reserve 的边界。
|
||||
- 「客户端边界」— 遗留 `app.js` 不在范围、`vitest` 覆盖范围。
|
||||
- 「上线与回滚形态」— `directEdit` 开关默认关闭时的行为。
|
||||
|
||||
输入 adapter 的候选、探针退出条件与回退优先级见父任务 `research/canvas-integration.md`。
|
||||
|
||||
## 本子任务的局部决定
|
||||
|
||||
- 三个 source 严格分离:`editHandles`(手柄)、`editGhost`(客户端即时轮廓与辅助线)、`editPreview`(服务端权威几何)。基线图层始终只读,绝不写入。
|
||||
- 手柄 feature 的 properties 只带 `handleId`,语义一律回 manifest 查。这样客户端没有第二份约束模型。
|
||||
- ghost 只画手柄、辅助线与半透明预估轮廓,不承担权威几何。服务端预览到达后替换 `editPreview`,ghost 随即淡出。
|
||||
- `EditSession` 是纯逻辑对象,不持有 OpenLayers 引用:命令栈、undo/redo、`previewSeq` 仲裁都可在 node 环境下单测。OL 只作为它的输入输出适配层。
|
||||
- `previewSeq` 仲裁放在 `EditSession` 而非请求层:请求层只负责发与取消,丢弃决策要可测。
|
||||
- 探针独立成可删除目录,不进 `MapCanvas`。探针结论只影响"手柄命中与拖拽由谁实现",不影响上面任何一条。
|
||||
- 开关关闭时不注册 interaction、不请求 manifest、不创建三个 source,确保零残留开销。
|
||||
@@ -0,0 +1,6 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "父任务权威合约:handle manifest 字段、坐标分层、预览时序、编辑所有权、客户端边界、directEdit 开关"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/canvas-integration.md", "reason": "ol-ext 探针的退出条件、回退优先级,以及为何不引入第二个 Canvas/视口"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/joint-solver.md", "reason": "EditSession 的交互序列与浏览器 ghost 的职责边界"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "现有单例 Map、source registry 与选择高亮的可复用点"}
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "manifest 字段从服务端到客户端类型的流转,避免局部 cast 与重复状态"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "图层与 source 更新必须复用现有 layers.ts 模式,米制换算不得重写"}
|
||||
@@ -0,0 +1,49 @@
|
||||
# 实施计划
|
||||
|
||||
对应父任务 `implement.md` 的第 1、6 步。每步一个提交,门禁不过就停。
|
||||
|
||||
前置:`direct-edit-solver-api` 第 2 步(handle manifest)已可用。完整验收需其预览与保存端点就绪。
|
||||
|
||||
## 1. `ol-ext` 限时探针
|
||||
|
||||
- 目标:只回答一个问题——能否复用通用 handle 的命中、pointer 生命周期与视觉反馈。
|
||||
- 范围:隔离探针目录,不修改生产基线图层,不进依赖清单直到通过。
|
||||
- 验证:手动跑探针页,确认 OL `Map` 未重建、proxy 拖拽稳定、基线 source 未被写入、事件能转成 draft 值。
|
||||
- 门禁:三条同时成立才把 `ol-ext` 固定为依赖。超过一个工作日或任一条不成立,立刻停止排障。
|
||||
- 回滚点:删除探针目录,改用原生 OL `Snap` + 小型 `PointerInteraction` adapter。数据合约不变,本步失败只换输入层。
|
||||
|
||||
## 2. `EditSession` 纯逻辑
|
||||
|
||||
- 目标:先把可单测的部分做完,与地图解耦。
|
||||
- 范围:命令栈与 undo/redo;`previewSeq` 仲裁;handle 事件到约束值的投影;指针位移到米的换算。不含任何 OpenLayers 引用。
|
||||
- 验证:`npm run test:client:unit` —— 命令栈压入/撤销/重做序列正确;乱序响应(后发先到)被丢弃且不覆盖较新预览;投影在四种 kind 上给出预期约束值;同一像素位移在低纬与高纬得到一致的米值(纬度缩放正确)。
|
||||
- 门禁:四类纯逻辑全部有测试;`EditSession` 不 import `ol`。
|
||||
- 回滚点:纯新增模块,无调用方,revert 无影响。
|
||||
|
||||
## 3. 手柄渲染与开关
|
||||
|
||||
- 目标:选中道路后能看到正确的手柄,且禁入区可见可解释。
|
||||
- 范围:`directEdit` 开关(默认关闭);selection → manifest 读取;`editHandles` source 渲染;reserve 内手柄置灰并带原因提示。只读不可拖。
|
||||
- 验证:`npm run test:client`、`npm run build`;手测——选中道路出现手柄,拖拽前后 `Map` 为同一实例;reserve 内手柄置灰并提示进入 `JunctionTools`;关闭开关后无手柄、无 manifest 请求。
|
||||
- 门禁:`Map` 未被重建;基线 source 未被写入。
|
||||
- 回滚点:开关关闭即止损。
|
||||
|
||||
## 4. 拖拽、ghost 与预览
|
||||
|
||||
- 目标:四类能力真正可拖,预览权威且不闪烁。
|
||||
- 范围:外缘偏移、步行带宽度、车道分隔位置、区间范围控制;`editGhost` 即时反馈;80ms 防抖与 `pointerup` 最终请求;`AbortController` 取消在途请求;`editPreview` 替换;无效草稿诊断与保留最后有效预览。
|
||||
- 验证:`npm run test:client:unit`、`npm run test:client`、`npm run build`;手测——拖动外缘时道路面/步行带/车道线/标线/connector 一致更新且只有受影响 source 被替换;车道分隔可独立调整;区间范围手柄改变影响区间且两端平滑;连续快速拖拽松手后无回跳;违反不变量时停在最后有效预览并显示诊断。
|
||||
- 门禁:只有受影响 source 被替换,无全图层重建;无回跳。
|
||||
- 回滚点:按能力分批提交,可单独回退某一类手柄。
|
||||
|
||||
## 5. 应用、保存与撤销
|
||||
|
||||
- 目标:闭合编辑生命周期。
|
||||
- 范围:应用/保存/取消;undo/redo 接到命令栈;保存走 `expectedDocumentVersion`;409 冲突提示。
|
||||
- 验证:`npm run test:client:unit`(保存冲突分支)、`npm run test:client`、`npm run build`;手测——未保存编辑可撤销/重做;保存后重新编译几何不变且状态 `exact`;两标签页同时保存后者收到 409 提示且前者结果保留。
|
||||
- 门禁:撤销不重写已保存历史;409 有明确用户提示而非静默失败。
|
||||
- 回滚点:revert 后回到第 4 步的预览-only 状态。
|
||||
|
||||
## 步骤依赖
|
||||
|
||||
1 与 2 可并行;3 依赖 1 的结论与 2;4 依赖 3;5 依赖 4。第 1 步失败不阻塞 2-5,只改变第 3、4 步的命中与拖拽实现方式。
|
||||
@@ -0,0 +1,57 @@
|
||||
# 主地图道路区间编辑器
|
||||
|
||||
父任务:`.trellis/tasks/08-26-direct-manipulation-road-editor`。需求来源与权威合约在父任务的 `prd.md` / `design.md`。
|
||||
|
||||
## 目标
|
||||
|
||||
在主地图上实现道路内部区间的直接操纵:外缘、步行带、车道分隔与区间范围可拖,拖拽即时有 ghost,服务端返回权威预览,改动可撤销、可保存。
|
||||
|
||||
## 顺序依赖
|
||||
|
||||
前置:`direct-edit-solver-api` 的第 2 步(handle manifest)交付后即可开工;完整验收需要其预览与保存端点全部就绪。
|
||||
后继:`direct-edit-junction-tools` 复用本任务的 `EditSession`、ghost/preview 图层与命令栈。
|
||||
|
||||
## 范围
|
||||
|
||||
- `ol-ext` Transform 的限时隔离探针,以及不通过时的原生 OpenLayers 回退。
|
||||
- selection → handle manifest 的读取与渲染;handles / ghost / preview 三个独立 source。
|
||||
- 四类可拖能力:外缘偏移、步行带宽度、车道分隔位置、区间范围控制。
|
||||
- `EditSession`:命令栈、undo/redo、应用/保存/取消。
|
||||
- 80ms 防抖预览、`pointerup` 强制最终请求、`previewSeq` 乱序丢弃、`AbortController` 取消在途请求。
|
||||
- 无效草稿的诊断反馈,保留最后一个有效预览。
|
||||
- `directEdit` 开关,默认关闭。
|
||||
|
||||
## 不做
|
||||
|
||||
- 路口 reserve 内的任何编辑(属 `direct-edit-junction-tools`)。
|
||||
- 遗留 `workbench/client/app.js` 的同步或删除。
|
||||
- OSM 中心线与拓扑的编辑。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 选中道路后出现手柄,拖拽前后 OpenLayers `Map` 为同一实例,未被重建。
|
||||
- [ ] 拖动外缘 → 道路面、步行带、车道线、标线、connector 一致更新,只有受影响 source 被替换。
|
||||
- [ ] 车道分隔手柄可独立调整某条分隔线,效果不等价于外缘偏移的副作用。
|
||||
- [ ] 区间范围手柄可修改影响区间,两端平滑过渡回基线。
|
||||
- [ ] 落在 junction reserve 内的手柄不可拖动,并提示进入 `JunctionTools`。
|
||||
- [ ] 连续快速拖拽后松手,最终几何与松手位置一致,无回跳。
|
||||
- [ ] 违反不变量的草稿显示阻塞诊断,地图停在最后一个有效预览。
|
||||
- [ ] 未保存编辑可撤销/重做;保存后重新编译几何不变,约束状态 `exact`。
|
||||
- [ ] 客户端单元测试覆盖:命令栈与 undo/redo、`previewSeq` 乱序丢弃、handle 事件到约束值的投影、米制换算在不同纬度的正确性。
|
||||
- [ ] 关闭 `directEdit` 开关后,工作台行为与当前 main 一致。
|
||||
- [ ] `npm run format:check`、`npm run test`、`npm run test:client`、`npm run test:client:unit`、`npm run build` 全绿。
|
||||
|
||||
## 交付状态(2026-08-28 收口)
|
||||
|
||||
11 条里 9 条通过,2 条**显式接受为未通过**,不勾选:
|
||||
|
||||
| 条目 | 结论 |
|
||||
| --- | --- |
|
||||
| 区间范围手柄可修改影响区间,两端平滑过渡回基线 | **阻塞**。`compileGeometry()` 不读 `profile.interval` / `profile.transitions`,每次编辑都作用于整条路。纯逻辑(`intervalRangeHandles`、`projectIntervalEnd`、`coordinateAtStation`)已交付并单测,UI 由 `intervalEditingSupported = false` 隐藏——拖了没效果的控件比没有控件更糟。详见 `research/interval-not-applied.md`,含实现路径。 |
|
||||
| 落在 junction reserve 内的手柄不可拖动,并提示进入 `JunctionTools` | **实现完成但无法在用户数据上演示**。判据(可编辑带 < 道路宽度)已实现,`fengshu-er-road.osm` 上实测 6 段置灰共 35 个手柄;用户导入的片区 125 个手柄全部可编辑,因为不存在被两个路口填满的短路段。由 `test/direct-edit-solver.js` 的 sandwich fixture 覆盖。 |
|
||||
|
||||
其余 9 条:1、2、3、6、7、10 由用户浏览器手测确认;8 的保存与重放由用户手测加 `test/workbench-edit-api.js` 断言;9 为 97 个客户端单测;11 为门禁。
|
||||
|
||||
本任务实施期间修掉的三个缺陷,均为跨层接线而非逻辑错误,已写入 spec:
|
||||
道路手柄左右放错边、约束未随 operation 发送、`/api/import` 未安装 `compileFresh`。
|
||||
后者早于直接编辑存在,一直让 UI 导入的会话无法重新生成。
|
||||
@@ -0,0 +1,63 @@
|
||||
# 区间范围手柄由客户端合成
|
||||
|
||||
第 4 步的实现决定,与 `design.md`「Handle manifest」小节的字面规则有偏离,故单独记录。
|
||||
决定人:dingkang,2026-08-27。
|
||||
|
||||
## 偏离了什么
|
||||
|
||||
`design.md` 写明:
|
||||
|
||||
> 服务端从同一语义模型生成手柄清单,**客户端不自行推导手柄位置或可拖方向**。
|
||||
|
||||
而区间范围手柄(第 4 类可拖能力)由客户端 `selection.ts` 的 `intervalRangeHandles()` 合成,
|
||||
位置由 `meters.ts` 的 `coordinateAtStation()` 从中心线插值得到。
|
||||
|
||||
## 为什么这样定
|
||||
|
||||
**1. manifest schema 装不下它。**
|
||||
`EditHandle.kind` 的类型是 `RoadConstraintKind`,而 `design.md` 的 kind 表明确声明
|
||||
「这 6 个 kind 与 PRD 首期范围一一对应,没有多余项也没有缺口」。
|
||||
范围手柄不是一种约束——它改的是既有约束 `anchor` 的 `startStation` / `endStation`。
|
||||
要下发就得加第 7 个求解器根本不认的假 kind,或加一个平行数组。为派生数据改合约不划算。
|
||||
|
||||
**2. 它的位置是服务端已发数据的纯函数。**
|
||||
区间来自服务端算好并下发的 `anchor.startStation` / `endStation`,
|
||||
合法窗口来自 `manifest.reserves`,拖拽轴是道路切线。
|
||||
服务端下发等于把自己刚发的东西再算一遍回显,正是 `cross-layer-thinking-guide.md`
|
||||
警告的「derived state 另立第二个游标」。
|
||||
|
||||
**3. 决定性的一条:客户端本来就需要 station → 坐标的插值。**
|
||||
`design.md` 要求 ghost 画「半透明预估轮廓」,即在道路上标出受影响的区间带。
|
||||
画这条带子必须把 station 插值成坐标,**与范围手柄由谁产出无关**。
|
||||
`coordinateAtStation()` 因此是客户端的既有需求;有了它,合成范围手柄几乎免费,
|
||||
服务端改动买不到任何东西。
|
||||
|
||||
**4. `projectIntervalEnd()` 已交付并测试**,签名恰好就是这些输入。
|
||||
|
||||
## 为什么认为符合规则的意图
|
||||
|
||||
那条规则防的是客户端发明**语义**——哪个约束、哪个方向、什么范围合法。
|
||||
范围手柄一样都没发明:
|
||||
|
||||
| 语义 | 来源 |
|
||||
| --- | --- |
|
||||
| 合法窗口 | `manifest.reserves`(服务端) |
|
||||
| 区间当前值 | 约束 `anchor` 的 station(服务端) |
|
||||
| 拖拽轴 | 道路中心线切线(编译模型,服务端) |
|
||||
| 屏幕位置 | 客户端插值 ← **仅此一项是派生的** |
|
||||
|
||||
客户端只做「把服务端已选定的 station 插值成屏幕位置」这一件事。
|
||||
|
||||
## 已接受的代价
|
||||
|
||||
`coordinateAtStation()` 是 `src/compile/direct-edit-solver.js` 里 `coordinateAt()` 的
|
||||
第二份实现。二者都用弧长插值,但分属不同 runtime(CommonJS 服务端 / ESM 浏览器),
|
||||
且客户端那份只服务于 ghost 渲染,不参与任何持久化或求解。
|
||||
`meters.ts` 的注释已标明这层关系,防止后来者误以为可以随意改动其中一份。
|
||||
|
||||
## 若要改回服务端下发
|
||||
|
||||
新增 manifest 字段 `rangeHandles: IntervalRangeHandle[]`(不要塞进 `handles`),
|
||||
由服务端用既有 `coordinateAt()` 算位置。客户端删掉 `intervalRangeHandles()` 即可,
|
||||
`projectIntervalEnd()` 与拖拽链路不受影响。
|
||||
`coordinateAtStation()` 仍需保留,因为 ghost 的区间带还要用。
|
||||
@@ -0,0 +1,60 @@
|
||||
# 几何编译器不施加 `road-interval` 区间
|
||||
|
||||
第 4 步手测时发现:区间范围手柄拖动有 ghost 反馈,但松手后几何毫无变化。
|
||||
经排查这是服务端的能力缺口,不是客户端拖拽逻辑的问题。
|
||||
|
||||
## 事实
|
||||
|
||||
`grep "\.interval\b" src/compile/native-road.js` 返回空。
|
||||
|
||||
求解器写入了区间与过渡,几何阶段一个都不读:
|
||||
|
||||
| profile 字段 | solver 写入 | `compileGeometry()` 读取 |
|
||||
| --- | --- | --- |
|
||||
| `edgeOffsets.left/right` | ✅ | ✅ `centerlineShift`(native-road.js:825) |
|
||||
| `widthMeters` | ✅ | ✅ native-road.js:832 |
|
||||
| `sidewalkWidths.left/right` | ✅ | ✅ 真实宽度,`sidewalkRing`(native-road.js:1666-1671) |
|
||||
| `laneDividerOffsets` | ✅ | ✅ native-road.js:1946 |
|
||||
| **`interval`** | ✅ direct-edit-solver.js:274-277、418 | ❌ **零消费者** |
|
||||
| **`transitions`** | ✅ direct-edit-solver.js:293、303、322 | ❌ **零消费者** |
|
||||
|
||||
## 后果
|
||||
|
||||
1. **每一次直接编辑都作用于整条路。** `anchor.startStation` / `endStation` 被存储、被
|
||||
`validateEditDocument()` 校验、被带进 `resolveDirectEditConstraints()` 的结果,然后被忽略。
|
||||
2. **区间范围手柄在几何上不可能有效果。** 客户端的 `intervalRangeHandles()`、
|
||||
`projectIntervalEnd()`、`coordinateAtStation()` 都正确且有单测,但下游无人接收。
|
||||
3. **`transition: 'smoothstep' | 'linear'` 是死字段。** 父任务 design.md 承诺
|
||||
「作用于 interval 两端回归基线的过渡段」,实际没有任何过渡。
|
||||
|
||||
## 已采取的处置
|
||||
|
||||
`workbench/client/src/edit/flag.ts` 增加 `intervalEditingSupported = false`,
|
||||
范围手柄的 UI 据此隐藏。纯逻辑与测试全部保留。
|
||||
|
||||
理由:一个拖起来有反馈、松手却没效果的控件比没有这个控件更糟——它会持续产生
|
||||
bug 报告,并让人怀疑整个编辑器的其余部分。区间生效的那个提交把这个常量翻成 `true` 即可,
|
||||
客户端不需要其他改动。
|
||||
|
||||
## 对验收标准的影响
|
||||
|
||||
父任务 prd.md 的这条**当前无法通过**:
|
||||
|
||||
> 区间范围手柄可修改影响区间,两端平滑过渡回基线。
|
||||
|
||||
`08-26-direct-edit-map-editor/prd.md` 的同名条目同理。这不是客户端欠工,
|
||||
而是需要 `compileGeometry()` 具备按 station 施加横断面 profile 的能力。
|
||||
|
||||
## 若要实现(未排期)
|
||||
|
||||
用户 2026-08-27 判断影响不大,故未开任务,仅记录。真要做时的形状:
|
||||
|
||||
1. `native-road.js` 的横断面生成需要接受「沿中心线变化的 profile」而非单一常量宽度。
|
||||
目前 `applyDirectEditProfiles()`(约 818-838 行)返回的是整条路一个 `widthMeters`
|
||||
和一次 `offsetLine()` 整体平移,没有沿 station 变化的余地。
|
||||
2. 区间两端按 `transitions[kind]` 做 smoothstep / linear 插值回基线值。
|
||||
3. 车道线、标线、connector、步行带都由横断面派生,必须一并跟随,否则会脱节
|
||||
(父任务 research/joint-solver.md 的依赖链)。
|
||||
4. `test/fixtures` 基线会变化,需要显式 update-baseline 并人工核对。
|
||||
|
||||
这是 `compileGeometry` 的实质改动,属已归档的 `direct-edit-solver-api` 任务范围。
|
||||
@@ -0,0 +1,73 @@
|
||||
# ol-ext Transform 探针结论
|
||||
|
||||
`implement.md` 第 1 步的门禁结果。人工验证由 dingkang 在浏览器完成,2026-08-27。
|
||||
|
||||
## 门禁结果:三条全过
|
||||
|
||||
| 门禁 | 结果 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| OL `Map` 未重建 | 通过 | 实例 #2 / 累计构造 2 次(StrictMode 双挂载),2438 次 React 渲染下未重建 |
|
||||
| 基线 source 未被写入 | 通过 | 60 次拖拽,写入 0 次,几何指纹未变 |
|
||||
| proxy 拖拽稳定且事件能转成 draft 值 | 通过 | 1377 个 `translating` 事件;跟手连续、不跳数;松手不回弹;反复几十次不失手、不报错 |
|
||||
|
||||
投影链路同时被验证:钳位范围内 `raw 3.924 / draft 3.924` 完全相等,说明
|
||||
`signedMetersAlongAxis()` → `projectHandleValue()` 的换算与钳位都正确。
|
||||
|
||||
## 但不把 ol-ext 用于道路手柄
|
||||
|
||||
门禁通过只意味着 ol-ext **可以**用(决策表第一行的「可选依赖」),不意味着它是更好的选择。
|
||||
探针同时暴露了决策表第三行的情况,因此按该行处置。
|
||||
|
||||
### 决定性证据:手柄与约束值脱钩
|
||||
|
||||
```
|
||||
translateend -> raw -24.146 / draft -5.400
|
||||
```
|
||||
|
||||
`Transform` 的 translate 分支按原始 delta 调 `geometry.translate()`,不知道也不关心我们的钳位。
|
||||
于是手柄被拖到 -24.1 米处,而约束只到 -5.4 米——手柄停在道路永远不会变成的位置上。
|
||||
|
||||
生产要求相反:手柄必须贴着钳位边界停下,位置由**约束值反算**而来,而不是跟随光标。
|
||||
这意味着位置更新必须由我们自己拥有。用 ol-ext 就得每帧撤销它刚做的 translate;
|
||||
自己写 `PointerInteraction` 则直接不移动过界。
|
||||
|
||||
### 其余理由
|
||||
|
||||
- 我们实际用到的只有「Point proxy 上的 translate + start/move/end 生命周期 + hitTolerance 命中」。
|
||||
这些 `ol/interaction/Translate` 原生就有,且自带一等 TypeScript 类型。
|
||||
ol-ext 的增量价值是 bounding box 的 scale / stretch / rotate —— 而道路法线偏移、
|
||||
区间范围、路口 cutback 都用不上它,`canvas-integration.md` 早已指出这点。
|
||||
- ol-ext 4.0.38 不带类型,也没有 `@types/ol-ext`。采用它就要长期自己维护一份声明文件。
|
||||
探针期间已经踩到一次:它的自定义事件名不在 OL 的事件类型联合里,
|
||||
`on` / `un` 无法直接声明在类上,只能另设接口做一次转换。
|
||||
- 每个 mousemove 都会走 `handleMoveEvent_ → getFeatureAtPixel_ → forEachFeatureAtPixel`,
|
||||
触发 OL 的 canvas 回读(控制台 `Canvas2D getImageData` 警告即来自此)。
|
||||
这不是崩溃原因,但属于固有开销。
|
||||
|
||||
## 探针期间发现的、与 ol-ext 无关的教训
|
||||
|
||||
浏览器白屏一度被误当成 ol-ext 的稳定性问题,实际是探针自身的 bug:
|
||||
|
||||
```
|
||||
at projectHandleValue (projection.ts:49)
|
||||
at ProbeMap.tsx:133
|
||||
at basicStateReducer → updateReducer → useState
|
||||
```
|
||||
|
||||
几何计算被写在了 `setState` 的 updater 函数里。React 会延迟、且在 StrictMode 下重复调用
|
||||
updater;真正执行时闭包里的 `start` 已被 `translateend` 置为 `null`,于是 `toLonLat(null)` 抛错。
|
||||
`start!` 的非空断言正是把运行时问题藏过类型检查的地方。
|
||||
|
||||
**教训(适用于第 3、4 步)**:绝不在 `setState` updater 内做几何计算或读取实时 OL 状态。
|
||||
先在事件处理器里算出普通值,再传进 updater。
|
||||
|
||||
另一条:逐个 `translating` 事件更新 React state 会产生上千次渲染。探针用
|
||||
`requestAnimationFrame` 合并后才可用。生产要更进一步——ghost 直接写自己的 OL source,
|
||||
绝不为每次指针移动重渲染 React 树。
|
||||
|
||||
## 结论
|
||||
|
||||
按回退方案 1 推进:原生 OL `Snap` + 小型 `PointerInteraction` adapter,位置由约束值反算。
|
||||
|
||||
`HandleManifest → RoadEditOperation → RoadConstraint → preview solver` 数据合约不变,
|
||||
第 2 步已交付的 `EditSession` / `projection` / `meters` 全部保留,探针已验证它们可用。
|
||||
@@ -0,0 +1,70 @@
|
||||
# reserve 内手柄在当前服务端实现下不可达
|
||||
|
||||
第 3 步人工验证时发现:点遍所有道路都看不到灰色(不可拖)手柄。经实测确认这是**正确行为**,不是漏测。
|
||||
|
||||
## 实测数据
|
||||
|
||||
`test/fixtures/fengshu-er-road.osm`,24 条方向道路、32 个 reserve:
|
||||
|
||||
```
|
||||
road handles : 126
|
||||
road NOT editable : 0
|
||||
with disabledReason : 0
|
||||
junction handles : 96 (JunctionTools 所有,主地图已过滤)
|
||||
```
|
||||
|
||||
## 为什么不可达
|
||||
|
||||
`makeRoadHandles()` 里 `editable: false` 的唯一来源是 `unavailable`:
|
||||
|
||||
```js
|
||||
const start = Math.min(1, startReserve);
|
||||
const end = Math.max(0, 1 - endReserve);
|
||||
const unavailable = start >= end;
|
||||
```
|
||||
|
||||
而 `junctionReserves()` 给每端的保留区比例有硬上限:
|
||||
|
||||
```js
|
||||
const fraction = Math.min(0.45, cutback / length);
|
||||
```
|
||||
|
||||
于是 `startReserve ≤ 0.45`、`endReserve ≤ 0.45`,即 `start ≤ 0.45` 且 `end ≥ 0.55`。
|
||||
`start >= end` 结构上永远为假,`unavailable` 分支是死代码。
|
||||
|
||||
实测输出里能直接看到上限生效的样子——两端都撞到 0.45 的短路段:
|
||||
|
||||
```
|
||||
segment:way/858770822/1 window=[0.450,0.550]
|
||||
segment:way/117947564/0 window=[0.450,0.550]
|
||||
segment:way/851989492/2 window=[0.450,0.550]
|
||||
```
|
||||
|
||||
另外手柄的 station 取自 `(start + end) / 2`,即无保留区窗口的正中,
|
||||
所以三类道路手柄的位置也永远不会落进 reserve。
|
||||
|
||||
## 对验收标准的影响
|
||||
|
||||
prd.md 的「落在 junction reserve 内的手柄不可拖动,并提示进入 JunctionTools」
|
||||
对当前三类道路手柄是**空真**:它们永远不落在 reserve 内,所以约束自动成立。
|
||||
|
||||
客户端一侧的置灰与原因提示路径有单测覆盖(`selection.test.ts`:disabled 手柄被保留、
|
||||
`disabledReasonOf` 始终给出原因),只是服务端从不产生这种输入。因此第 3 步不需要改动。
|
||||
|
||||
reserve 边界真正变得用户可见是在第 4 步的**区间范围手柄**:它可以被拖向 reserve 边界,
|
||||
由 `projectIntervalEnd()` 钳到 `window.minStation` / `maxStation`。届时这条边界才有可演示的行为。
|
||||
|
||||
## 留给用户决定的设计问题
|
||||
|
||||
那个死分支暗示原作者期望「短路段应完全归 JunctionTools」。但 0.45 的上限把结果改成了:
|
||||
一条几何上被两个大路口主导的短路,仍然会在正中间得到一条仅占全长 10% 的可编辑带
|
||||
(上面 `[0.450,0.550]` 那几条)。
|
||||
|
||||
两种取向都讲得通,需要产品判断:
|
||||
|
||||
- 保持现状:任何道路都留一条可编辑带,哪怕很窄。
|
||||
- 改为:当 `cutback / length` 两端之和超过某阈值时,整条路标 `editable: false`,
|
||||
引导用户进入 JunctionTools。这会让死分支复活,也让验收标准变成可演示的。
|
||||
|
||||
改动落在 `src/compile/direct-edit-solver.js`(属已归档的 `direct-edit-solver-api` 任务范围),
|
||||
不在本任务范围内,故此处只记录不实施。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "direct-edit-map-editor",
|
||||
"name": "direct-edit-map-editor",
|
||||
"title": "主地图道路区间编辑器",
|
||||
"description": "ol-ext 限时探针与主地图外缘/步行带/车道分隔/区间范围拖拽",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-28",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-26-direct-manipulation-road-editor",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
Reference in New Issue
Block a user