feat: make crosswalk and stop-line offsets solvable
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>
This commit is contained in:
@@ -37,6 +37,8 @@ type RoadConstraintKind =
|
||||
| 'junction-approach-width'
|
||||
| 'junction-cutback'
|
||||
| 'junction-corner-radius'
|
||||
| 'junction-crosswalk-inset'
|
||||
| 'junction-stop-line-offset'
|
||||
```
|
||||
|
||||
| kind | 锚点 | value | 单位与含义 | 所有者 |
|
||||
@@ -47,8 +49,18 @@ type RoadConstraintKind =
|
||||
| `junction-approach-width` | `junction-approach` | `{ widthMeters }` | 米 | JunctionTools |
|
||||
| `junction-cutback` | `junction-approach` | `{ cutbackMeters }` | 米 | JunctionTools |
|
||||
| `junction-corner-radius` | `junction-corner` | `{ radiusMeters }` | 米 | JunctionTools |
|
||||
| `junction-crosswalk-inset` | `junction-approach` | `{ insetMeters }` | 米,斑马线距路口边界的退让 | 主地图 / JunctionTools |
|
||||
| `junction-stop-line-offset` | `junction-approach` | `{ offsetMeters }` | 米,停止线沿进口后退的距离 | 主地图 / JunctionTools |
|
||||
|
||||
这 6 个 kind 与 PRD 首期范围一一对应(外缘、步行带、车道分隔 + 进口、cutback、角部),没有多余项也没有缺口。
|
||||
前 6 个 kind 对应 PRD 首期范围(外缘、步行带、车道分隔 + 进口、cutback、角部)。
|
||||
|
||||
后 2 个于 2026-08-28 追加,是「更多元素可直接操纵」方向的第一类扩展:控制标线的**位置**
|
||||
成为可编辑参数。它们不改变路口形状,只移动标线,因此不触发路口边界重算。
|
||||
放置是否成立的判据(`STOP_LINE_MAX_APPROACH_DISTANCE_METERS` 等)不在此范围内。
|
||||
|
||||
新增 kind 的前提:**几何阶段必须已经消费它**。本项目曾为 `profile.interval` 交付过
|
||||
一个拖动无效果的手柄(见 `08-26-direct-edit-map-editor/research/interval-not-applied.md`),
|
||||
所以扩展 kind 时先证明 `compileGeometry()` 读取该参数,再画任何手柄。
|
||||
|
||||
`transition: 'smoothstep' | 'linear'`,默认 `smoothstep`,作用于 interval 两端回归基线的过渡段。
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
"08-26-direct-edit-solver-api",
|
||||
"08-26-direct-edit-map-editor",
|
||||
"08-26-direct-edit-junction-tools",
|
||||
"08-27-junction-dominated-roads"
|
||||
"08-27-junction-dominated-roads",
|
||||
"08-28-edit-interaction-polish",
|
||||
"08-28-junction-control-offsets"
|
||||
],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
91
.trellis/tasks/08-28-junction-control-offsets/prd.md
Normal file
91
.trellis/tasks/08-28-junction-control-offsets/prd.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 路口控制标线偏移:斑马线与停止线可拖
|
||||
|
||||
父任务:`.trellis/tasks/08-26-direct-manipulation-road-editor`。
|
||||
这是「更多元素可直接操纵」方向的第一个元素类型。
|
||||
|
||||
## 为什么先做这一类
|
||||
|
||||
`08-26-direct-edit-map-editor` 交付了完整的直接操纵链路——
|
||||
manifest → 命中 → 拖拽 → 投影 → 钳位 → ghost → 防抖 → 乱序仲裁 → 预览 → 保存 → 撤销。
|
||||
这条链路**不认识具体元素**,只认 `kind + anchor + axis + value`。所以加一类元素
|
||||
本应是「定义一个 kind + 一个写入器」,而不是造新子系统。
|
||||
|
||||
选斑马线/停止线打头,因为可行性已实测确认(2026-08-28):
|
||||
|
||||
```js
|
||||
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 级约束**已经能流到控制标线的生成**,管线是通的。挡路的只是模块级常量:
|
||||
|
||||
```js
|
||||
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 步的目标泛化。
|
||||
26
.trellis/tasks/08-28-junction-control-offsets/task.json
Normal file
26
.trellis/tasks/08-28-junction-control-offsets/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "junction-control-offsets",
|
||||
"name": "junction-control-offsets",
|
||||
"title": "路口控制标线偏移:斑马线与停止线可拖",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-28",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-26-direct-manipulation-road-editor",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
Reference in New Issue
Block a user