3.2 KiB
区间范围手柄由客户端合成
第 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 的区间带还要用。