Three things, kept in one commit because they touch overlapping hunks of the same two files and this environment has no interactive hunk staging. Splitting them by file would have drawn boundaries that misrepresent what changed. 1. Step 4 of the map editor. A native OpenLayers PointerInteraction turns a drag into a clamped constraint value, the ghost source shows it immediately, and the solver's answer replaces a parallel set of preview layers while the baseline layers are hidden rather than overwritten. Preview requests debounce at 80 ms, pointerup flushes without waiting, and a newer request aborts the one in flight; EditSession decides which answers count. Handle positions come from the clamped value, so a handle stops at its limit instead of following the cursor. Three of the four drag capabilities are live: edge offset, sidewalk width, lane divider. 2. Road edge handles were drawn on the wrong side. offsetLine() offsets counter-clockwise from the direction of travel and sidewalks use `heading + (side === 'left' ? -90 : 90)`, so left is `tangent - 90`; makeRoadHandles() placed the left handle at `tangent + 90`, over the right kerb. Dragging the visually-left handle moved the right edge. Fixed on both sides of the wire, with regression tests that name the sides geographically rather than by axis sign. 3. Roads the junctions geometrically fill are now read-only. The 0.45 cap per reserve made the existing `unavailable` branch unreachable, so a 14.5 m stub between two junctions was offered a 1.5 m editable band with no room for the transitions a road-interval constraint needs. Greying only affects the manifest: constraints already saved against such a road keep being solved, so the geometry output is unchanged and the fixture baselines do not move. Range handles are built and unit-tested but hidden behind `intervalEditingSupported`: compileGeometry() reads neither profile.interval nor profile.transitions, so every edit applies to the whole road and the control would have had no effect. Recorded in research/interval-not-applied.md, which also blocks one PRD acceptance criterion. The ol-ext probe stays in the tree as a manual harness; ol-ext is still not a dependency. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
4.5 KiB
Markdown
88 lines
4.5 KiB
Markdown
# 路口主导的短路段整条置灰
|
||
|
||
父任务:`.trellis/tasks/08-26-direct-manipulation-road-editor`。约束模型与编辑所有权的权威定义在父任务 `design.md`。
|
||
|
||
## 问题
|
||
|
||
`src/compile/direct-edit-solver.js` 里 `editable: false` 的唯一来源是 `unavailable`:
|
||
|
||
```js
|
||
const start = Math.min(1, startReserve);
|
||
const end = Math.max(0, 1 - endReserve);
|
||
const unavailable = start >= end;
|
||
```
|
||
|
||
但 `junctionReserves()` 给每端保留区的比例有硬上限 `Math.min(0.45, cutback / length)`,
|
||
于是 `start ≤ 0.45`、`end ≥ 0.55`,`start >= end` 结构上永远为假。
|
||
**该分支是死代码,连带那句提示语「该道路全部位于路口保留区,请进入 JunctionTools 编辑。」永远不可能显示。**
|
||
|
||
实测 `test/fixtures/fengshu-er-road.osm`:126 个道路手柄,0 个 `editable: false`。
|
||
|
||
后果是短路段拿到了一条无意义的编辑带(`test/fixtures/fengshu-er-road.osm`,20 个有保留区的段):
|
||
|
||
| 路长(米) | 总宽(米) | 窗口 | 可编辑带(米) |
|
||
| --- | --- | --- | --- |
|
||
| 14.5 | 9.8 | [0.45,0.55] | 1.5 |
|
||
| 15.4 | 9.8 | [0.45,0.55] | 1.5 |
|
||
| 18.6 | 9.8 | [0.45,0.55] | 1.9 |
|
||
| 19.1 | 9.8 | [0.45,0.55] | 1.9 |
|
||
| 19.2 | 9.8 | [0.45,0.55] | 1.9 |
|
||
| 19.6 | 6.5 | [0.45,0.55] | 2.0 |
|
||
| 82.2 | 9.8 | [0.17,0.83] | 54.9 |
|
||
| 199.2 | 9.8 | [0.00,0.93] | 185.6 |
|
||
|
||
第一行那条路只有 14.5 米长、9.8 米宽,cutback 是 `max(宽度) × 1.4 ≈ 13.7` 米——
|
||
**每一端**的路口都要这条路的 94%,两端合计 188%,远超全长。上限把它压成各 45%,
|
||
凭空造出中间 10%(1.5 米)的「可编辑带」。
|
||
|
||
这违反两条已写明的约定:
|
||
|
||
- 父任务不变量「相邻 profile 之间必须有可计算的过渡」。约束默认 `transition: 'smoothstep'`
|
||
作用于区间两端回归基线,1.5 米的区间两侧紧贴保留区,没有任何余量做过渡。
|
||
- 父任务「编辑所有权」:主地图只编辑两个 junction reserve **之间**的道路内部 interval。
|
||
这种路的真实路口几何占满全长,根本不存在「之间」。
|
||
|
||
## 目标
|
||
|
||
让路口几何占满的短路段整条归 JunctionTools,恢复那个分支的可达性,
|
||
并使父任务 prd 的验收标准「落在 junction reserve 内的手柄不可拖动,并提示进入 JunctionTools」
|
||
从空真变成可演示。
|
||
|
||
## 判据
|
||
|
||
**可编辑带长度(米)< 道路总宽度(米)→ 该 segment 的所有道路手柄 `editable: false`。**
|
||
|
||
用物理长度而非归一化 station,理由是过渡段需要的是实际距离,与道路长短无关。
|
||
一条比自身宽度还短的编辑带装不下横断面的平滑变化。
|
||
|
||
在上表数据上分界干净,无边界模糊样本:1.5 < 9.8 置灰;54.9 > 9.8 保留。
|
||
恰好切出可编辑带 < 10 米的那 6 段。
|
||
|
||
判断必须在上限生效**之后**用实际窗口算,而不是拿未截断的 `cutback / length` 之和——
|
||
后者与「过渡段放不下」这件事没有直接关系。
|
||
|
||
## 验收标准
|
||
|
||
- [ ] 可编辑带短于道路宽度的 segment,其全部 `road-*` 手柄 `editable: false` 且带 `disabledReason`。
|
||
- [ ] `disabledReason` 复用既有文案,引导进入 JunctionTools。
|
||
- [ ] 可编辑带不短于道路宽度的 segment,行为与当前 main 完全一致(手柄数量、位置、`editable: true`)。
|
||
- [ ] 置灰不改变 `reserves` 本身,也不改变任何几何输出:`compileGeometry` 结果与置灰前逐字节一致。
|
||
- [ ] 置灰的 segment 仍然出现在 manifest 中(不是被删除),否则客户端无法解释为什么不能编辑。
|
||
- [ ] junction 手柄不受影响,仍然 `editable: true`。
|
||
- [ ] `test/direct-edit-solver.js` 覆盖:短路段全部置灰、正常路段不受影响、判据边界(带长 ≈ 宽度)。
|
||
- [ ] `npm run test`、`npm run test:client`、`npm run test:client:unit`、`npm run format:check`、`npm run build` 全绿。
|
||
- [ ] 客户端无需改动即可显示灰色手柄与原因(`08-26-direct-edit-map-editor` 已实现该路径)。
|
||
|
||
## 不做
|
||
|
||
- 不改 `junctionReserves()` 的 0.45 上限本身。上限保护的是 reserve 语义,改它会牵动 junction 侧几何。
|
||
- 不改 cutback 的推导公式。
|
||
- 不实现 JunctionTools 侧的编辑能力(属 `08-26-direct-edit-junction-tools`)。
|
||
- 不做客户端改动。
|
||
- 不把置灰的 segment 从 manifest 里移除。
|
||
|
||
## 顺序依赖
|
||
|
||
与 `08-26-direct-edit-map-editor` 的第 4、5 步无依赖,可并行或后置。
|
||
客户端置灰渲染路径已在该任务第 3 步交付并有单测覆盖,本任务只补上服务端的输入。
|