Files
road-compiler/.trellis/tasks/archive/2026-08/08-26-direct-edit-map-editor/research/client-synthesised-range-handles.md

3.2 KiB
Raw Blame History

区间范围手柄由客户端合成

第 4 步的实现决定,与 design.md「Handle manifest」小节的字面规则有偏离故单独记录。 决定人dingkang2026-08-27。

偏离了什么

design.md 写明:

服务端从同一语义模型生成手柄清单,客户端不自行推导手柄位置或可拖方向

而区间范围手柄(第 4 类可拖能力)由客户端 selection.tsintervalRangeHandles() 合成, 位置由 meters.tscoordinateAtStation() 从中心线插值得到。

为什么这样定

1. manifest schema 装不下它。 EditHandle.kind 的类型是 RoadConstraintKind,而 design.md 的 kind 表明确声明 「这 6 个 kind 与 PRD 首期范围一一对应,没有多余项也没有缺口」。 范围手柄不是一种约束——它改的是既有约束 anchorstartStation / 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.jscoordinateAt() 的 第二份实现。二者都用弧长插值,但分属不同 runtimeCommonJS 服务端 / ESM 浏览器), 且客户端那份只服务于 ghost 渲染,不参与任何持久化或求解。 meters.ts 的注释已标明这层关系,防止后来者误以为可以随意改动其中一份。

若要改回服务端下发

新增 manifest 字段 rangeHandles: IntervalRangeHandle[](不要塞进 handles 由服务端用既有 coordinateAt() 算位置。客户端删掉 intervalRangeHandles() 即可, projectIntervalEnd() 与拖拽链路不受影响。 coordinateAtStation() 仍需保留,因为 ghost 的区间带还要用。