Step 1 of the control-marking task, and deliberately server-only: no handle is drawn yet. This project already shipped a range handle for `profile.interval`, which compileGeometry ignores, so the control dragged and changed nothing. The consumer comes first now. Two kinds join the taxonomy — `junction-crosswalk-inset` and `junction-stop-line-offset`, both on the existing `junction-approach` anchor. The solver writes them onto the approach entry, `applyDirectJunctionPlans` carries them onto the compiled approach, and `compileControlMarkings` reads them in place of the module constants it used for every junction. They move markings without reshaping the junction, so unlike width and cutback they deliberately do not trigger a boundary recompute. `applyJunctionConstraint` becomes an explicit switch. Its trailing `else` had meant every kind that was not approach-width fell through to the cutback validator, so a new kind would have been silently validated and written as a cutback. The same non-exhaustive shape in the test fixture's `valueFor` is fixed the same way, and now throws for an unnamed kind rather than answering with a corner radius. design.md's taxonomy is updated with it — a test asserts the two cannot drift, which is what caught the omission. Measured on a 41-road workspace with 8 crossings: both constraints change their marking geometry, neither drags the other, and out-of-range blocks instead of clamping. That measurement is not in the suite: the synthetic junction resolves `junction_inset_m` to 0 because its crossing never binds to a plan, and the committed OSM fixture has no crossings at all. The tests assert the wiring the handles will depend on — values reaching the approach entry, distinct branches, blocking diagnostics — and the gap is recorded in the test itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.1 KiB
路口控制标线偏移:斑马线与停止线可拖
父任务:.trellis/tasks/08-26-direct-manipulation-road-editor。
这是「更多元素可直接操纵」方向的第一个元素类型。
为什么先做这一类
08-26-direct-edit-map-editor 交付了完整的直接操纵链路——
manifest → 命中 → 拖拽 → 投影 → 钳位 → ghost → 防抖 → 乱序仲裁 → 预览 → 保存 → 撤销。
这条链路不认识具体元素,只认 kind + anchor + axis + value。所以加一类元素
本应是「定义一个 kind + 一个写入器」,而不是造新子系统。
选斑马线/停止线打头,因为可行性已实测确认(2026-08-28):
611 model = modelWithDirectEditProfiles(model, options.directEdit);
613 const junctionPlans = compileJunctionPlans(model, options, diagnostics);
614 applyDirectJunctionPlans(junctionPlans, options.directEdit); // 直接编辑已合并
768 const controls = compileControlMarkings(model, lanes, diagnostics, junctionPlans);
1299 const plan = junctionPlans.get(junctionNodeId); // 已按路口取到 plan
junction 级约束已经能流到控制标线的生成,管线是通的。挡路的只是模块级常量:
const STOP_LINE_OFFSET_METERS = 2.7;
const CROSSWALK_JUNCTION_INSET_METERS = 1.5;
它们全局一个值、不按进口区分。把常量换成「先查进口配置,没有再用默认」即可。
这与区间那件事性质不同:区间要求横断面沿路变化,是几何管线重构;这里只是
在已经拿到手的 plan 上多读一个字段。
强制顺序:编译器优先,UI 最后
08-26-direct-edit-map-editor 交付了一个拖了没有任何效果的区间手柄,因为
compileGeometry() 从不读 profile.interval——UI 先于消费者建成。
见 research/interval-not-applied.md。
本任务反过来:第 1 步不产出任何 UI,只证明编译器真的消费这个参数。 第 1 步的门禁不过就停,不进入第 2、3 步。
范围
1. 服务端打通并证明(无 UI)
- 新增两个约束 kind:
junction-crosswalk-inset、junction-stop-line-offset, 锚点均为junction-approach(求解器已建模该锚点)。 native-road-edits.js:schema 校验与取值范围。direct-edit-solver.js:applyJunctionConstraint()把值写进该进口的 plan 条目。native-road.js:compileControlMarkings()读进口配置,缺省时退回既有常量。
2. 手柄目标泛化
EditHandle 声明「一次手势写到哪里」,客户端在提交处分岔。
验收标准是:加第 N+1 种元素时不需要修改任何已有<E5B7B2><E69C89>柄代码。
3. 手柄与拖拽
选中路口后斑马线、停止线上出现手柄,沿进口方向拖动调整其距路口的距离。 复用既有链路,不新增预览或保存机制。
不做
- 不做红绿灯(属「有位姿的实体资产」,需要不同的写入器与相对锚点)
- 不做箭头(需先定交互语义:拖箭头应改行驶动作,而非移动图标)
- 不做区间生效,方块手柄继续隐藏
- 不改
STOP_LINE_MAX_APPROACH_DISTANCE_METERS等与放置是否成立有关的判据, 只改放置位置
验收标准
第 1 步(门禁,不过则停)
- 带
junction-crosswalk-inset约束编译出的斑马线坐标,与不带时不同 - 带
junction-stop-line-offset约束编译出的停止线坐标,与不带时不同 - 无约束时输出与当前 main 逐字节相同(fixture 基线无 diff)
- 越界取值产生阻塞诊断,不静默钳制
- 约束经保存与重新加载后状态为
exact
第 2、3 步
- 选中路口后斑马线与停止线出现手柄,拖动可改变其距路口距离
- 手柄位置由钳位值反算,拖到极限停在边界
- 新增元素类型未修改既有手柄代码(以 diff 佐证)
npm run format:check、npm run test、npm run test:client、npm run test:client:unit、npm run build全绿
顺序依赖
无前置。后继:红绿灯位姿与箭头语义,二者都依赖第 2 步的目标泛化。