chore(road-editor): plan direct-edit task tree and add client test infra

Planning: parent design.md becomes the single authoritative contract
(constraint model with 6 kinds, handle manifest, coordinate/unit
layering, preview sequencing, storage layout and lazy migration, area
config snapshot, API contract). Work is split into four independently
verifiable child tasks with per-step gates and rollback points.

Test infra: pin vitest 4.1.11, add test:client:unit for client pure
logic, extend prettier globs to root *.ts so vitest.config.ts is checked.

Add .gitignore: the repo had none, so inputs/, outputs/, workbench-data/
and the client build output were untracked rather than ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 17:33:42 +08:00
parent 7dc7ede2b8
commit 93f09e399e
41 changed files with 2001 additions and 4 deletions

View File

@@ -0,0 +1,108 @@
# Canvas 与控制柄工具方案
## OpenLayers 能力确认
当前项目使用 `ol@10.10.0`。OpenLayers `VectorLayer` 默认由 Canvas renderer 绘制,当前工作台正是这种模式。类型声明可确认:
- `ol/interaction/Modify` 支持对 source/feature collection 的顶点修改,带 `modifystart` / `modifyend`、自定义 vertex style、pixel tolerance 与 hit detection。
- `ol/interaction/Snap` 支持顶点、边、交点吸附,且会改写交互事件的 coordinate/pixel供其他 pointer interaction 使用。
- `ol/interaction/Translate` 支持 feature 集合的拖动和 start/move/end 事件。
因此OL 具备 Canvas 渲染、命中测试、投影换算、视口同步与低层 pointer 交互所需的基础能力。它没有白板编辑器的命令历史、语义控制柄、约束求解、工具状态或多对象选择模型。
## 不使用原生 `Modify` 直接编辑道路面
`Modify` 会直接改变传入 feature 的 geometry 坐标,也允许插入/删除顶点。这适合通用 GIS 几何编辑,但不适合本项目:道路面和路口面是编译产物,顶点没有稳定的编辑语义,直接修改会使车道、标线和 connector 脱节。
它可被借鉴的只是事件生命周期、Canvas 命中与样式机制;道路源图层必须保持只读。
## 建议架构
```text
OpenLayers Map / VectorLayer (Canvas)
- 基线与预览 GeoJSON 图层:只读、由编译器/preview solver 提供
- editHandles VectorLayerPoint/LineString featureCanvas 画柄与影响区间
- EditPointerInteraction命中 handle -> 投影拖拽方向 -> 更新 draft command
- Snap只对批准的语义锚点/网格 source 生效
|
v
React EditSessionselection、draft、undo/redo
|
v
constraint preview solver -> 仅更新受影响 source
```
控制柄是独立的、可丢弃的 UI feature它的 properties 只携带 `handleId`,并通过 manifest 反查 semantic anchor。任何鼠标移动都先被投影为结构化约束值再重算预览绝不把 handle 的地理坐标直接写为道路 polygon 坐标。
## 引入独立 Canvas 白板库的判断
将 tldraw、Fabric、Konva 等作为覆盖层或替换 OL均会引入第二个 viewport、平移/缩放手势、坐标系与 hit-test 系统。与地理坐标、OL hit detection、原有图层开关和地图导航同步的成本很高且不能自动提供道路领域的约束模型。
除非“Quickdraw”是一个能在既有地图 Canvas 内以 OpenLayers coordinate/pointer API 运行的明确库,否则首选是继续把 OL 作为唯一 Canvas 和视口,使用它成熟的 interaction primitives加一个小而领域化的 `EditPointerInteraction`。这不是手写画布;渲染、坐标、事件和吸附都由 OL 提供,新增代码只负责道路语义。
## 成熟的 OpenLayers 扩展ol-ext
`ol-ext` 是当前最匹配的成熟扩展候选npm 最新版为 `4.0.38`2026-02-23BSD-3-Clause仓库仍持续维护。其 `interaction/Transform` 明确提供:
- 单独的 Canvas overlay layer 与可定制的 transform handles
-`Select` 交互同步选择;
- feature translate / scale / stretch / rotate
- `translatestart``translating``translateend``scalestart``scaling``scaleend` 等生命周期事件;
- 通过 `PointerInteraction` 实现,因此共享 OpenLayers 的地图坐标、事件分发与视口。
这能承担通用控制柄的视觉和 pointer 生命周期,消除自行实现 hover、命中、capture、拖拽状态机和控制柄绘制的大部分工作。
### 不能委托给 ol-ext 的部分
`Transform` 的最终行为仍是缩放、旋转或平移传入 feature 几何。其控制柄是矩形 bounding box而道路编辑需要沿道路法线的 edge offset、沿中心线的范围控制和路口特定的 cutback/corner radius这些语义并非该扩展的能力。
推荐将 ol-ext 只绑定到**短生命周期 proxy feature**
```text
语义 handle manifest -> proxy feature / ol-ext Transform
-> transform event -> 约束值法向偏移、station interval、cutback、radius
-> preview solver -> 新预览几何与新的 proxy
```
真实编译产物图层不可传给 `Transform`。对于不能表达为标准 translate/scale 的角部圆角和区间范围控制,仍需一个很小的领域 adapter但它只做“事件到约束”的投影不维护自己的 Canvas 或通用手势系统。
## 建议的验证闸门
在正式实现前完成一个隔离技术验证:将 `ol-ext` 的 Transform 绑定到单道路的临时 proxy确认它能在当前 `ol@10.10.0`、React map 生命周期下稳定工作,并验证一次拖拽只更新 proxy/preview source、不会改写基线 source、不会重建地图。通过后再将它固定为依赖不通过则保留 OpenLayers 原生 interaction 方案,避免在主分支承诺未经验证的扩展。
## 有限探针与回退策略
`ol-ext` 不是架构前提。探针只允许解决一个问题:复用通用 handle 的命中、pointer lifecycle 和视觉反馈。它有明确的时间上限和退出条件:
| 结果 | 动作 |
| --- | --- |
| 可在临时 proxy 上平稳拖拽,且不写基线 source、不重建 Map | 作为标准 transform 代理的可选依赖 |
| 与 `ol@10.10.0` 或 React 生命周期不兼容 | 删除探针,不迁移现有 MapCanvas走原生 OL 方案 |
| 能运行但矩形 scale/rotate 模型妨碍道路法线/路口约束 | 不把它用于道路手柄;仅保留可复用部分或删除 |
| 探针超过预设时间仍无法达到以上条件 | 停止排障,按回退方案推进 |
回退方案按优先顺序:
1. **OpenLayers 原生 proxy + 小型 `PointerInteraction` adapter**Point/LineString proxy 仍由 OL Canvas 绘制和命中,使用 `Snap` 处理吸附adapter 只处理已命中的少量语义 handle 到 constraint 的投影。它不重写渲染、相机、图层或通用选择,工作量受限。
2. **DOM handle overlay + 成熟手势库**:仅在当前选中道路/路口上放置少数固定像素的 React/HTML controls用成熟拖拽手势库处理 pointer capture每个地图 postrender 将它们用 `map.getPixelFromCoordinate()` 对齐。地图仍是唯一视口,未命中 handle 的事件继续交给 OL。适合复杂的范围、数值标签和路口控制但不是 Canvas 风格。
3. **不采用的方向**:迁移到 MapLibre/Leaflet 仅为获取绘制插件,或把 Quickdraw 叠到 OL 上。这会更换地图/视口基础设施,成本远高于有限 adapter且不能解决道路语义。
无论哪个输入 adapter 运行,`HandleManifest -> RoadEditOperation -> RoadConstraint -> preview solver` 的数据合同保持不变。因此探针失败最多替换输入层,不会推翻 schema、求解器或已保存编辑。
## Drawtonomy 的关系
公开 checkout 未包含 `Canvas.tsx`,也未在 package manifests 或 lockfile 中暴露 `tldraw`、Konva、Fabric、Excalidraw 或名为 Quickdraw 的依赖。因此不能从该源码确认其编辑器具体使用了哪个成熟 Canvas 库,只能借鉴其公开的对象图原则。
## Quickdraw 核查结论
检查 `/tmp/quickdraw` 后,确认它是 MIT 许可、零运行时依赖的完整无限白板 SDK而不是地图编辑器控制柄库
- `packages/core/src/editor.js` 在传入 container 内创建自己的 scene canvas 和 overlay canvas并实现独立 camera`x/y/z`、平移、缩放、pinch、pointer capture、hit test、selection 和 resize/rotate/arrow handles。
- `packages/core/src/store.js` 使用不可变 record、transaction、diff、batch undo/redo同一手势的连续更新会合并为一个 history entry。
- 内建形状和工具是封闭集合(笔、箭头、几何形状、文字等),公开资料中没有可把 OpenLayers feature 注册为原生 shape/handle 或让它共享外部 map camera 的扩展接口。
### 结论
不将 Quickdraw 作为覆盖层或替代 OpenLayers二者都会处理 pointer、wheel、pinch、屏幕到世界坐标转换和 camera接入后要持续同步两套视口且会破坏当前 OL 地图导航和图层命中。也不将 Quickdraw 的 flat drawing document 作为道路约束的存储格式。
可直接借鉴的成熟实现原则是固定像素尺寸的控制柄、overlay 与场景分离、pointer capture、一个 gesture 对应一个 transaction、不可变 diff 的 undo/redo、以及局部渲染。项目自身的 `EditSession` 应采用相同语义,但以 `RoadEditOperation` / `RoadConstraint` 为数据而非 Quickdraw shape。

