feat: scope the handle manifest and re-aim the ghost

Two consequences of the preview dropping from ~1535 ms to ~142 ms.

The manifest is now scoped to the object being edited. It was 844 KB on a
41-road workspace — 239 handles, 89% of the bytes being `affects` id lists at 46
ids per junction handle — shipped on every preview while the map rendered six.
A segment selection returns 12.2 KB, a 69x reduction, and the cost no longer
multiplies with each handle kind we are about to add. Omitting the selection
keeps the full manifest for the compiler and existing callers, and the tests
assert that scoping is a filter of the full manifest rather than a second
derivation.

The ghost is re-aimed from "estimated geometry" to "what you asked for". Its
guide line existed to mark the origin through a long wait that no longer happens,
so it is gone; what remains is what the preview cannot say — the numeric delta
and whether the drag has hit its clamp. The translucent outline
research/joint-solver.md asked for is deliberately not built: drawing it
accurately means recomputing the road surface in the browser, which the design
forbids, and drawing it crudely would be wrong exactly at transitions, junction
boundaries and clamps. A preview that lies is worse than none.

`degraded` finally has a consumer. EditSession has tracked it since the session
work but nothing read it; a slow solve now dims the ghost in place with a pending
label instead of clearing it and letting the geometry flicker, as design.md
requires.

Selection also fixed a latent hazard: the manifest effect reloads on every
selection change, and it used to call session.load() each time, which would have
discarded unsaved edits the moment the user clicked another road. It now adopts a
document only when the compiled document actually changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-28 12:26:15 +08:00
parent d893e406d6
commit 5292c00926
13 changed files with 305 additions and 41 deletions

View File

@@ -0,0 +1,87 @@
# 直接编辑交互收尾ghost 定位与 manifest 裁剪
父任务:`.trellis/tasks/08-26-direct-manipulation-road-editor`
前置:`08-26-direct-edit-map-editor`(已归档)交付了手柄、拖拽、预览与保存。
## 背景
`perf: stop recomputing control bounds in overlap tests`d893e40把预览从
~1535ms 降到 ~142ms 之后,两件事的前提变了:
**1. ghost 的原始职责被大半抵消。** 它当初是为了在 1.5 秒等待里提供即时反馈。
现在权威几何在约 220ms 内落地80ms 防抖 + 142ms 求解),用户观察到的是
「好像是实时修改」——那正是服务端几何,不是 ghost。
`research/joint-solver.md:48` 要求 ghost 包含「半透明预估轮廓」,该轮廓未实现。
**本任务不补它**,因为它与父任务 design.md 的另一条规则冲突:
> 浏览器不实现第二套道路几何算法
准确的预估轮廓就是在浏览器里重算路面。粗糙的轮廓(把边缘线整体外移 N 米)
在过渡段、路口交界、钳位处都是错的——**会撒谎的预览比没有预览更糟**。
**2. manifest 裁剪的时机到了。** 现在 41 条路的片区产出 221 个手柄,每次预览全量下发,
客户端再过滤成 6 个。后续每加一类元素(停止线、斑马线、红绿灯、箭头)都会成倍放大,
应在手柄种类增长**之前**把过滤挪到服务端。
## 目标
把 ghost 重新定位为「意图与约束的读数」,并让 manifest 只返回选中对象的手柄,
使这套子系统在加入新元素类型前处于稳定、可扩展的状态。
| 谁 | 回答什么问题 |
| --- | --- |
| ghost | 我**要求**的是什么?到极限了吗?求解还在路上吗? |
| preview | 我**会得到**什么? |
## 范围
### A. ghost 精简
去掉回到起点的虚线辅助线。几何已经实时在动,那条线的原始用途(漫长等待中标示起点)
已经消失,现在是视觉噪音。保留 ghost 独有、preview 表达不了的三件事:
- 圆点手柄本身
- 数值增量标签(`+2.35 米`)——看几何看不出改了多少
- 钳位配色——几何单纯停住有歧义:是到极限了还是卡了?
### B. degraded 状态
design.md
> 超预算仍返回结果但标 `degraded: true`,客户端保留 ghost 与待定状态,不做几何闪烁。
`EditSession.degraded` 已实现并有单测,但**没有任何消费者**。求解超预算时 ghost 必须
保持可见并显示待定,而不是清除后让几何跳变。
### C. manifest 按选中裁剪
`GET /api/edit-state``POST /api/edit-preview` 接受可选的选中标识segment id
只返回该对象的手柄。未提供时保持现有全量行为,供其他调用方与既有测试使用。
客户端随之停止本地过滤——`handlesForSegment()` 的过滤职责移到服务端,
但该函数**保留**并继续单测:它仍是「哪些手柄属于主地图」这条规则的唯一定义
road kinds、排除 junction kinds、保留置灰手柄服务端与它必须一致。
## 不做
- 不实现半透明预估轮廓(理由见背景)
- 不做局部编译d893e40 之后没有必要,且是全项目最高风险项)
- 不做 Web Worker 预览(用户 2026-08-28 明确搁置)
- 不实现区间生效,方块手柄继续隐藏
- 不新增元素类型
## 验收标准
- [ ] 拖拽时 ghost 显示圆点 + 数值增量,**不再有**回到起点的虚线
- [ ] 拖到钳位边界时 ghost 变色,与「仍在跟手」明显可分
- [ ] `degraded: true` 的响应不清除 ghost且有可见的待定提示几何不闪烁
- [ ] `/api/edit-state``/api/edit-preview` 接受选中标识并只返回该 segment 的手柄
- [ ] 不带选中标识时返回全量手柄,既有测试不受影响
- [ ] 服务端裁剪结果与客户端 `handlesForSegment()` 对同一输入一致(测试断言)
- [ ] 裁剪前后手柄数与响应体积实测记录在案
- [ ] `npm run format:check``npm run test``npm run test:client``npm run test:client:unit``npm run build` 全绿
## 顺序依赖
无前置。后继:手柄目标泛化与新元素类型,两者都受益于本任务的裁剪。