feat: add direct edit handles behind directEdit flag

Steps 1-3 of the main map road interval editor.

EditSession keeps the command stack, undo/redo and previewSeq arbitration as
pure logic with no OpenLayers reference, so all of it is unit-tested in node.
Pointer displacement converts to meters through EPSG:4326 and spherical
distance: treating a 3857 delta as meters desyncs the geometry from the cursor
by 1/cos(latitude). Handle drags project onto the axis the manifest declares
and clamp to its range, so the client never writes a coordinate into a road
polygon.

All of it sits behind a directEdit flag that defaults to off. With the flag off
the workbench requests no manifest, creates no extra source and registers no
interaction, so behaviour matches main.

The ol-ext probe passed its three gates but is not adopted for road handles.
Transform translates by the raw pointer delta, so a handle detaches from its
clamped constraint value: a drag reading -24.1 m produced a draft of -5.4 m.
Production needs the handle position derived from the constraint instead, which
means owning the position update, so native OL PointerInteraction will carry
the drag. ol-ext stays out of package.json; the probe is kept as a manual
harness. Reserve handles are unreachable with the current solver, recorded in
research/ rather than worked around.

Also names the dead backend when an API response is empty, instead of
surfacing "Unexpected end of JSON input" from response.json().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 14:59:11 +08:00
parent e25c564bb9
commit bc4b9a9717
25 changed files with 2463 additions and 22 deletions

View File

@@ -0,0 +1,68 @@
# ol-ext Transform 限时探针
对应 `.trellis/tasks/08-26-direct-edit-map-editor/implement.md` 第 1 步。
这个探针只回答一个问题:**能否复用 ol-ext 通用 handle 的命中、pointer 生命周期与视觉反馈。**
它不回答“道路语义怎么建模”——那部分由 `src/edit/``EditSession` / `projection` 负责,且已经独立单测通过。
## 怎么跑
```bash
npm install ol-ext --no-save # 门禁通过前不进依赖清单
npm run dev
```
然后打开 <http://localhost:5173/probe/ol-ext-transform/>。
不需要导入 OSM也不需要后端页面用 `fixture.ts` 里的合成道路。
## 你要做的事
拖动地图上那个**蓝色圆点手柄**,来回拖几次,快慢都试,然后看右侧面板。
## 三条门禁(必须同时成立)
| # | 门禁 | 面板怎么读 |
|---|---|---|
| 1 | OL `Map` 未被重建 | “实例 #N,累计构造 N 次”两个数字必须相等。再点几次“强制 React 重渲染”,数字仍要相等。 |
| 2 | 基线 source 未被写入 | “写入 0 次,几何未改变”。拖拽过程中这行**任何变化都算失败**。 |
| 3 | proxy 拖拽稳定且事件能转成 draft 值 | 看**「原始位移」**那行跟手连续变化、松手不跳;手柄不粘滞、不丢命中。 |
门禁 3 要看的是「原始位移」,不是「约束值」。约束值带生产钳位 `±5.4 米`
而 5.4 米在 zoom 20 只有约 47 像素,稍微拖远就会顶到边界显示成常量——那是正常的,不代表跟踪有问题。
「原始位移」不钳位,所以它才反映跟手质量。
两个数都是用生产代码算的(`signedMetersAlongAxis()` / `projectHandleValue()`
所以门禁 3 同时验证了“事件 → 约束值”这条链路。
另外面板会显示收到了多少个 `translating` 事件。这个数字和 React 渲染次数的比例是个有用的信号:
探针已经把逐帧读数用 `requestAnimationFrame` 合并了,生产实现还要更进一步——
ghost 直接写自己的 OL source绝不为每次指针移动重渲染 React 树。
注意:**卸载/重新挂载地图会构造新的 Map这是预期行为**,不算门禁 1 失败。门禁 1 针对的是拖拽与重渲染。
## 请回报给我
1. 三条门禁分别通过/未通过。
2. 未通过的,面板上的具体数字或现象。
3. 主观手感:拖拽是否顺滑,手柄命中是否可靠。
## 已知的、探针之外的结论
这些我已经查过,不用你验证,但会计入最终取舍:
- ol-ext 4.0.38BSD-3-Clause`peerDependencies: ol >= 5.3.0`,无运行时依赖。
- **不带 TypeScript 类型,也没有 `@types/ol-ext`。** 本目录的 `ol-ext.d.ts` 是我为探针手写的最小声明;
真要把 ol-ext 提为正式依赖,就要长期自己维护一份声明文件。
- `Transform``translate` 分支直接对传入 feature 调 `geometry.translate()`
所以它只能绑一次性 proxy绝不能绑编译产物图层——这也是门禁 2 存在的原因。
- 它的手柄模型是 bounding box 的 scale/rotate/stretch跟道路法线偏移、区间范围、路口 cutback 不是一回事。
即使门禁全过,它最多承担“通用手柄的命中与拖拽生命周期”,语义投影仍然是我们自己的 `projection.ts`
## 如果门禁没过
`design.md` 的回退优先级,删掉本目录,改用回退方案 1
原生 OL `Snap` + 小型 `PointerInteraction` adapter。
`HandleManifest → RoadEditOperation → RoadConstraint → preview solver` 的数据合约不变,
所以这一步失败只换输入层,第 2 步已交付的 `EditSession` / `projection` / `meters` 全部保留。