View File

@@ -0,0 +1,30 @@
# 当前系统能力地图
## 编译链路
`src/compile/compiler.js:16` 读取 OSM 和 `native-road-overrides.json`,调用 `compileRoadModel()``compileGeometry()`,写入 staging 目录后原子替换产物目录。生成 GeoJSON 因此是可丢弃、可重建的派生层。
`src/compile/native-road.js:79` 将 OSM way 按共享节点拆分为方向道路;每条道路有稳定的 `road:way/<way>[:segment/<n>]:<direction>` ID、`segmentId`、OSM 节点端点和中心线。道路面/步行带/车道线/中心线/路口/连接线均由这个模型计算。
## 可直接复用
| 现有能力 | 直接操纵中的职责 |
| --- | --- |
| 道路、端点、车道与 segment ID | 约束的语义锚点和 OSM 重导入后的匹配基础 |
| `native-road-overrides/v1` 校验与 stale diagnostics | 新 schema 的版本化校验、失效检测和编辑列表 |
| `applyRoadOverrides()` | 把可反推的拖拽收敛为既有 `widthMeters`、步行带等参数 |
| `compileGeometry()` | 约束生效后的权威重算边界 |
| feature `native_id``road_id``segment_id``osm_node_id` | 渲染 feature 到语义编辑目标的回链 |
| OpenLayers 单实例 + source registry | 手柄、预览和选中态可作为独立 source/layer 增量更新,避免重建容器 |
| 暂存/保存/重新编译 API | 命令历史与持久化操作的基础链路 |
## 当前缺口
- 没有描述“一个渲染边界/顶点对应哪个语义控制点”的 handle manifest。
- 现有 road override 只能整体宽度/车道数,不表达沿道路位置变化、边缘偏移、局部过渡或路口角部约束。
- 没有操作日志、撤销/重做、预览求解器或冲突 rebase 状态。
- 路口模板在 area 配置中,不在 overrides 文件;直接操纵要明确哪些路口细节可落到 area 模板、哪些可成为用户覆盖项。
## 约束
不要将道路面 polygon、标线 polygon 或任意 OpenLayers `Feature` 坐标作为主要持久化编辑内容。它们没有足够的拓扑语义,且在 OSM 拆段、路口重算或编译器升级后很难可靠重放。

