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:
@@ -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` 任务范围),
|
||||
不在本任务范围内,故此处只记录不实施。
|
||||
Reference in New Issue
Block a user