View File

@@ -0,0 +1,81 @@
# 可编辑数据模型的比较
> 本文是方案比较的探索记录。约束模型、kind 枚举、锚点类型与文档 schema 的权威定义在 `design.md`;如有冲突以 `design.md` 为准。
## 共同不变量
- OSM 原文与其解析出的道路模型是基线,不由拖拽直接覆盖。
- 所有持久化修改都必须含版本、稳定锚点、创建时基线指纹、参数、状态和解释信息。
- 每个输出 feature 都必须能给出生成它的输入语义 ID 与适用约束,供 UI 选择和诊断使用。
- 拖拽过程可使用临时求解结果;只有显式保存才写入可重放命令。
## 方案比较
| 方案 | 持久化内容 | 优点 | 根本限制 |
| --- | --- | --- | --- |
| 参数反推 | `widthMeters`、lane count、步行带宽度等 | 可最大复用 v1重编译最稳 | 不能表达沿程局部收放、独立边缘或路口角部 |
| 语义几何约束 | 对道路横断面、边缘、过渡、路口的约束 | 兼顾直接操纵与 OSM 重放;适合本项目 | 需要 solver、优先级和冲突诊断 |
| 局部 patch 几何 | 渲染 polygon 的顶点/边偏移 | 表达最自由 | 极难在 OSM 或生成算法变化后重定位,拓扑易破坏 |
## 建议方向:版本化的约束文档
不是将所有编辑统一存为一个巨型 mesh而是将它们存为带锚点的声明式约束并保留“由哪个交互生成”的命令记录。
```ts
interface RoadEditDocument {
schema: 'native-road-edits/v2'
base: {
osmSha256: string
compilerGeometryVersion: string
createdAt: string
}
constraints: RoadConstraint[]
operations: RoadEditOperation[]
}
interface RoadConstraint {
id: string
kind: RoadConstraintKind // 6 个取值见 design.md「约束模型」
anchor: SemanticAnchor
anchorSnapshot: AnchorSnapshot
value: unknown
enabled: boolean
status: ConstraintStatus // exact | recheck | pending | conflicted | stale
provenance: { operationId: string; createdAt: string; author?: string }
}
type SemanticAnchor =
| { type: 'road-station'; roadId: string; station: number; side?: 'left' | 'right' }
| { type: 'road-interval'; roadId: string; startStation: number; endStation: number; side?: 'left' | 'right' }
| { type: 'junction-approach'; nodeId: string; segmentId: string; side?: 'left' | 'right' }
| { type: 'junction-corner'; nodeId: string; incomingRoadId: string; outgoingRoadId: string }
```
`station` 是相对道路中心线归一化弧长 `0..1`,而不是绝对经纬度或数组下标。它对顶点加减和轻微 OSM 几何调整更稳定;保存时还应记录 `anchorSnapshot`(当时位置、切线、道路长度、相邻 OSM node ID作为重定位/冲突检测证据。
道路横断面拖拽默认写入 `road-interval` 约束:拖拽位置为 interval 中心,系统根据道路长度与邻近路口预留距离推导初始 `startStation` / `endStation`,并在两端使用明确的 `transition: 'smoothstep'` 回归基线。用户调整范围手柄时只更新该 interval不存储鼠标轨迹。
## 分层求解规则
1. **基线层**OSM → 当前道路模型和默认横断面。
2. **现有参数层**v1 road/connection/style overrides可由简单拖拽写入继续兼容。
3. **几何约束层**:按语义锚点计算横断面、边界和路口局部形状;冲突时按明确优先级或提示用户处理。
4. **派生层**:道路面、步行带、车道中心线、标线、停止线和 connector 必须一起重新计算,不能只移动视觉面。
## 操作、撤销和重导入
- UI 维护本地命令栈pointer down 生成草稿pointer move 更新临时 constraintpointer up 合并为一个 `operation`undo/redo 仅移动指针,不写文件。
- 保存后写入完整约束文档和不可变 `operations` 记录;已保存编辑的撤销创建反向操作或禁用约束,不修改历史。
- 重导入时先按 `roadId`/`nodeId` 精确匹配;失败时使用 `anchorSnapshot` 的 OSM node、距离和切线作候选匹配。匹配不唯一或偏差超阈值即标记 `conflicted`,不自动应用。
- v1 覆盖项保留并逐步迁移:全路宽度仍是 road override只有无法表达的局部调整进入 v2 constraints。
## 需要在产品边界确认后细化
- 路口角部是否由独立约束处理,还是将路口升级为可编辑的模板/参数对象。
- 是否需要多人协作若需要operation log 必须具备 actor、revision 和并发合并策略。
## 已确认边界
OSM 中心线与拓扑保持只读。直接操纵不会新增 `centerline-control-point`、分段、合并或连接/断开拓扑操作;这类修改通过修正 OSM 后重新导入完成。v2 约束因此只作用在由中心线派生的横断面、边缘、步行带与路口细节。
首期范围包含道路外缘、步行带、车道分隔以及路口进口、cutback 与转弯角部。路口必须是独立的 `junction-*` anchor不应伪装成某一条道路末端的 edge offset它会同时影响道路截断、路口面、connector、停止线、斑马线和标线。

View File

@@ -0,0 +1,26 @@
# Drawtonomy可验证的设计证据
本笔记仅基于本机 `/Users/que01/Project/drawtonomy` 的公开源码与文档。该 checkout 不包含编辑器 Canvas、指针事件、选择或历史实现不能据此断言它具体如何处理拖拽状态机。
## 公开事实
- `README.md` 将其描述为 topology-aware lane、snap 与 point sharing连接会随编辑保持。
- `packages/drawtonomy-sdk/src/types.ts` 定义 `point``linestring``lane`lane 用 `leftBoundaryId` / `rightBoundaryId` 引用边界,边界以 `pointIds` 引用点,`next`/`prev` 表示车道关系。
- `packages/drawtonomy-sdk/src/exporter/osmToShapes.ts` 从 Lanelet2 导入时复用相同的点和 linestring对相邻道路保留共享边界与方向反转信息。
- `packages/drawtonomy-sdk/src/exporter/laneCenterline.ts` 从左右边界按归一化弧长采样,导出中心线和宽度;中心线是派生数据。
- `docs/exporter.md` 指出可重新编辑的 SVG 内嵌完整 snapshot导出器只从对象图读取数据。
## 可迁移原则
1. **拥有关系而非复制关系**:共享点/边界只保存一次,依赖对象通过 ID 引用。因此一个动作自然波及所有关联几何。
2. **编辑源与展示/导出分离**:编辑对象图是源,中心线、边界渲染、导出格式是派生物。
3. **显式拓扑**:车道连接不是靠距离猜测,而是用 `next` / `prev` 表示。
4. **可重新打开的完整状态**:持久化内容足以重新构建编辑场景,而不是一张结果图。
## 不应直接照搬
Drawtonomy 是从空白画布或 Lanelet2 形状开始编辑的创作工具;本项目以 OSM 为权威输入、编译器推导道路横断面和路口。若完全改为点/边界对象图,会失去现有 OSM 重导入、规则推导和可追溯性,且制造第二个不一致的地图源。
## 对本项目的结论
借鉴其“语义对象图 + 依赖重算 + 共享锚点”的原则,但将持久化内容定义为附着在 OSM 派生语义 ID 上的约束,而不是迁移到 Drawtonomy 的自由形状 snapshot。

View File

@@ -0,0 +1,56 @@
# 道路与路口的联合求解边界
## 当前依赖已经存在
`compileGeometry()` 目前以同一 `model``junctionPlans` 依次生成道路面、车道中心线、边缘线、控制设施、中心线/车道标线、步行带、connector 与路口面。它们不是孤立图层:
```text
道路横断面 / 进口尺寸
-> 道路面、步行带、车道偏移
-> 车道线与 connector
-> 路口边界、cutback、转弯圆角
-> 停止线、斑马线、箭头与诊断
```
普通路口目前由 `compileJunctionPlans()` 的 approach、`cutbackMeters` 和 boundary 驱动;复合路口则由 `junctionTemplates.clusters``complex-junction.js` 的 core radius、进口包络和角部岛生成。直接操纵不能在最终 `roadSurface` / `intersectionSurface` 上独立移动顶点,否则会破坏这条依赖链。
## 提议的新增阶段
```text
OSM -> compileRoadModel -> 现有 v1 参数覆盖
-> resolveDirectEditConstraints
-> editableRoadModel + editableJunctionPlans
-> 既有图层编译器(逐步接收扩展参数) -> GeoJSON
```
`resolveDirectEditConstraints` 的职责是:
1.`road-station` / `road-interval` 约束整理为道路横断面 profile
2. 将道路末端 profile 与 `junction-approach` 约束合并,构建一致的进口截面;
3.`junction-corner` / `junction-cutback` 约束生成可验证的 junction plan
4. 在违反最小车道宽、相邻道路相交、交叉口连接线包络等不变量时,返回明确冲突而非偷偷修复;
5. 输出一个 handle manifest使地图手柄能从同一语义模型读取位置、可拖动方向、受影响对象和可见的值。
道路 interval 的默认范围由求解器决定,而不是 UI 预设像素:避开两端的 junction cutback优先取拖拽站点两侧可用长度的有限比例若道路过短或与另一个约束重叠预览返回可调整范围或冲突信息。
## 交互预览
拖动不触发文件写入或完整工作台重建。地图维护 `EditSession`
```text
pointer down: 读取 handle 的 semantic anchor创建 draft command
pointer move: 投影鼠标位置 -> 立即更新 ghost 与 draft constraint -> 防抖服务端预览求解
pointer up: 验证成功则压入本地命令栈;失败保留提示并回退到上一个有效预览
save: 批量持久化 constraints + operations
compile: 用已保存文档运行权威全量编译
```
浏览器 ghost 仅包含控制柄、辅助线与半透明预估轮廓,不承担权威道路几何。服务端复用正式编译的约束求解逻辑,返回受影响的少数 OpenLayers source道路面、步行带、路口、标线、connector 和 handles客户端替换它们而非卸载 `MapCanvas` 或重新创建 `Map`。这与现有图层可见性和选择修复一致。
## 约束不变量
- 单车道最小宽度,例如 2.4m;道路横断面总宽度等于车道、边缘和步行带之和。
- 一个站点的左右外缘不得交叉;相邻 profile 之间必须有可计算的过渡。
- 进口截面必须在路口 cutback 处与路口边界连续。
- connector、停止线和人行横道必须保持在所属道路/路口可用面内,否则产生阻塞性诊断。
- 普通路口与复杂 cluster 不共用相同低层约束:前者锚定 node后者锚定 cluster 和 arm避免将 cluster 缩减为多个互相冲突的普通路口编辑。

View File

@@ -0,0 +1,71 @@
# JunctionTools 专用路口编辑工作区
## 结论
路口应从主地图的上下文控制柄升级为专用编辑工作区。主地图负责发现、选择和进入;`JunctionTools` 负责高密度的局部几何、拓扑和诊断操作。这不改变“OSM 中心线/拓扑只读”和“约束驱动派生输出”的原则。
## 入口与范围
从路口面或诊断进入,路由参数使用语义引用:
```ts
type JunctionRef =
| { type: 'node'; id: string }
| { type: 'cluster'; id: string }
```
工作区加载活动 revision 中该 junction 的 context路口面、外部进口、相邻道路的短上下文、车道/connector、步行带、停止线、斑马线、诊断和当前 constraints。保留返回主地图的入口与位置避免用户失去全局方位。
## 统一 UI、不同求解器
普通 node 和复合 cluster 仍共享“进口、角部、控制设施、诊断”的用户心智模型;但 `JunctionTools` 根据 `JunctionRef` 提供不同内部投影:
- node按 OSM node 的 incoming/outgoing approaches 和 corners 编辑。
- cluster按对外 arms 编辑;内部短连接不作为用户可拖的普通路口边缘,避免约束彼此冲突。
这比强迫主地图显示一套通用控制柄更可靠,也比暴露多套产品工具更易理解。
## 快速拓扑拟合与更新
此处“拓扑更新”指重新求解**生成道路的可行拓扑**,不修改 OSM 图拓扑进口横断面、车道分配、connector/movement、路口边界、步行带、停止线、斑马线和标线必须一起重算。
```text
JunctionTools 草稿操作
-> 客户端即时 ghost
-> 防抖服务端局部 junction solve
-> 返回 fit 后的 junction plan、派生 GeoJSON、拓扑诊断
-> 当前编辑器局部刷新;返回主地图后复用同一 preview
```
局部解必须校验最小车道宽、连接线包含性、surface 自交、控制设施可放置性与 cluster arm 连续性。无法拟合时返回最后有效几何与结构化诊断;不能把不合法的图形显示成已应用。
## 与 revision 的关系
`JunctionTools` 的草稿属于活动工作副本的 `EditSession`,不是独立导出文件。用户可以取消并丢弃草稿,或将有效操作合并到主工作区;之后仍通过普通保存和命名检查点进入 revision 历史。
## 预览与提交状态
“预览”“应用”“保存”是三个不同状态,不能把保存当作看见效果的前提:
```text
首次拖拽 -> session draft + 即时 ghost
防抖服务端返回 -> JunctionTools 显示权威局部几何/诊断预览
应用到工作区 -> 将有效 draft 合并为主工作区的未保存约束;主地图继续显示预览
保存 -> 将活动副本写入持久化文档
保存检查点 -> 冻结为不可变 revision
取消 -> 丢弃未应用的 JunctionTools draft恢复进入前的工作区状态
```
预览必须包含所有受影响的派生对象道路面、路口面、步行带、车道中心线、connector/movement、停止线、斑马线与标线以及拟合失败时的诊断和最后一个有效结果。
## 会话边界
一次 `JunctionTools` 会话严格聚焦一个 `JunctionRef`。进入相邻路口前,用户必须应用或取消当前草稿;系统不允许两个路口草稿在同一个局部 solver session 内并存。已应用但未保存的编辑属于主工作区,可在主地图的普通 undo/redo 中管理。
## 与道路编辑的所有权
主地图 road editor 只拥有两个 junction reserve 之间的内部 road interval。路口两端的 reserve 在地图上可见但不可由道路区间 handle 覆盖;`JunctionTools` 独占 reserve 内的 approach width、transition、cutback 和 corner 约束。对同一进口,显式 `junction-approach` constraint 优先于道路 profilesolver 在边界自动求连续过渡。
## 当前阶段的延后边界
`JunctionTools` 会成为核心竞争力,但当前任务只设计其对象边界、会话、预览和约束合同。高级复合路口模板、多个路口联动编辑、控制设施的单对象创作、协作与全面拓扑创作均延后,避免这些问题遮蔽直接编辑基础链路的验证。

View File

@@ -0,0 +1,64 @@
# 可复现存盘与 OSM 重导入
## 问题
仅让最新的 `native-road-overrides.json` 跟随当前导入 OSM会使用户无法安全比较“原始 OSM + 精修”和“新的 OSM + 重放精修”。输出 GeoJSON 又不足以解释或再次编辑场景。
## 建议:不可变 revision活动工作副本
工作区有一个可编辑的活动副本;每个 revision 都是可重新编译、可审计的冻结快照。OSM 重导入创建新的候选 revision 或工作分支,永不原地覆盖已保存 revision。
```text
工作区
active/ 当前暂存编辑
revisions/
rev-0001/ 不可变的“导入基线”
rev-0002/ 命名检查点
rev-0003/ 基于新 OSM 的候选版本
```
每个 revision 至少保存:
```ts
interface RoadRevisionManifest {
schema: 'road-workbench-revision/v1'
id: string
createdAt: string
label?: string
parentRevisionId?: string
source: {
osmFile: string
osmSha256: string
areaConfigFile: string
areaConfigSha256: string
}
documents: {
nativeRoadOverrides: string
directEdits: string
trafficSignals: string
}
compiler: {
packageVersion: string
gitCommit?: string
geometrySchema: string
}
outputs?: { manifest: string; compiledSha256: string }
}
```
OSM、area config 和各 JSON 文档均在 revision 目录中复制manifest 保存 digest。派生输出可作为缓存/审阅证据保存,但“复现来源”始终是输入文档和 compiler identity恢复时应可重新编译验证 digest并提示编译器版本差异。
## 对冲突体验的作用
- 在当前 revision 内修改 OSM 并重导入,不会破坏旧 revision 的可用性。
- 用户可以在新 revision 上尝试约束重放并获得精确/可疑/失效状态,同时随时返回旧版本查看原效果。
- 确认 rebase 后才将新 revision 设为活动版本;失败也不会丢失已精修场景。
- 保存操作记录同样随 revision 冻结,便于审计;活动副本的未保存 undo/redo 不进入不可变 revision。
## 已确定的创建策略
- 导入 OSM 自动创建不可变基线 revision。
- 用户明确执行“保存检查点”时创建带名称的不可变 revision。
- 普通保存、草稿预览和重新编译只更新活动副本,不自动制造 revision。
这一策略以可审阅的检查点承载长期历史,避免把高频拖拽/保存变成难以浏览的版本噪声。