Compare commits
43 Commits
v0.1.1
...
d893e406d6
| Author | SHA1 | Date | |
|---|---|---|---|
| d893e406d6 | |||
| e08af11b7d | |||
| 4ef7b478b2 | |||
| ef55556f19 | |||
| 62795a97b9 | |||
| d94c546450 | |||
| 38593f6f67 | |||
| 92297270f1 | |||
| bc4b9a9717 | |||
| e25c564bb9 | |||
| 2d85211558 | |||
| a26d4be666 | |||
| f3f763f94c | |||
| 41c314fc61 | |||
| 735ce69d26 | |||
| 94e319c717 | |||
| 0eb404d8f7 | |||
| 67e67e66bf | |||
| ab63d23896 | |||
| 3297d0c97a | |||
| c5e762e1a9 | |||
| 98e8a770a3 | |||
| ba8eaba9d9 | |||
| c0c8a16dd4 | |||
| 35002371ae | |||
| 93f09e399e | |||
| 7dc7ede2b8 | |||
| 02c3a4d3e4 | |||
| 89e0a9ce37 | |||
| faaa33717f | |||
| f7601d554e | |||
| 3c5f39ed73 | |||
| 2f2bd6cbf0 | |||
| 4133a5244a | |||
| 89b402de7a | |||
| 3ddb33e321 | |||
| f15e69c868 | |||
| 181e575e1e | |||
| 81e02c670d | |||
| 3b4befb025 | |||
| 54d77ef09d | |||
| 82afafa294 | |||
| 90400d967f |
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
node_modules/
|
||||
|
||||
# 本机独有的编译输入(体积大、按区域各异,不入库)
|
||||
inputs/
|
||||
|
||||
# 编译与测试产物
|
||||
outputs/
|
||||
workbench/client/dist/
|
||||
|
||||
# 工作台运行期数据:导入的 OSM、overrides、信号与派生产物
|
||||
workbench-data/
|
||||
|
||||
.DS_Store
|
||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
workbench/client/dist/
|
||||
.trellis/
|
||||
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
149
.trellis/spec/backend/direct-edit-api.md
Normal file
149
.trellis/spec/backend/direct-edit-api.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Direct Edit Solver And API
|
||||
|
||||
## 1. Scope / Trigger
|
||||
|
||||
This contract covers the pure direct-edit resolver and the workbench endpoints
|
||||
that preview, persist, checkpoint, and rebase `native-road-edits/v2` documents.
|
||||
It is required because the feature crosses compiler, storage, and HTTP layers.
|
||||
|
||||
## 2. Signatures
|
||||
|
||||
```js
|
||||
resolveDirectEditConstraints(model, editDocument, context)
|
||||
// -> { roadProfiles, junctionPlans, constraintStates, handles, diagnostics }
|
||||
|
||||
GET /api/edit-state
|
||||
POST /api/edit-preview
|
||||
POST /api/edits
|
||||
POST /api/revisions
|
||||
POST /api/revisions/:id/rebase
|
||||
```
|
||||
|
||||
## 3. Contracts
|
||||
|
||||
- The resolver is deterministic and must not import `fs`, `path`, `http`, or
|
||||
write files. It accepts a baseline model, an optional validated v2 document,
|
||||
and `{ revisionId, compilerGeometryVersion, previewSeq }` context.
|
||||
- `GET /api/edit-state` returns the active document, `documentVersion`, active
|
||||
revision, constraint states, diagnostics, and `road-edit-handles/v1` manifest.
|
||||
- `POST /api/edit-preview` accepts `document` (or replacement `constraints`
|
||||
and `operations`) and optional integer `previewSeq`. It returns the same
|
||||
manifest/state data plus compiled layers, echoes `previewSeq`, and never
|
||||
changes active files. Work over the 300ms budget sets `degraded: true`.
|
||||
- `POST /api/edits` requires integer `expectedDocumentVersion` and either a
|
||||
complete `document` or replacement arrays. Saves through a staging file and
|
||||
rename, increments `documentVersion`, and returns the active revision.
|
||||
- A version mismatch returns HTTP 409 with `{ ok: false, error, current }` and
|
||||
leaves the active document byte-identical.
|
||||
- `POST /api/revisions` requires a non-empty `label`; it creates an immutable
|
||||
checkpoint and returns its manifest plus current edit state.
|
||||
- `POST /api/revisions/:id/rebase` is read-only and returns status counts plus
|
||||
per-constraint details. `pending`, `conflicted`, and `stale` constraints do
|
||||
not enter geometry solving.
|
||||
|
||||
## 4. Validation & Error Matrix
|
||||
|
||||
| Condition | Required behavior |
|
||||
| --- | --- |
|
||||
| Missing or invalid v2 document fields | Reject with a field-specific 400 error |
|
||||
| Missing `expectedDocumentVersion` | Reject with 400; do not write |
|
||||
| Expected version differs from active version | Return 409 and current version; do not write |
|
||||
| Empty checkpoint label | Reject with 400; no revision is created |
|
||||
| Missing active workspace for preview | Reject with 400; no files are created |
|
||||
| Solver replay anchor cannot resolve | Mark `stale` or `conflicted` with diagnostic; never silently apply |
|
||||
| Compiler geometry version differs | Mark all replayable constraints `recheck` without changing values |
|
||||
| Invariant violation (lane width, crossing boundaries, self-intersection, connector bounds) | Return a blocking diagnostic; do not clamp or silently repair |
|
||||
|
||||
## 5. Good / Base / Bad Cases
|
||||
|
||||
- Good: preview the same document later saved with the same active baseline;
|
||||
compiled layers and solver diagnostics agree, while tracked file bytes and
|
||||
mtimes remain unchanged.
|
||||
- Base: an empty v2 document yields byte-for-byte baseline geometry and an
|
||||
empty constraint-state list.
|
||||
- Bad: two tabs save version `N`; the second save with `N` receives 409 and
|
||||
cannot overwrite the first tab's document.
|
||||
|
||||
## 6. Tests Required
|
||||
|
||||
- Unit: resolver purity, empty-document identity, five replay statuses, handle
|
||||
manifest fields, and blocking invariant diagnostics.
|
||||
- Integration: preview file bytes/mtimes unchanged, `previewSeq` echoed,
|
||||
preview layers matching formal compilation, and `degraded` represented as a
|
||||
boolean.
|
||||
- Persistence: atomic save, version-409 no-write path, checkpoint parent and
|
||||
digest integrity, and rebase counts/details.
|
||||
|
||||
## 7. Wrong vs Correct
|
||||
|
||||
### Wrong
|
||||
|
||||
```js
|
||||
fs.writeFileSync(activeEdits, JSON.stringify(draft));
|
||||
```
|
||||
|
||||
This permits half-written documents and allows a stale browser tab to overwrite
|
||||
newer edits.
|
||||
|
||||
### Correct
|
||||
|
||||
```js
|
||||
if (expectedDocumentVersion !== current.documentVersion) throw conflict409();
|
||||
saveEditDocument(activeEdits, { ...draft, documentVersion: current.documentVersion });
|
||||
```
|
||||
|
||||
The service checks the version before the atomic version-bumping write, while
|
||||
the pure resolver remains reusable by preview and formal compilation.
|
||||
|
||||
## 8. Left and right
|
||||
|
||||
`left` is `heading - 90`. Three places agree and must keep agreeing:
|
||||
|
||||
- `offsetLine()` (`native-road.js`) offsets a positive value counter-clockwise
|
||||
from the direction of travel, i.e. toward geographic left.
|
||||
- `centerlineShift = (edgeOffsets.left - edgeOffsets.right) / 2`, so widening the
|
||||
left edge moves the centerline left.
|
||||
- Sidewalks use `heading + (side === 'left' ? -90 : 90)`.
|
||||
|
||||
The handle manifest reports `axisAzimuth = tangent + 90` for both sides, which is
|
||||
therefore *right*. Handle placement must offset the left handle by `tangent - 90`.
|
||||
|
||||
**Regression**: `makeRoadHandles()` once placed the left handle at `tangent + 90`,
|
||||
drawing it over the right kerb, so dragging the visually-left handle moved the
|
||||
right edge. Assert sides by geography (a north-heading road's left handle is west
|
||||
of its centerline), never by axis sign — a sign convention can be wrong on both
|
||||
sides of the wire at once and still look self-consistent.
|
||||
|
||||
## 9. What the geometry stage actually reads
|
||||
|
||||
`compileGeometry()` honours only four profile fields:
|
||||
|
||||
| Field | Read at |
|
||||
| --- | --- |
|
||||
| `edgeOffsets.left/right` | `native-road.js` centerline shift |
|
||||
| `widthMeters` | road width |
|
||||
| `sidewalkWidths.left/right` | `sidewalkRing()` |
|
||||
| `laneDividerOffsets` | lane separator placement |
|
||||
|
||||
`profile.interval` and `profile.transitions` are written by the solver and read by
|
||||
nothing: every direct edit currently applies to the whole road. Do not add UI for
|
||||
interval-scoped editing until that changes — see
|
||||
`research/interval-not-applied.md` in the map-editor task.
|
||||
|
||||
## 10. Saving is not applying
|
||||
|
||||
`POST /api/edits` writes the v2 document only. `/api/state` serves the outputs of
|
||||
the last compile, so a save without a recompile leaves a refresh showing pre-edit
|
||||
geometry — indistinguishable from a failed save.
|
||||
|
||||
`compileInput()` is the only caller that passes `editsFile`, and
|
||||
`session.context.compileFresh` must be installed by **every** path that sets
|
||||
`session.area`, including `/api/import`. It was originally wired only when the
|
||||
server started with an area on the command line, so every UI-imported session
|
||||
answered `请先导入 OSM 文件` to `/api/compile`.
|
||||
|
||||
An identity test cannot catch a `editsFile` regression: an ignored document and an
|
||||
empty one produce identical output. Assert that a **non-empty** document changes
|
||||
the compiled output. Wiring gaps between two handlers need an HTTP-level test, and
|
||||
a regression test is only trustworthy once you have watched it fail without the
|
||||
fix.
|
||||
6
.trellis/spec/backend/index.md
Normal file
6
.trellis/spec/backend/index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Backend Development Guidelines
|
||||
|
||||
## Revision Storage
|
||||
|
||||
- [Road Revision Storage](./road-revision-storage.md) -- immutable workbench revision layout, validation, and recovery contract.
|
||||
- [Direct Edit Solver And API](./direct-edit-api.md) -- pure constraint solving, preview/save endpoints, version protection, and rebase behavior.
|
||||
93
.trellis/spec/backend/road-revision-storage.md
Normal file
93
.trellis/spec/backend/road-revision-storage.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Road Revision Storage
|
||||
|
||||
## 1. Scope / Trigger
|
||||
|
||||
`src/compile/road-revisions.js` owns the persistent v2 workbench layout. It is called after a successful import and when an existing workbench area is read, so legacy `import-*` directories migrate lazily without an administrative command.
|
||||
|
||||
## 2. Signatures
|
||||
|
||||
```js
|
||||
ensureRevisionStore(workspace) // -> { paths, active, baseline }
|
||||
setActiveAreaConfig(workspace, options) // -> { paths, areaConfig }
|
||||
readAreaConfigSnapshot(file) // -> compiler options
|
||||
createCheckpoint(workspace, label) // -> restored revision
|
||||
readRevision(workspace, revisionId) // -> { manifest, osm, areaConfig, nativeRoadOverrides, directEdits, trafficSignals }
|
||||
```
|
||||
|
||||
## 3. Contracts
|
||||
|
||||
- Legacy inputs remain at `source.osm`, `native-road-overrides.json`, and `native-traffic-signals.json`; migration must never rewrite them.
|
||||
- OSM bytes are stored once under `osm/<sha256>.osm`.
|
||||
- `active/native-road-edits.json` starts as a validated v2 document at version `0`; `active/state.json` points to the active revision.
|
||||
- The active config is `{ schema: 'road-workbench-area-config/v1', options }` at `active/area-config.snapshot.json`. Updating it must also update `base.areaConfigSha256` through a versioned v2 document write.
|
||||
- A revision is immutable at `revisions/rev-NNNN/`. Its `manifest.json` uses `road-workbench-revision/v1`, references the content-addressed OSM and frozen `area-config.snapshot.json`, lists frozen JSON documents, and records SHA-256 digests.
|
||||
- `compileInput()` uses `areaConfigSnapshotFile` when present. Its external `options` are ignored in that mode; a missing or invalid snapshot is an error, never a fallback.
|
||||
|
||||
## 4. Validation And Error Matrix
|
||||
|
||||
| Condition | Error / behavior |
|
||||
| --- | --- |
|
||||
| Required legacy input missing | `Revision source is missing: <path>` |
|
||||
| Snapshot file is missing | `Area config snapshot is missing: <path>` |
|
||||
| Snapshot schema or options are invalid | `Invalid area config snapshot: <path>` |
|
||||
| Content-addressed OSM differs from its filename digest | `Content-addressed OSM is corrupt` |
|
||||
| Revision manifest schema or ID mismatch | `Invalid revision manifest` |
|
||||
| Revision ID is not `rev-NNNN` | Reject with `Invalid revision id` |
|
||||
| Manifest path leaves its OSM or revision directory | Reject the read; never resolve an external path |
|
||||
| Referenced frozen OSM or JSON digest mismatch | Reject the read with a digest mismatch error |
|
||||
| Empty checkpoint label | Reject with `label must be a non-empty string` |
|
||||
|
||||
## 5. Good / Base / Bad Cases
|
||||
|
||||
- Good: first open of a legacy directory adds `osm/`, `active/`, and `revisions/rev-0001/`, leaving old files byte-identical.
|
||||
- Base: opening an already migrated directory is idempotent; an interrupted migration with a baseline but no active state repairs only the missing state file.
|
||||
- Bad: mutating a frozen revision document, config snapshot, or referenced OSM must make `readRevision()` fail rather than returning altered input.
|
||||
|
||||
## 6. Tests Required
|
||||
|
||||
`test/road-revisions.js` must cover:
|
||||
|
||||
- legacy-byte preservation and baseline creation;
|
||||
- equal OSM bytes yielding one content-addressed copy;
|
||||
- named checkpoint creation, parent linkage, and complete restore;
|
||||
- manifest digest equality with the actual frozen files, including area config;
|
||||
- active config writes incrementing `documentVersion` and preserving old revision snapshots;
|
||||
- compiler snapshot precedence and an explicit missing-snapshot error;
|
||||
- invalid checkpoint labels.
|
||||
|
||||
`test/fixtures.js` compares normalized current compiler outputs with the checked-in `native-road-package/v1.1` fixture baselines. Regenerate them only through `npm run test:fixtures:update-baseline` after an intentional compiler output change.
|
||||
|
||||
## 7. Wrong Vs Correct
|
||||
|
||||
### Wrong
|
||||
|
||||
```js
|
||||
fs.copyFileSync(sourceOsm, path.join(revisionDirectory, 'source.osm'));
|
||||
```
|
||||
|
||||
This duplicates equal OSM files and provides no integrity check.
|
||||
|
||||
### Correct
|
||||
|
||||
```js
|
||||
const { file, digest } = storeOsm(paths);
|
||||
manifest.source = { osmFile: path.relative(paths.workspace, file), osmSha256: digest };
|
||||
```
|
||||
|
||||
The content address is both the deduplication key and the integrity contract.
|
||||
|
||||
### Wrong
|
||||
|
||||
```js
|
||||
compileInput({ ...input, options: externalOptions });
|
||||
```
|
||||
|
||||
This allows later external config changes to alter a frozen revision.
|
||||
|
||||
### Correct
|
||||
|
||||
```js
|
||||
compileInput({ ...input, areaConfigSnapshotFile: revisionSnapshot });
|
||||
```
|
||||
|
||||
The compiler validates and consumes the frozen snapshot exclusively.
|
||||
5
.trellis/spec/frontend/component-guidelines.md
Normal file
5
.trellis/spec/frontend/component-guidelines.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Component Guidelines
|
||||
|
||||
Use typed React function components. `App.tsx` owns session/editor composition, while reusable map and UI concerns live in named components. Prefer the shared `ui/button.tsx` primitive for commands. Do not create business UI with `querySelector` or `innerHTML`.
|
||||
|
||||
Forms use controlled inputs and submit handlers. Keep map canvas DOM owned by OpenLayers and surrounding controls owned by React.
|
||||
164
.trellis/spec/frontend/direct-edit-client.md
Normal file
164
.trellis/spec/frontend/direct-edit-client.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Direct Edit Client
|
||||
|
||||
> Trigger: any change under `workbench/client/src/edit/`, `MapCanvas.tsx`, or the
|
||||
> direct-edit wiring in `App.tsx`. Read this before editing handles, ghosts,
|
||||
> previews, or the save path.
|
||||
|
||||
Related: [Direct Edit Solver And API](../backend/direct-edit-api.md) for the wire
|
||||
contract this client consumes.
|
||||
|
||||
---
|
||||
|
||||
## 1. Layer ownership
|
||||
|
||||
Four layer groups, and only three of them are ever written by the client:
|
||||
|
||||
| Group | Written by | Rule |
|
||||
| --- | --- | --- |
|
||||
| baseline (`createLayers()`) | `updateLayers()` only | **Never** written by edit code |
|
||||
| `editHandles` | `EditHandleLayer.render()` | Handle features carry only `handleId` |
|
||||
| `editGhost` | `EditGhostLayer` | Client-side, non-authoritative |
|
||||
| `editPreview` | `EditPreviewLayer.show()` | Server geometry; **hides** baseline, never overwrites it |
|
||||
|
||||
`EditPreviewLayer` builds its own layer set from the same `createLayers()` factory
|
||||
so preview and baseline cannot drift in styling, and `show()` hides only the
|
||||
baseline layers it actually supersedes. `clear()` restores visibility from the
|
||||
layer switches, never from a hardcoded default.
|
||||
|
||||
**Forbidden**: writing compiler output into a baseline source. It is the invariant
|
||||
the whole split exists to protect, and nothing in the type system enforces it.
|
||||
|
||||
---
|
||||
|
||||
## 2. A handle carries nothing but its id
|
||||
|
||||
Handle features hold `handleId` and nothing else. Kind, anchor, axis, range and
|
||||
disabled reason are looked up in the manifest. That is why `EditHandleLayer` owns
|
||||
both the source and the index — they cannot fall out of step.
|
||||
|
||||
**Forbidden**: copying manifest fields onto the feature. It creates a second
|
||||
constraint model on the client, which is the failure the cross-layer guide warns
|
||||
about.
|
||||
|
||||
---
|
||||
|
||||
## 3. Handle position comes from the value, never the cursor
|
||||
|
||||
`handlePositionFor(handle, value)` is the inverse of `projectHandleValue()`. A
|
||||
drag projects onto the manifest's axis, clamps to the manifest's range, and the
|
||||
handle is then placed from the clamped value.
|
||||
|
||||
This is not cosmetic. The ol-ext probe translated its proxy by the raw pointer
|
||||
delta, and a drag reading `-24.146 m` left the handle 24 m out while the
|
||||
constraint clamped at `-5.400 m` — the handle pointed at a road shape that cannot
|
||||
exist. Any input adapter must own the position update for this reason.
|
||||
|
||||
---
|
||||
|
||||
## 4. Sides: left is `tangent - 90`
|
||||
|
||||
The manifest reports `axisAzimuth = tangent + 90` for **both** sides, and that
|
||||
azimuth is the geometry compiler's *right*. See the backend spec for why. On the
|
||||
client this means `outwardSign()` negates the left side, and a test that asserts a
|
||||
side must name the direction geographically (`OUTWARD_LEFT` / `OUTWARD_RIGHT`)
|
||||
rather than by axis sign.
|
||||
|
||||
**Mistake made**: the first implementation assumed left was the positive axis, so
|
||||
dragging the visually-left handle moved the right kerb. Both the server placement
|
||||
and the client sign were wrong together, so neither side's tests caught it.
|
||||
|
||||
---
|
||||
|
||||
## 5. A constraint must travel with its operation
|
||||
|
||||
`validateEditDocument()` rejects any constraint whose `provenance.operationId` is
|
||||
not a recorded operation, and the check covers already-saved constraints too.
|
||||
Build the pair with `draftConstraint()` + `operationFor()`, and send
|
||||
`EditSession.fragment()`, which assembles both halves.
|
||||
|
||||
One gesture mints one operation id, released on pointerup. Reusing an id across
|
||||
gestures puts two operations with the same id in the document, which is also
|
||||
rejected.
|
||||
|
||||
**Mistake made**: sending `constraints` alone returned HTTP 400 on every drag.
|
||||
|
||||
---
|
||||
|
||||
## 6. Never compute inside a `setState` updater
|
||||
|
||||
React defers updater functions and, under StrictMode, calls them more than once.
|
||||
By the time one runs, closure variables captured during the gesture may already be
|
||||
cleared.
|
||||
|
||||
```ts
|
||||
// WRONG — threw `toLonLat(null)` and white-screened the page
|
||||
setReport((current) => ({ ...current, meters: projectHandleValue(handle, start!, now) }));
|
||||
|
||||
// RIGHT — compute in the event handler, pass plain values in
|
||||
const meters = projectHandleValue(handle, start, now);
|
||||
setReport((current) => ({ ...current, meters }));
|
||||
```
|
||||
|
||||
The `start!` non-null assertion is what hid the runtime problem from the type
|
||||
checker. The same rule covers reading live OpenLayers state inside an updater: it
|
||||
samples a different moment than the event did.
|
||||
|
||||
---
|
||||
|
||||
## 7. Per-frame feedback does not go through React
|
||||
|
||||
The ghost writes its OpenLayers source directly. Driving a readout from state on
|
||||
every `pointermove` produced ~1600 renders in a single probe session.
|
||||
`requestAnimationFrame` coalescing is the minimum; writing straight to the source
|
||||
is the rule for production.
|
||||
|
||||
---
|
||||
|
||||
## 8. `EditSession` owns the constraint set, and is mutated in place
|
||||
|
||||
`EditSession` holds the saved baseline, its operations, the command stack, and the
|
||||
`previewSeq` watermark. `PreviewRequester` captures the session object once, so
|
||||
adopting a server document uses `load()` rather than constructing a new session —
|
||||
a swapped object would leave the requester arbitrating for a session nobody reads.
|
||||
|
||||
`previewSeq` arbitration lives in `acceptPreview()`, not in the request layer: the
|
||||
request layer only sends and cancels. The discard rule is worth unit-testing and
|
||||
must not depend on network timing.
|
||||
|
||||
Undo of a **saved** gesture appends an inverse operation; persisted history is
|
||||
never rewritten. Undo of an unsaved one just moves the cursor.
|
||||
|
||||
---
|
||||
|
||||
## 9. The `directEdit` flag means nothing exists
|
||||
|
||||
With the flag off: no edit source is created, no layer is added, no interaction is
|
||||
registered, and no manifest request is made. The network trace and the canvas must
|
||||
match `main` exactly. Guard once at construction, not per call site.
|
||||
|
||||
`intervalEditingSupported` is a second, narrower switch: range handles stay hidden
|
||||
while `compileGeometry()` ignores `profile.interval`. A control whose drag changes
|
||||
nothing is worse than no control.
|
||||
|
||||
---
|
||||
|
||||
## 10. Saving is not the same as applying
|
||||
|
||||
`POST /api/edits` writes the document; `/api/state` serves the outputs of the last
|
||||
compile. Saving without recompiling leaves a refresh showing the pre-edit geometry,
|
||||
which is indistinguishable from a failed save. Report the save and the recompile
|
||||
separately — if the recompile fails, the save still succeeded and the message must
|
||||
say so.
|
||||
|
||||
---
|
||||
|
||||
## Checklist before committing edit-client changes
|
||||
|
||||
- [ ] No baseline source is written
|
||||
- [ ] No manifest field is copied onto a feature
|
||||
- [ ] Handle positions derive from clamped values
|
||||
- [ ] Sides asserted geographically, not by axis sign
|
||||
- [ ] Constraints sent with their operations
|
||||
- [ ] No geometry or OL reads inside a `setState` updater
|
||||
- [ ] No per-`pointermove` React state updates
|
||||
- [ ] Flag off ⇒ no sources, no layers, no interactions, no requests
|
||||
5
.trellis/spec/frontend/directory-structure.md
Normal file
5
.trellis/spec/frontend/directory-structure.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Directory Structure
|
||||
|
||||
The Vite workbench lives in `workbench/client/`. Application code is under `src/`: `components/` for UI, `map/` for OpenLayers adapters, `lib/` for API clients, `types/` for API contracts, and `ui/` for shared primitives. The Node HTTP server remains `workbench/server.js`.
|
||||
|
||||
Keep OpenLayers construction and source mutation outside page components; `src/components/MapCanvas.tsx` owns lifecycle and `src/map/layers.ts` owns the layer registry.
|
||||
3
.trellis/spec/frontend/hook-guidelines.md
Normal file
3
.trellis/spec/frontend/hook-guidelines.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Hook Guidelines
|
||||
|
||||
Use effects for imperative OpenLayers lifecycle only. `MapCanvas.tsx` constructs one map on mount, updates sources when server state changes, and updates visibility separately. Store callback-sensitive values such as selected road and scene mode in refs so stable OpenLayers style callbacks see current values.
|
||||
40
.trellis/spec/frontend/index.md
Normal file
40
.trellis/spec/frontend/index.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Frontend Development Guidelines
|
||||
|
||||
> Best practices for frontend development in this project.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains guidelines for frontend development. Fill in each file with your project's specific conventions.
|
||||
|
||||
---
|
||||
|
||||
## Guidelines Index
|
||||
|
||||
| Guide | Description | Status |
|
||||
|-------|-------------|--------|
|
||||
| [Direct Edit Client](./direct-edit-client.md) | Handle/ghost/preview layer ownership, drag projection, session and save contracts | Filled |
|
||||
| [Directory Structure](./directory-structure.md) | Module organization and file layout | To fill |
|
||||
| [Component Guidelines](./component-guidelines.md) | Component patterns, props, composition | To fill |
|
||||
| [Hook Guidelines](./hook-guidelines.md) | Custom hooks, data fetching patterns | To fill |
|
||||
| [State Management](./state-management.md) | Local state, global state, server state | To fill |
|
||||
| [Quality Guidelines](./quality-guidelines.md) | Code standards, forbidden patterns | To fill |
|
||||
| [Type Safety](./type-safety.md) | Type patterns, validation | To fill |
|
||||
|
||||
---
|
||||
|
||||
## How to Fill These Guidelines
|
||||
|
||||
For each guideline file:
|
||||
|
||||
1. Document your project's **actual conventions** (not ideals)
|
||||
2. Include **code examples** from your codebase
|
||||
3. List **forbidden patterns** and why
|
||||
4. Add **common mistakes** your team has made
|
||||
|
||||
The goal is to help AI assistants and new team members understand how YOUR project works.
|
||||
|
||||
---
|
||||
|
||||
**Language**: All documentation should be written in **English**.
|
||||
37
.trellis/spec/frontend/quality-guidelines.md
Normal file
37
.trellis/spec/frontend/quality-guidelines.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Quality Guidelines
|
||||
|
||||
> Code quality standards for frontend development.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The workbench frontend uses Prettier as the source formatting authority. Code is formatted before review; formatting is not left to individual editor settings.
|
||||
|
||||
---
|
||||
|
||||
## Forbidden Patterns
|
||||
|
||||
- Do not commit dense, manually minified, or single-line JSX/TypeScript.
|
||||
- Do not hand-format around Prettier output.
|
||||
|
||||
---
|
||||
|
||||
## Required Patterns
|
||||
|
||||
- Run `npm run format` after editing frontend files.
|
||||
- Run `npm run format:check` in the validation gate.
|
||||
- Keep React components, hooks, types, and API clients in separately formatted modules once a component becomes non-trivial.
|
||||
|
||||
---
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Run `npm run format:check`, `npm run test:client`, and `npm run build` for frontend changes.
|
||||
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
- Formatting check passes.
|
||||
- UI behavior changes do not rebuild the OpenLayers map unless the map lifecycle explicitly requires it.
|
||||
3
.trellis/spec/frontend/state-management.md
Normal file
3
.trellis/spec/frontend/state-management.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# State Management
|
||||
|
||||
Use component `useState` for server state, selection, staged overrides, layer visibility and UI filters. `src/lib/api.ts` is the sole browser HTTP boundary. Keep staged overrides separate from persisted `WorkbenchState.overrides`; save them before compile.
|
||||
3
.trellis/spec/frontend/type-safety.md
Normal file
3
.trellis/spec/frontend/type-safety.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Type Safety
|
||||
|
||||
Define browser contracts in `src/types/state.ts` from actual workbench API payloads. Use `unknown` for extensible GeoJSON properties and comparison payloads, then narrow locally. Run `npm run test:client` after TypeScript changes; avoid `any` and broad assertions.
|
||||
223
.trellis/spec/guides/code-reuse-thinking-guide.md
Normal file
223
.trellis/spec/guides/code-reuse-thinking-guide.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Code Reuse Thinking Guide
|
||||
|
||||
> **Purpose**: Stop and think before creating new code - does it already exist?
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
**Duplicated code is the #1 source of inconsistency bugs.**
|
||||
|
||||
When you copy-paste or rewrite existing logic:
|
||||
- Bug fixes don't propagate
|
||||
- Behavior diverges over time
|
||||
- Codebase becomes harder to understand
|
||||
|
||||
---
|
||||
|
||||
## Before Writing New Code
|
||||
|
||||
### Step 1: Search First
|
||||
|
||||
```bash
|
||||
# Search for similar function names
|
||||
grep -r "functionName" .
|
||||
|
||||
# Search for similar logic
|
||||
grep -r "keyword" .
|
||||
```
|
||||
|
||||
### Step 2: Ask These Questions
|
||||
|
||||
| Question | If Yes... |
|
||||
|----------|-----------|
|
||||
| Does a similar function exist? | Use or extend it |
|
||||
| Is this pattern used elsewhere? | Follow the existing pattern |
|
||||
| Could this be a shared utility? | Create it in the right place |
|
||||
| Am I copying code from another file? | **STOP** - extract to shared |
|
||||
|
||||
---
|
||||
|
||||
## Common Duplication Patterns
|
||||
|
||||
### Pattern 1: Copy-Paste Functions
|
||||
|
||||
**Bad**: Copying a validation function to another file
|
||||
|
||||
**Good**: Extract to shared utilities, import where needed
|
||||
|
||||
### Pattern 2: Similar Components
|
||||
|
||||
**Bad**: Creating a new component that's 80% similar to existing
|
||||
|
||||
**Good**: Extend existing component with props/variants
|
||||
|
||||
### Pattern 3: Repeated Constants
|
||||
|
||||
**Bad**: Defining the same constant in multiple files
|
||||
|
||||
**Good**: Single source of truth, import everywhere
|
||||
|
||||
### Pattern 4: Repeated Payload Field Extraction
|
||||
|
||||
**Bad**: Multiple consumers cast the same JSON/event fields locally:
|
||||
|
||||
```typescript
|
||||
const description = (ev as { description?: string }).description;
|
||||
const context = (ev as { context?: ContextEntry[] }).context;
|
||||
```
|
||||
|
||||
This is duplicated contract logic even when the code is only two lines. Each
|
||||
consumer now has its own definition of what a valid payload means.
|
||||
|
||||
**Good**: Put the decoder, type guard, or projection next to the data owner:
|
||||
|
||||
```typescript
|
||||
if (isThreadEvent(ev)) {
|
||||
renderThreadEvent(ev);
|
||||
}
|
||||
```
|
||||
|
||||
**Rule**: If the same untyped payload field is read in 2+ places, create a
|
||||
shared type guard / normalizer / projection before adding a third reader.
|
||||
|
||||
---
|
||||
|
||||
## When to Abstract
|
||||
|
||||
**Abstract when**:
|
||||
- Same code appears 3+ times
|
||||
- Logic is complex enough to have bugs
|
||||
- Multiple people might need this
|
||||
|
||||
**Don't abstract when**:
|
||||
- Only used once
|
||||
- Trivial one-liner
|
||||
- Abstraction would be more complex than duplication
|
||||
|
||||
---
|
||||
|
||||
## After Batch Modifications
|
||||
|
||||
When you've made similar changes to multiple files:
|
||||
|
||||
1. **Review**: Did you catch all instances?
|
||||
2. **Search**: Run grep to find any missed
|
||||
3. **Consider**: Should this be abstracted?
|
||||
|
||||
### Reducers Should Use Exhaustive Structure
|
||||
|
||||
When state is derived from action-like values (`action`, `kind`, `status`,
|
||||
`phase`), prefer a reducer with one `switch` over scattered `if/else` updates.
|
||||
|
||||
```typescript
|
||||
// BAD - action-specific state transitions are hard to audit
|
||||
if (action === "opened") { ... }
|
||||
else if (action === "comment") { ... }
|
||||
else if (action === "status") { ... }
|
||||
|
||||
// GOOD - one reducer owns the transition table
|
||||
switch (event.action) {
|
||||
case "opened":
|
||||
...
|
||||
return;
|
||||
case "comment":
|
||||
...
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
This matters when the event log is the source of truth. A reducer is the
|
||||
documented replay model; display code and commands should not duplicate pieces
|
||||
of that replay model.
|
||||
|
||||
---
|
||||
|
||||
## Checklist Before Commit
|
||||
|
||||
- [ ] Searched for existing similar code
|
||||
- [ ] No copy-pasted logic that should be shared
|
||||
- [ ] No repeated untyped payload field extraction outside a shared decoder
|
||||
- [ ] Constants defined in one place
|
||||
- [ ] Similar patterns follow same structure
|
||||
- [ ] Reducer/action transitions live in one reducer or command dispatcher
|
||||
|
||||
---
|
||||
|
||||
## Gotcha: Python if/elif/else Exhaustive Check
|
||||
|
||||
**Problem**: Python's if/elif/else chains have no compile-time exhaustive check. When you add a new value to a `Literal` type (e.g., `Platform`), existing if/elif/else chains silently fall through to `else` with wrong defaults.
|
||||
|
||||
**Symptom**: New platform works partially — some methods return Claude defaults instead of platform-specific values. No error is raised.
|
||||
|
||||
**Example** (`cli_adapter.py`):
|
||||
```python
|
||||
# BAD: "gemini" falls through to else, returns "claude"
|
||||
@property
|
||||
def cli_name(self) -> str:
|
||||
if self.platform == "opencode":
|
||||
return "opencode"
|
||||
else:
|
||||
return "claude" # gemini silently gets "claude"!
|
||||
|
||||
# GOOD: explicit branch for every platform
|
||||
@property
|
||||
def cli_name(self) -> str:
|
||||
if self.platform == "opencode":
|
||||
return "opencode"
|
||||
elif self.platform == "gemini":
|
||||
return "gemini"
|
||||
else:
|
||||
return "claude"
|
||||
```
|
||||
|
||||
**Prevention**: When adding a new value to a Python `Literal` type, search for ALL if/elif/else chains that switch on that type and add explicit branches. Don't rely on `else` being correct for new values.
|
||||
|
||||
---
|
||||
|
||||
## Gotcha: Asymmetric Mechanisms Producing Same Output
|
||||
|
||||
**Problem**: When two different mechanisms must produce the same file set (e.g., recursive directory copy for init vs. manual `files.set()` for update), structural changes (renaming, moving, adding subdirectories) only propagate through the automatic mechanism. The manual one silently drifts.
|
||||
|
||||
**Symptom**: Init works perfectly, but update creates files at wrong paths or misses files entirely.
|
||||
|
||||
**Prevention**:
|
||||
- **Best**: Eliminate the asymmetry — have the manual path call the automatic one (e.g., `collectTemplateFiles()` calls `getAllScripts()` instead of maintaining its own list)
|
||||
- **If asymmetry is unavoidable**: Add a regression test that compares outputs from both mechanisms
|
||||
- When migrating directory structures, search for ALL code paths that reference the old structure
|
||||
|
||||
**Real example**: `trellis update` had a manual `files.set()` list for 11 scripts that `getAllScripts()` already tracked. Fix: replaced the manual list with a `for..of getAllScripts()` loop. See `update.ts` refactor in v0.4.0-beta.3.
|
||||
|
||||
---
|
||||
|
||||
## Template File Registration (Trellis-specific)
|
||||
|
||||
When adding new files to `src/templates/trellis/scripts/`:
|
||||
|
||||
**Single registration point**: `src/templates/trellis/index.ts`
|
||||
|
||||
1. Add `export const xxxScript = readTemplate("scripts/path/file.py");`
|
||||
2. Add to `getAllScripts()` Map
|
||||
|
||||
That's it. `commands/update.ts` uses `getAllScripts()` directly — no manual sync needed.
|
||||
|
||||
**Why this matters**: Without registration in `getAllScripts()`, `trellis update` won't sync the file to user projects. Bug fixes and features won't propagate.
|
||||
|
||||
**History**: Before v0.4.0-beta.3, `update.ts` had its own hand-maintained file list that frequently fell out of sync with `getAllScripts()`. This caused 11 Python files to be silently skipped during `trellis update`. The fix was to eliminate the duplicate list and use `getAllScripts()` as the single source of truth.
|
||||
|
||||
### Quick Checklist for New Scripts
|
||||
|
||||
```bash
|
||||
# After adding a new .py file, verify it's in getAllScripts():
|
||||
grep -l "newFileName" src/templates/trellis/index.ts # Should match
|
||||
```
|
||||
|
||||
### Template Sync Convention
|
||||
|
||||
`.trellis/scripts/` (dogfooded) and `packages/cli/src/templates/trellis/scripts/` (template) must stay identical. After editing `.trellis/scripts/`, always sync:
|
||||
|
||||
```bash
|
||||
rsync -av --delete --exclude='__pycache__' .trellis/scripts/ packages/cli/src/templates/trellis/scripts/
|
||||
```
|
||||
|
||||
**Gotcha**: Running rsync with wrong source/destination paths can create nested garbage directories (e.g., `.trellis/scripts/packages/cli/...`). Always double-check paths before running.
|
||||
327
.trellis/spec/guides/cross-layer-thinking-guide.md
Normal file
327
.trellis/spec/guides/cross-layer-thinking-guide.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# Cross-Layer Thinking Guide
|
||||
|
||||
> **Purpose**: Think through data flow across layers before implementing.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
**Most bugs happen at layer boundaries**, not within layers.
|
||||
|
||||
Common cross-layer bugs:
|
||||
|
||||
- API returns format A, frontend expects format B
|
||||
- Database stores X, service transforms to Y, but loses data
|
||||
- Multiple layers implement the same logic differently
|
||||
|
||||
---
|
||||
|
||||
## Before Implementing Cross-Layer Features
|
||||
|
||||
### Step 1: Map the Data Flow
|
||||
|
||||
Draw out how data moves:
|
||||
|
||||
```
|
||||
Source → Transform → Store → Retrieve → Transform → Display
|
||||
```
|
||||
|
||||
For each arrow, ask:
|
||||
|
||||
- What format is the data in?
|
||||
- What could go wrong?
|
||||
- Who is responsible for validation?
|
||||
|
||||
### Step 2: Identify Boundaries
|
||||
|
||||
| Boundary | Common Issues |
|
||||
| --------------------- | --------------------------------- |
|
||||
| API ↔ Service | Type mismatches, missing fields |
|
||||
| Service ↔ Database | Format conversions, null handling |
|
||||
| Backend ↔ Frontend | Serialization, date formats |
|
||||
| Component ↔ Component | Props shape changes |
|
||||
|
||||
### Step 3: Define Contracts
|
||||
|
||||
For each boundary:
|
||||
|
||||
- What is the exact input format?
|
||||
- What is the exact output format?
|
||||
- What errors can occur?
|
||||
|
||||
---
|
||||
|
||||
## Common Cross-Layer Mistakes
|
||||
|
||||
### Mistake 1: Implicit Format Assumptions
|
||||
|
||||
**Bad**: Assuming date format without checking
|
||||
|
||||
**Good**: Explicit format conversion at boundaries
|
||||
|
||||
### Mistake 2: Scattered Validation
|
||||
|
||||
**Bad**: Validating the same thing in multiple layers
|
||||
|
||||
**Good**: Validate once at the entry point
|
||||
|
||||
### Mistake 3: Leaky Abstractions
|
||||
|
||||
**Bad**: Component knows about database schema
|
||||
|
||||
**Good**: Each layer only knows its neighbors
|
||||
|
||||
### Mistake 4: Every Consumer Parses The Same Payload
|
||||
|
||||
**Bad**: A command reads JSONL events and casts fields inline:
|
||||
|
||||
```typescript
|
||||
const thread = (ev as { thread?: string }).thread;
|
||||
const labels = (ev as { labels?: string[] }).labels;
|
||||
```
|
||||
|
||||
This looks local, but it means every consumer owns a private version of the
|
||||
event contract. The next field change will update one command and miss another.
|
||||
|
||||
**Good**: Decode once at the event boundary, then export typed projections:
|
||||
|
||||
```typescript
|
||||
if (!isThreadEvent(ev)) return false;
|
||||
return ev.thread === filter.thread;
|
||||
```
|
||||
|
||||
**Rule**: For append-only logs, JSON streams, RPC payloads, or config files,
|
||||
create one owner for:
|
||||
|
||||
- event / payload type definitions
|
||||
- type guards and normalization from `unknown`
|
||||
- metadata projections used by UI commands
|
||||
- reducers that replay state from the source of truth
|
||||
|
||||
Rendering code may format fields, but it must not redefine the payload contract.
|
||||
|
||||
---
|
||||
|
||||
## Checklist for Cross-Layer Features
|
||||
|
||||
Before implementation:
|
||||
|
||||
- [ ] Mapped the complete data flow
|
||||
- [ ] Identified all layer boundaries
|
||||
- [ ] Defined format at each boundary
|
||||
- [ ] Decided where validation happens
|
||||
|
||||
After implementation:
|
||||
|
||||
- [ ] Tested with edge cases (null, empty, invalid)
|
||||
- [ ] Verified error handling at each boundary
|
||||
- [ ] Checked data survives round-trip
|
||||
- [ ] Checked that consumers import shared decoders / projections instead of
|
||||
casting payload fields locally
|
||||
- [ ] Checked that derived state points back to the source event identifier
|
||||
(`seq`, `id`, `version`) instead of inventing a second cursor
|
||||
|
||||
---
|
||||
|
||||
## Cross-Platform Template Consistency
|
||||
|
||||
In Trellis, command templates (e.g., `record-session.md`) exist in **multiple platforms** with identical or near-identical content. This is a cross-layer boundary.
|
||||
|
||||
### Checklist: After Modifying Any Command Template
|
||||
|
||||
- [ ] Find all platforms with the same command: `find src/templates/*/commands/trellis/ -name "<command>.*"`
|
||||
- [ ] Update all platform copies (Markdown `.md` and TOML `.toml`)
|
||||
- [ ] For Gemini TOML: adapt line continuations (`\\` vs `\`) and triple-quoted strings
|
||||
- [ ] Run `/trellis:check-cross-layer` to verify nothing was missed
|
||||
|
||||
**Real-world example**: Updated `record-session.md` in Claude to use `--mode record`, but forgot iFlow, Kilo, OpenCode, and Gemini — caught by cross-layer check.
|
||||
|
||||
---
|
||||
|
||||
## Generated Runtime Template Upgrade Consistency
|
||||
|
||||
Some generated files are both documentation and runtime input. In Trellis,
|
||||
`.trellis/workflow.md` is parsed by `get_context.py`, `workflow_phase.py`,
|
||||
SessionStart filters, and per-turn hooks. Template changes must be validated
|
||||
against both fresh init and upgrade paths.
|
||||
|
||||
### Checklist: After Modifying A Runtime-Parsed Template
|
||||
|
||||
- [ ] Identify every runtime parser that reads the template, not just the file
|
||||
writer that installs it
|
||||
- [ ] Check whether relevant syntax lives outside obvious managed regions
|
||||
such as tag blocks
|
||||
- [ ] Verify fresh `init` output and a versioned `update` scenario that writes
|
||||
the older `.trellis/.version`
|
||||
- [ ] Add an upgrade regression using an older pristine template fixture, then
|
||||
assert the installed file reaches the current packaged shape
|
||||
- [ ] Update the backend spec that owns the runtime contract
|
||||
|
||||
---
|
||||
|
||||
## Versioned Documentation Boundary
|
||||
|
||||
Versioned documentation is a cross-layer boundary: source paths, `docs.json`
|
||||
version routing, and the rendered version selector must all describe the same
|
||||
release line.
|
||||
|
||||
### Checklist: Before Editing Versioned Docs
|
||||
|
||||
- [ ] Identify the target release line: stable, beta, or RC
|
||||
- [ ] Verify the edited MDX path matches that line:
|
||||
- stable: `docs-site/{start,advanced,...}` and `docs-site/zh/{start,advanced,...}`
|
||||
- beta: `docs-site/beta/**` and `docs-site/zh/beta/**`
|
||||
- RC: `docs-site/rc/**` and `docs-site/zh/rc/**`
|
||||
- [ ] Verify `docs.json` navigation points the version label to the same paths
|
||||
- [ ] Grep the opposite tree for release-line-specific terms before committing
|
||||
- [ ] Treat beta content appearing under root release paths as a source-path bug,
|
||||
not a rendering bug
|
||||
|
||||
**Real-world example**: A beta-only task workflow change documented
|
||||
`prd.md` + `design.md` + `implement.md`, task-creation consent, and Codex
|
||||
mode banners under root `start/` and `advanced/` paths. The docs site then
|
||||
served 0.6 beta behavior under the Release selector. The fix was to restore root
|
||||
release docs, move the 0.6 content to `beta/` and `zh/beta/`, and add a grep
|
||||
audit for beta markers against the root release tree.
|
||||
|
||||
**Real-world example**: Codex inline mode changed workflow platform markers from
|
||||
`[Codex]` / `[Kilo, Antigravity, Windsurf]` to `[codex-sub-agent]` /
|
||||
`[codex-inline, Kilo, Antigravity, Windsurf]`. Fresh init was correct, but
|
||||
`trellis update` only merged `[workflow-state:*]` blocks and preserved stale
|
||||
markers outside those blocks. Result: upgraded projects got new hook scripts
|
||||
but old workflow routing, so `get_context.py --mode phase --platform codex`
|
||||
could return empty Phase 2.1 detail.
|
||||
|
||||
---
|
||||
|
||||
## Mode-Detection Probe Checklist
|
||||
|
||||
When a CLI auto-detects a mode by probing a remote resource (e.g., checking if `index.json` exists to decide marketplace vs direct download):
|
||||
|
||||
### Before implementing:
|
||||
|
||||
- [ ] Probe runs in **ALL** code paths that use the result (interactive, `-y`, `--flag` combos)
|
||||
- [ ] 404 vs transient error are distinguished — don't treat both as "not found"
|
||||
- [ ] Transient errors **abort or retry**, never silently switch modes
|
||||
- [ ] Shared state (caches, prefetched data) is **reset** when context changes (e.g., user switches source)
|
||||
- [ ] **Shortcut paths** (e.g., `--template` skipping picker) must have the same error-handling quality as the probed path — check that downstream functions don't call catch-all wrappers
|
||||
|
||||
### After implementing:
|
||||
|
||||
- [ ] Trace every path from probe result to the mode-decision branch — no fallthrough
|
||||
- [ ] External format contracts (giget URI, raw URLs) are tested or at least documented as comments
|
||||
- [ ] Metadata reads consume a complete response or use a streaming parser — never parse a fixed-size prefix as full JSON
|
||||
- [ ] When reconstructing a composite identifier from parsed parts, verify **all** fields are included and in the **correct position** (e.g., `provider:repo/path#ref` not `provider:repo#ref/path`)
|
||||
- [ ] Verify that **action functions** called after a shortcut don't internally use the old catch-all fetch — they must use the probe-quality variant when error distinction matters
|
||||
|
||||
**Real-world example**: Custom registry flow had 8 bugs across 3 review rounds: (1) probe only ran in interactive mode, (2) transient errors fell through to wrong mode, (3) giget URI had `#ref` in wrong position, (4) prefetched templates leaked across source switches, (5) `--template` shortcut bypassed probe but `downloadTemplateById` internally used catch-all `fetchTemplateIndex`, turning timeouts into "Template not found".
|
||||
|
||||
**Real-world example**: Agent-session update hints fetched npm `latest` metadata with `response.read(4096)` and then parsed it as complete JSON. The `@mindfoldhq/trellis` package metadata exceeded 4 KB, so the JSON was truncated, parse failed silently, and the first session injection showed no update hint. Fix: read the complete response before parsing, and add a regression where `version` is followed by an 8 KB metadata tail.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Platform Template Consistency
|
||||
|
||||
In Trellis, command templates (e.g., `record-session.md`) exist in **multiple platforms** with identical or near-identical content. This is a cross-layer boundary.
|
||||
|
||||
### Checklist: After Modifying Any Command Template
|
||||
|
||||
- [ ] Find all platforms with the same command: `find src/templates/*/commands/trellis/ -name "<command>.*"`
|
||||
- [ ] Update all platform copies (Markdown `.md` and TOML `.toml`)
|
||||
- [ ] For Gemini TOML: adapt line continuations (`\\` vs `\`) and triple-quoted strings
|
||||
- [ ] Run `/trellis:check-cross-layer` to verify nothing was missed
|
||||
|
||||
**Real-world example**: Updated `record-session.md` in Claude to use `--mode record`, but forgot iFlow, Kilo, OpenCode, and Gemini — caught by cross-layer check.
|
||||
|
||||
---
|
||||
|
||||
## Generated Runtime Template Upgrade Consistency
|
||||
|
||||
Some generated files are both documentation and runtime input. In Trellis,
|
||||
`.trellis/workflow.md` is parsed by `get_context.py`, `workflow_phase.py`,
|
||||
SessionStart filters, and per-turn hooks. Template changes must be validated
|
||||
against both fresh init and upgrade paths.
|
||||
|
||||
### Checklist: After Modifying A Runtime-Parsed Template
|
||||
|
||||
- [ ] Identify every runtime parser that reads the template, not just the file
|
||||
writer that installs it
|
||||
- [ ] Check whether relevant syntax lives outside obvious managed regions
|
||||
such as tag blocks
|
||||
- [ ] Verify fresh `init` output and a versioned `update` scenario that writes
|
||||
the older `.trellis/.version`
|
||||
- [ ] Add an upgrade regression using an older pristine template fixture, then
|
||||
assert the installed file reaches the current packaged shape
|
||||
- [ ] Update the backend spec that owns the runtime contract
|
||||
|
||||
**Real-world example**: Codex inline mode changed workflow platform markers from
|
||||
`[Codex]` / `[Kilo, Antigravity, Windsurf]` to `[codex-sub-agent]` /
|
||||
`[codex-inline, Kilo, Antigravity, Windsurf]`. Fresh init was correct, but
|
||||
`trellis update` only merged `[workflow-state:*]` blocks and preserved stale
|
||||
markers outside those blocks. Result: upgraded projects got new hook scripts
|
||||
but old workflow routing, so `get_context.py --mode phase --platform codex`
|
||||
could return empty Phase 2.1 detail.
|
||||
|
||||
---
|
||||
|
||||
## Mode-Detection Probe Checklist
|
||||
|
||||
When a CLI auto-detects a mode by probing a remote resource (e.g., checking if `index.json` exists to decide marketplace vs direct download):
|
||||
|
||||
### Before implementing:
|
||||
- [ ] Probe runs in **ALL** code paths that use the result (interactive, `-y`, `--flag` combos)
|
||||
- [ ] 404 vs transient error are distinguished — don't treat both as "not found"
|
||||
- [ ] Transient errors **abort or retry**, never silently switch modes
|
||||
- [ ] Shared state (caches, prefetched data) is **reset** when context changes (e.g., user switches source)
|
||||
- [ ] **Shortcut paths** (e.g., `--template` skipping picker) must have the same error-handling quality as the probed path — check that downstream functions don't call catch-all wrappers
|
||||
|
||||
### After implementing:
|
||||
- [ ] Trace every path from probe result to the mode-decision branch — no fallthrough
|
||||
- [ ] External format contracts (giget URI, raw URLs) are tested or at least documented as comments
|
||||
- [ ] Metadata reads consume a complete response or use a streaming parser — never parse a fixed-size prefix as full JSON
|
||||
- [ ] When reconstructing a composite identifier from parsed parts, verify **all** fields are included and in the **correct position** (e.g., `provider:repo/path#ref` not `provider:repo#ref/path`)
|
||||
- [ ] Verify that **action functions** called after a shortcut don't internally use the old catch-all fetch — they must use the probe-quality variant when error distinction matters
|
||||
|
||||
**Real-world example**: Custom registry flow had 8 bugs across 3 review rounds: (1) probe only ran in interactive mode, (2) transient errors fell through to wrong mode, (3) giget URI had `#ref` in wrong position, (4) prefetched templates leaked across source switches, (5) `--template` shortcut bypassed probe but `downloadTemplateById` internally used catch-all `fetchTemplateIndex`, turning timeouts into "Template not found".
|
||||
|
||||
**Real-world example**: Agent-session update hints fetched npm `latest` metadata with `response.read(4096)` and then parsed it as complete JSON. The `@mindfoldhq/trellis` package metadata exceeded 4 KB, so the JSON was truncated, parse failed silently, and the first session injection showed no update hint. Fix: read the complete response before parsing, and add a regression where `version` is followed by an 8 KB metadata tail.
|
||||
|
||||
---
|
||||
|
||||
## When to Create Flow Documentation
|
||||
|
||||
Create detailed flow docs when:
|
||||
|
||||
- Feature spans 3+ layers
|
||||
- Multiple teams are involved
|
||||
- Data format is complex
|
||||
- Feature has caused bugs before
|
||||
|
||||
---
|
||||
|
||||
## Event Log / Projection Boundary
|
||||
|
||||
Append-only logs are cross-layer contracts. A single event travels through:
|
||||
|
||||
```
|
||||
CLI input → event writer → events.jsonl → reader → filter → reducer → display
|
||||
```
|
||||
|
||||
### Checklist: After Adding A New Event Kind Or Field
|
||||
|
||||
- [ ] Add the event kind to the central event taxonomy
|
||||
- [ ] Add a typed event variant or type guard at the event layer
|
||||
- [ ] Add normalization helpers for array/object fields that come from
|
||||
user input or JSON
|
||||
- [ ] Keep `seq` / `id` assignment in the event writer only
|
||||
- [ ] Make filters and reducers consume the typed event guard, not local casts
|
||||
- [ ] Make display code consume reducer output or typed events, not raw JSON
|
||||
- [ ] Add at least one regression that proves history replay and live filtering
|
||||
use the same filter model
|
||||
|
||||
**Real-world example**: Thread channels added `kind: "thread"`, `description`,
|
||||
`context`, labels, and `lastSeq`. The first implementation replayed thread
|
||||
state correctly, but several commands still re-parsed event payload fields with
|
||||
local casts. The fix was to make the core event layer own `ThreadChannelEvent`
|
||||
and `isThreadEvent`, make `reduceChannelMetadata` the only channel metadata
|
||||
projection, and make `reduceThreads` the only thread replay reducer.
|
||||
97
.trellis/spec/guides/index.md
Normal file
97
.trellis/spec/guides/index.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Thinking Guides
|
||||
|
||||
> **Purpose**: Expand your thinking to catch things you might not have considered.
|
||||
|
||||
---
|
||||
|
||||
## Why Thinking Guides?
|
||||
|
||||
**Most bugs and tech debt come from "didn't think of that"**, not from lack of skill:
|
||||
|
||||
- Didn't think about what happens at layer boundaries → cross-layer bugs
|
||||
- Didn't think about code patterns repeating → duplicated code everywhere
|
||||
- Didn't think about edge cases → runtime errors
|
||||
- Didn't think about future maintainers → unreadable code
|
||||
|
||||
These guides help you **ask the right questions before coding**.
|
||||
|
||||
---
|
||||
|
||||
## Available Guides
|
||||
|
||||
| Guide | Purpose | When to Use |
|
||||
|-------|---------|-------------|
|
||||
| [Code Reuse Thinking Guide](./code-reuse-thinking-guide.md) | Identify patterns and reduce duplication | When you notice repeated patterns |
|
||||
| [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) | Think through data flow across layers | Features spanning multiple layers |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Thinking Triggers
|
||||
|
||||
### When to Think About Cross-Layer Issues
|
||||
|
||||
- [ ] Feature touches 3+ layers (API, Service, Component, Database)
|
||||
- [ ] Data format changes between layers
|
||||
- [ ] Multiple consumers need the same data
|
||||
- [ ] You're not sure where to put some logic
|
||||
- [ ] You are adding an event kind, JSONL record, RPC payload, or config field
|
||||
- [ ] UI / command code starts casting raw payload fields directly
|
||||
|
||||
→ Read [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md)
|
||||
|
||||
### When to Think About Code Reuse
|
||||
|
||||
- [ ] You're writing similar code to something that exists
|
||||
- [ ] You see the same pattern repeated 3+ times
|
||||
- [ ] You're adding a new field to multiple places
|
||||
- [ ] **You're modifying any constant or config**
|
||||
- [ ] **You're creating a new utility/helper function** ← Search first!
|
||||
- [ ] Two files read the same untyped payload field with local casts
|
||||
- [ ] Multiple branches update the same derived state from `kind` / `action`
|
||||
|
||||
→ Read [Code Reuse Thinking Guide](./code-reuse-thinking-guide.md)
|
||||
|
||||
### When Verifying AI Cross-Review Results
|
||||
|
||||
- [ ] Reviewer claims "user input can be malicious" → Check the actual data source (internal manifest? user config? external API?)
|
||||
- [ ] Reviewer flags "missing validation" → Is the data from a trusted internal source?
|
||||
- [ ] Reviewer says "behavior change" → Read the code comments — is it intentional design?
|
||||
- [ ] Reviewer identifies a "bug" in test → Mentally delete the feature being tested — does the test still pass? If yes → tautological test
|
||||
|
||||
**Common AI reviewer false-positive patterns**:
|
||||
1. **Trust boundary confusion**: Treating internal data (bundled JSON manifests) as untrusted external input
|
||||
2. **Ignoring design comments**: Flagging intentional behavior documented in code comments as bugs
|
||||
3. **Variable misreading**: Not tracing a variable to its actual definition (e.g., Map keyed by path vs name)
|
||||
|
||||
**Verification rule**: Every CRITICAL/WARNING finding must be verified against the actual code before prioritizing. Budget ~35% false-positive rate for AI reviews.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Modification Rule (CRITICAL)
|
||||
|
||||
> **Before changing ANY value, ALWAYS search first!**
|
||||
|
||||
```bash
|
||||
# Search for the value you're about to change
|
||||
grep -r "value_to_change" .
|
||||
```
|
||||
|
||||
This single habit prevents most "forgot to update X" bugs.
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Directory
|
||||
|
||||
1. **Before coding**: Skim the relevant thinking guide
|
||||
2. **During coding**: If something feels repetitive or complex, check the guides
|
||||
3. **After bugs**: Add new insights to the relevant guide (learn from mistakes)
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a new "didn't think of that" moment? Add it to the relevant guide.
|
||||
|
||||
---
|
||||
|
||||
**Core Principle**: 30 minutes of thinking saves 3 hours of debugging.
|
||||
@@ -0,0 +1,4 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/junction-tools.md", "reason": "逐条核对四态语义、会话互斥与「应用不等于保存」是否被正确实现"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "核对路口三个 kind 的锚点、approach 优先级与 reserve 独占边界"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/joint-solver.md", "reason": "核对预览是否覆盖全部受影响派生对象,以及拟合失败时的诊断行为"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "检查是否重复实现 EditSession、命令栈或 ghost/preview 图层"}
|
||||
21
.trellis/tasks/08-26-direct-edit-junction-tools/design.md
Normal file
21
.trellis/tasks/08-26-direct-edit-junction-tools/design.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 设计
|
||||
|
||||
技术合约不在本文重复定义。权威定义见父任务 `.trellis/tasks/08-26-direct-manipulation-road-editor/design.md` 的以下小节:
|
||||
|
||||
- 「约束模型」— `junction-approach-width`、`junction-cutback`、`junction-corner-radius` 三个 kind 与其锚点。
|
||||
- 「编辑所有权」— junction reserve 独占、`junction-approach-width` 优先于道路 profile、单 `JunctionRef` 会话。
|
||||
- 「Handle manifest」— 手柄字段与 `reserves`。
|
||||
- 「预览时序与延迟预算」— 防抖、`previewSeq` 仲裁、`degraded`。
|
||||
- 「上线与回滚形态」— `directEdit` 开关。
|
||||
|
||||
会话状态、四态语义与所有权边界的完整推导见父任务 `research/junction-tools.md`。
|
||||
|
||||
## 本子任务的局部决定
|
||||
|
||||
- 工作区是独立路由,不是主地图上的浮层面板:路口编辑是高密度局部操作,叠在主地图上会与道路手柄争夺命中区。
|
||||
- 复用 `direct-edit-map-editor` 的 `EditSession`、ghost/preview source 与命令栈,不新建第二套编辑基础设施。差异只在加载的上下文范围与可用 kind。
|
||||
- 草稿属于活动工作副本的 session,不是独立导出文件。因此「取消」只需丢弃 session,不涉及文件回滚。
|
||||
- 「应用到工作区」不写盘:合并后的约束是活动副本的未保存状态,交给主地图的普通 undo/redo 管理。这样保存路径只有一条。
|
||||
- 会话互斥用显式守卫实现:存在未处理草稿时切换 `JunctionRef` 直接拒绝并提示,不做自动应用或自动丢弃——两者都会让用户丢失意图。
|
||||
- 本期只实现 `JunctionRef.type === 'node'`。`cluster` 分支保留类型但入口置灰,避免把 cluster 降级成多个互相冲突的普通路口编辑。
|
||||
- 单个角部圆角:本期只允许编辑一个 corner,多角部批量编辑属后续任务。
|
||||
@@ -0,0 +1,6 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "父任务权威合约:三个 junction kind、编辑所有权、handle manifest、预览时序、directEdit 开关"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/junction-tools.md", "reason": "会话边界、四态语义、入口与上下文加载范围、所有权划分的完整推导"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/joint-solver.md", "reason": "路口局部求解必须一起重算的派生对象清单与约束不变量"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "现有 compileJunctionPlans、cutbackMeters 与 junctionTemplates 的能力边界"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "必须复用 EditSession、ghost/preview source 与命令栈,禁止新建第二套编辑基础设施"}
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "会话状态与约束在客户端、API、求解器之间的流转一致性"}
|
||||
41
.trellis/tasks/08-26-direct-edit-junction-tools/implement.md
Normal file
41
.trellis/tasks/08-26-direct-edit-junction-tools/implement.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 实施计划
|
||||
|
||||
对应父任务 `implement.md` 的第 7 步。每步一个提交,门禁不过就停。
|
||||
|
||||
前置:`direct-edit-solver-api`(junction 三个 kind 的求解与预览端点)与 `direct-edit-map-editor`(`EditSession`、ghost/preview source、命令栈)均已交付。
|
||||
|
||||
## 1. 工作区路由与上下文加载
|
||||
|
||||
- 目标:能进能回,且不丢全局方位。
|
||||
- 范围:单 `JunctionRef` 路由(本期只走 `node`,`cluster` 入口置灰);从路口面或诊断进入;加载该路口的编辑上下文(路口面、进口、相邻道路短上下文、车道/connector、步行带、停止线、斑马线、诊断、当前约束);返回主地图并恢复位置。
|
||||
- 验证:`npm run test:client`、`npm run build`;手测——从路口面进入、从诊断进入、返回后主地图位置与选中态保持。
|
||||
- 门禁:`cluster` 入口置灰且有原因提示,不落到 node 分支。
|
||||
- 回滚点:纯新增路由,`directEdit` 开关关闭即隐藏入口。
|
||||
|
||||
## 2. 会话状态机与互斥守卫
|
||||
|
||||
- 目标:四态语义先于几何落地,避免后续用几何调试状态。
|
||||
- 范围:`draft` / `applied` / `saved` / `cancelled` 的纯逻辑;未处理草稿时切换 `JunctionRef` 的拒绝守卫;取消复原。复用 `EditSession`,不新建第二套命令栈。
|
||||
- 验证:`npm run test:client:unit` —— draft → 应用后成为主工作区未保存约束;取消后活动副本无残留;存在未处理草稿时切换被拒绝且草稿不丢;应用后的编辑可被主地图 undo/redo 撤销。
|
||||
- 门禁:切换拒绝路径有测试;不存在自动应用或自动丢弃草稿的分支。
|
||||
- 回滚点:纯逻辑模块,revert 后回到第 1 步的只读工作区。
|
||||
|
||||
## 3. 三项可拖能力与完整预览
|
||||
|
||||
- 目标:进口宽度、cutback、单角部圆角真正可编辑,预览覆盖全部受影响对象。
|
||||
- 范围:三类手柄的渲染与拖拽;首次拖拽即 ghost;防抖后服务端局部权威预览;预览覆盖道路面、路口面、步行带、车道中心线、connector/movement、停止线、斑马线、标线;拟合失败返回最后有效几何加结构化诊断。
|
||||
- 验证:`npm run test:client:unit`(手柄事件到三个 kind 的约束值投影)、`npm run test`(局部求解与全量编译对同一约束集结果一致)、`npm run test:client`、`npm run build`;手测——三项各自可拖;预览包含上述全部对象;制造拟合失败时不把非法图形显示成已应用。
|
||||
- 门禁:预览对象清单逐项核对通过;失败态显示最后有效几何而非空白或非法图形。
|
||||
- 回滚点:按 kind 分批提交,可单独回退某一类手柄。
|
||||
|
||||
## 4. 闭环验证
|
||||
|
||||
- 目标:与主工作区和持久化链路接通。
|
||||
- 范围:应用回主地图、保存、重新编译的完整往返;`expectedDocumentVersion` 复用主地图已实现的冲突处理。
|
||||
- 验证:`npm run format:check`、`npm run test`、`npm run test:client`、`npm run test:client:unit`、`npm run build`;手测——应用后返回主地图显示同一几何;保存后重新编译几何不变且约束状态 `exact`。
|
||||
- 门禁:`prd.md` 全部验收标准勾选。
|
||||
- 回滚点:revert 后回到第 3 步的预览-only 状态。
|
||||
|
||||
## 步骤依赖
|
||||
|
||||
1 → 2 → 3 → 4 硬顺序。第 2 步先于第 3 步是刻意的:会话四态若在几何可拖之后才做,取消与互斥的缺陷会被误当成求解问题。
|
||||
41
.trellis/tasks/08-26-direct-edit-junction-tools/prd.md
Normal file
41
.trellis/tasks/08-26-direct-edit-junction-tools/prd.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 最小 JunctionTools
|
||||
|
||||
父任务:`.trellis/tasks/08-26-direct-manipulation-road-editor`。需求来源与权威合约在父任务的 `prd.md` / `design.md`。
|
||||
|
||||
## 目标
|
||||
|
||||
用专用工作区承载路口编辑,验证单一普通路口的进口宽度、cutback 与单个角部圆角,闭合"预览 → 应用到工作区 → 保存 → 取消"四态。这是路口能力的最小可信切片,不是完整路口编辑器。
|
||||
|
||||
## 顺序依赖
|
||||
|
||||
前置:`direct-edit-solver-api`(junction 相关的三个 kind 求解与预览端点)与 `direct-edit-map-editor`(`EditSession`、ghost/preview 图层、命令栈)。
|
||||
后继:无。cluster 高级编辑、多路口联动为独立后续任务。
|
||||
|
||||
## 范围
|
||||
|
||||
- 单 `JunctionRef`(`{type:'node'|'cluster', id}`,本期只走 `node`)的工作区路由与返回主地图的上下文保持。
|
||||
- 加载该路口的编辑上下文:路口面、进口、相邻道路短上下文、车道/connector、步行带、停止线、斑马线、诊断与当前约束。
|
||||
- 三项可拖能力:进口宽度、cutback、单个角部圆角。
|
||||
- session draft:首次拖拽即 ghost,防抖后服务端权威局部预览。
|
||||
- 「应用到工作区」合并有效草稿为活动副本的未保存约束;「保存」持久化;「取消」丢弃草稿并复原。
|
||||
- 会话边界:切换相邻路口前必须应用或取消。
|
||||
|
||||
## 不做
|
||||
|
||||
- cluster 的高级编辑与复合路口模板编排。
|
||||
- 多个路口的联合会话或联动编辑。
|
||||
- 控制设施的逐个手工布置。
|
||||
- 主地图道路区间的编辑(属 `direct-edit-map-editor`)。
|
||||
- OSM 图拓扑的修改。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 从路口面或诊断可进入工作区,并能带位置返回主地图。
|
||||
- [ ] 三项能力各自可拖,首次拖拽即有 ghost,防抖后显示服务端权威几何。
|
||||
- [ ] 预览包含全部受影响派生对象:道路面、路口面、步行带、车道中心线、connector/movement、停止线、斑马线、标线。
|
||||
- [ ] 拟合失败时返回最后有效几何加结构化诊断,不把非法图形显示成已应用。
|
||||
- [ ] 「应用到工作区」后返回主地图仍显示同一几何,且该编辑处于未保存状态、可被主地图 undo/redo 管理。
|
||||
- [ ] 「取消」后回到进入工作区前的状态,活动副本无残留。
|
||||
- [ ] 未处理草稿时切换相邻路口被拒绝,并提示先应用或取消。
|
||||
- [ ] 「保存」后重新编译几何不变,约束状态 `exact`。
|
||||
- [ ] `npm run format:check`、`npm run test`、`npm run test:client`、`npm run test:client:unit`、`npm run build` 全绿。
|
||||
26
.trellis/tasks/08-26-direct-edit-junction-tools/task.json
Normal file
26
.trellis/tasks/08-26-direct-edit-junction-tools/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "direct-edit-junction-tools",
|
||||
"name": "direct-edit-junction-tools",
|
||||
"title": "最小 JunctionTools",
|
||||
"description": "单一普通路口的进口宽度、cutback、单角部圆角与预览/应用/取消",
|
||||
"status": "planning",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"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": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "检查跨层数据流是否一致:约束 kind、manifest 字段、API 载荷、客户端类型不得各写一套"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "检查是否出现第二套米制换算、第二套 ID 校验或重复的 overrides 应用逻辑"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/joint-solver.md", "reason": "逐条核对约束不变量:最小车道宽、外缘不交叉、路口连续、connector 包含性"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/junction-tools.md", "reason": "核对编辑所有权与会话边界未被越过:道路 handle 不得进入 junction reserve"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "核对 v1 overrides、feature 回链字段与既有 API 行为未被破坏"}
|
||||
253
.trellis/tasks/08-26-direct-manipulation-road-editor/design.md
Normal file
253
.trellis/tasks/08-26-direct-manipulation-road-editor/design.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# 直接操纵道路编辑设计
|
||||
|
||||
> 本文是约束模型、合约与边界的唯一权威定义。`research/` 下的文档是探索记录;如与本文冲突,以本文为准。
|
||||
|
||||
## 架构边界
|
||||
|
||||
OSM、area config 快照和现有 v1 overrides 仍是道路模型的基线。`native-road-edits/v2` 记录 OSM 语义锚点上的约束与操作,不保存最终 GeoJSON。
|
||||
|
||||
```text
|
||||
OSM + area config snapshot + v1 overrides + v2 direct edits
|
||||
-> compileRoadModel
|
||||
-> applyRoadOverrides
|
||||
-> resolveDirectEditConstraints
|
||||
-> editable profiles + junction plans + handle manifest
|
||||
-> compileGeometry -> GeoJSON / diagnostics / layers
|
||||
```
|
||||
|
||||
预览与正式编译共用 `resolveDirectEditConstraints` 与 `compileGeometry`;浏览器不实现第二套道路几何算法。
|
||||
|
||||
## 约束模型
|
||||
|
||||
锚点类型(约束挂在哪里)与约束 kind(约束什么)是两个维度。此前 `research/data-model-options.md` 与本文混用二者,导致两套命名冲突,此处统一。
|
||||
|
||||
```ts
|
||||
type Side = 'left' | 'right'
|
||||
|
||||
type SemanticAnchor =
|
||||
| { type: 'road-station'; roadId: string; station: number; side?: Side }
|
||||
| { type: 'road-interval'; roadId: string; startStation: number; endStation: number; side?: Side }
|
||||
| { type: 'junction-approach'; nodeId: string; segmentId: string; side?: Side }
|
||||
| { type: 'junction-corner'; nodeId: string; incomingRoadId: string; outgoingRoadId: string }
|
||||
|
||||
type RoadConstraintKind =
|
||||
| 'road-edge-offset'
|
||||
| 'road-sidewalk-width'
|
||||
| 'road-lane-divider'
|
||||
| 'junction-approach-width'
|
||||
| 'junction-cutback'
|
||||
| 'junction-corner-radius'
|
||||
```
|
||||
|
||||
| kind | 锚点 | value | 单位与含义 | 所有者 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `road-edge-offset` | `road-interval` | `{ offsetMeters, transition }` | 米,相对基线外缘的法向偏移 | 主地图 |
|
||||
| `road-sidewalk-width` | `road-interval` | `{ widthMeters, transition }` | 米,`0` 表示关闭步行带 | 主地图 |
|
||||
| `road-lane-divider` | `road-interval` | `{ boundaryIndex, offsetMeters, transition }` | 米,相对中心线的带符号横向偏移(左负右正) | 主地图 |
|
||||
| `junction-approach-width` | `junction-approach` | `{ widthMeters }` | 米 | JunctionTools |
|
||||
| `junction-cutback` | `junction-approach` | `{ cutbackMeters }` | 米 | JunctionTools |
|
||||
| `junction-corner-radius` | `junction-corner` | `{ radiusMeters }` | 米 | JunctionTools |
|
||||
|
||||
这 6 个 kind 与 PRD 首期范围一一对应(外缘、步行带、车道分隔 + 进口、cutback、角部),没有多余项也没有缺口。
|
||||
|
||||
`transition: 'smoothstep' | 'linear'`,默认 `smoothstep`,作用于 interval 两端回归基线的过渡段。
|
||||
|
||||
`boundaryIndex` 从左外缘起 1-based,有效范围 `1..laneCount-1`。重放时若车道数变化导致越界,该约束转 `stale`,不做近似映射。
|
||||
|
||||
`station` 是相对道路中心线的归一化弧长 `0..1`,不是绝对经纬度或数组下标。保存时同时写 `anchorSnapshot`(当时坐标、切线方位、道路长度、相邻 OSM node ID)作为重定位与冲突检测证据。
|
||||
|
||||
道路横断面拖拽默认写 `road-interval`:拖拽位置为区间中心,求解器按道路长度与两端 junction reserve 推导初始 `startStation` / `endStation`;用户用范围手柄只更新该区间,不存储鼠标轨迹。
|
||||
|
||||
## Handle manifest
|
||||
|
||||
服务端从同一语义模型生成手柄清单,客户端不自行推导手柄位置或可拖方向。
|
||||
|
||||
```ts
|
||||
interface HandleManifest {
|
||||
schema: 'road-edit-handles/v1'
|
||||
revisionId: string
|
||||
previewSeq: number
|
||||
handles: EditHandle[]
|
||||
reserves: JunctionReserve[]
|
||||
}
|
||||
|
||||
interface EditHandle {
|
||||
handleId: string
|
||||
kind: RoadConstraintKind
|
||||
anchor: SemanticAnchor
|
||||
position: [number, number] // EPSG:4326
|
||||
axisAzimuth: number // 允许拖拽的方向,度,相对真北
|
||||
value: { current: number; min: number; max: number; unit: 'meter' }
|
||||
constraintId?: string // 已有约束回链
|
||||
affects: string[] // 受影响派生 feature 的 native_id
|
||||
editable: boolean
|
||||
disabledReason?: string
|
||||
}
|
||||
|
||||
interface JunctionReserve {
|
||||
nodeId: string
|
||||
roadId: string
|
||||
fromStation: number
|
||||
toStation: number
|
||||
}
|
||||
```
|
||||
|
||||
手柄 feature 的 properties 只携带 `handleId`,语义通过 manifest 反查。`reserves` 是主地图禁入区:落在其中的道路手柄必须 `editable: false` 且给出 `disabledReason`,引导用户进入 `JunctionTools`。`affects` 用于拖拽时高亮将被同步改变的对象,满足 PRD 第 2 条。
|
||||
|
||||
## 坐标与单位分层
|
||||
|
||||
持久化层只有米和归一化 station,没有像素、没有度。
|
||||
|
||||
| 层 | 坐标 / 单位 | 规则 |
|
||||
| --- | --- | --- |
|
||||
| 持久化约束 | 米 + station `0..1` | 禁止写入经纬度或像素 |
|
||||
| 服务端求解 | 局部米制框架 | 复用 `src/geometry/lane-geometry.js` 的 `metersAt()`、`haversineMeters()`、`projectedDistanceAlong()`、`lateralOffsetFrom()`,不新增第二套换算 |
|
||||
| 传输 | EPSG:4326 + 方位角 | manifest 与预览 GeoJSON 一律 4326 |
|
||||
| 客户端渲染 | EPSG:3857 | 沿用 `workbench/client/src/map/layers.ts` 现有 `dataProjection: 'EPSG:4326'` / `featureProjection: 'EPSG:3857'` |
|
||||
|
||||
客户端把指针位移换成米时必须先转回 4326 再用球面距离,禁止拿 3857 坐标差当米:3857 在纬度 φ 处有 `1/cos φ` 放大,直接相减会让同一次拖拽在不同纬度得到不同结果。
|
||||
|
||||
## 预览时序与延迟预算
|
||||
|
||||
```text
|
||||
pointer down -> 读 handle 的 anchor,建 draft operation
|
||||
pointer move -> 立即更新客户端 ghost + 递增 previewSeq 的防抖请求
|
||||
pointer up -> 强制发一次非防抖的最终请求
|
||||
```
|
||||
|
||||
- 防抖:拖拽中 80ms trailing;`pointerup` 不防抖。
|
||||
- 时序:每个会话的 `previewSeq` 单调递增,响应回显它。客户端丢弃 `previewSeq` 小于已应用最大值的响应,并在发新请求时用 `AbortController` 取消在途请求。乱序响应绝不允许覆盖较新的预览。
|
||||
- 延迟预算:单道路 / 单路口局部求解 p95 ≤ 300ms。超预算仍返回结果但标 `degraded: true`,客户端保留 ghost 与待定状态,不做几何闪烁。
|
||||
- 无效草稿返回 `diagnostics` 且不替换最后一个有效预览。
|
||||
|
||||
## 文档、活动副本与 revision
|
||||
|
||||
活动工作副本包含 v1 overrides、v2 edits、area config 快照和信号数据。导入 OSM 自动创建不可变基线 revision;用户命名检查点时冻结所有输入副本、hash、compiler identity 和可选产物。普通保存与预览只更新活动副本。
|
||||
|
||||
```ts
|
||||
interface RoadEditDocument {
|
||||
schema: 'native-road-edits/v2'
|
||||
documentVersion: number
|
||||
base: { osmSha256: string; areaConfigSha256: string; compilerGeometryVersion: string }
|
||||
constraints: RoadConstraint[]
|
||||
operations: RoadEditOperation[]
|
||||
}
|
||||
|
||||
interface RoadConstraint {
|
||||
id: string
|
||||
kind: RoadConstraintKind
|
||||
anchor: SemanticAnchor
|
||||
anchorSnapshot: AnchorSnapshot
|
||||
value: unknown
|
||||
enabled: boolean
|
||||
status: ConstraintStatus
|
||||
provenance: { operationId: string; createdAt: string; author?: string }
|
||||
}
|
||||
|
||||
type ConstraintStatus = 'exact' | 'recheck' | 'pending' | 'conflicted' | 'stale'
|
||||
```
|
||||
|
||||
| status | 含义 | 是否参与求解 |
|
||||
| --- | --- | --- |
|
||||
| `exact` | 锚点 ID 精确匹配 | 是 |
|
||||
| `recheck` | 锚点匹配,但编译器几何版本已变 | 是,且需用户显式确认 |
|
||||
| `pending` | 需按 snapshot 重定位的候选匹配 | 否,等用户确认 |
|
||||
| `conflicted` | 候选不唯一或偏差超阈值 | 否 |
|
||||
| `stale` | 锚点已不存在,或参数越界 | 否 |
|
||||
|
||||
重导入先按 `roadId` / `segmentId` / `nodeId` 精确匹配;失败才用 `anchorSnapshot` 的 OSM node、距离与切线找候选。绝不静默模糊应用。
|
||||
|
||||
## 存储布局与迁移
|
||||
|
||||
现状是每次导入创建一个 `workbench-data/import-<id>/` 目录(`workbench/server.js` 用 `fs.mkdtempSync`),内含 `source.osm`、`native-road-overrides.json`、`native-traffic-signals.json`、`outputs/`。新结构在该目录内扩展,不改动既有文件语义:
|
||||
|
||||
```text
|
||||
workbench-data/import-<id>/
|
||||
source.osm 保留,v1 路径兼容
|
||||
native-road-overrides.json 保留,仍是 v1 覆盖的权威文件
|
||||
native-traffic-signals.json 保留
|
||||
outputs/ 保留
|
||||
osm/<sha256>.osm 内容寻址的 OSM 副本,多 revision 共享
|
||||
active/
|
||||
native-road-edits.json v2 直接编辑文档
|
||||
area-config.snapshot.json 区域配置快照(含 nativeRoad.junctionTemplates)
|
||||
state.json documentVersion、activeRevisionId
|
||||
revisions/
|
||||
rev-0001/manifest.json 导入基线
|
||||
rev-0002/manifest.json 命名检查点
|
||||
```
|
||||
|
||||
迁移是惰性的:新版本首次打开既有 import 目录时创建 `active/`,并把当前 `source.osm` + overrides + signals + area config 冻结为 `rev-0001`。不移动、不重写任何既有文件,旧版本仍能读原路径。
|
||||
|
||||
留存:revision 永不自动删除。OSM 内容寻址后,重复导入同一文件不产生副本。`outputs/` 缓存可显式清理,清理不影响可复现性——复现来源始终是输入文档加 compiler identity。
|
||||
|
||||
## area config 所有权
|
||||
|
||||
`nativeRoad.junctionTemplates` 目前住在项目级区域配置文件里,`/api/junction-clusters` 直接改写它(`workbench/server.js:224-243`),旧客户端还提示用户手工复制回配置文件。这让 revision 不可复现:同一份 OSM + overrides 在不同 area config 下编译结果不同。
|
||||
|
||||
规则:
|
||||
|
||||
- 每个 revision 与活动副本各持有一份 `area-config.snapshot.json`。
|
||||
- 编译与预览只读快照,不在编译期读外部配置文件。
|
||||
- `/api/junction-clusters` 改为写活动副本快照,响应仍返回可复制到项目配置的片段,保留现有导出提示的价值。
|
||||
- `RoadEditDocument.base.areaConfigSha256` 指向快照。
|
||||
- cluster 的高级编辑延后,但快照合约现在建立,避免最小 JunctionTools 落地后返工。
|
||||
|
||||
## 编译器几何版本变更
|
||||
|
||||
`base.compilerGeometryVersion` 与当前不一致时,约束仍按精确锚点重放,但全部标 `recheck` 并要求一次显式"确认重放";不自动改值,也不自动判失效。rebase 响应必须给出各 status 的计数与逐条明细。
|
||||
|
||||
## 编辑所有权
|
||||
|
||||
主地图 Road editor 只编辑两个 junction reserve 之间的道路内部 interval。reserve 内的 approach width、transition、cutback 和 corner 由 `JunctionTools` 独占。同一进口上 `junction-approach-width` 显式约束优先于道路 profile,求解器负责边界连续。
|
||||
|
||||
`JunctionTools` 是单 `JunctionRef` session(`{type:'node'|'cluster', id}`):拖拽立即 ghost,服务端返回完整局部派生图层;"应用到工作区"把有效草稿合并为活动副本的未保存约束,"保存"才持久化,"取消"回到进入前状态。切换相邻路口前必须应用或取消。首个交付仅支持普通 node junction 的进口宽度、cutback 与一个角部圆角。
|
||||
|
||||
## 不变量
|
||||
|
||||
- 基线 OSM topology 与 centerline 不由直接编辑改变。
|
||||
- 单车道最小宽度 2.4m;横断面总宽等于车道、边缘与步行带之和。
|
||||
- 同一站点左右外缘不得交叉;相邻 profile 之间必须有可计算的过渡。
|
||||
- 进口截面在 cutback 处与路口边界连续;路口面不自交。
|
||||
- connector、停止线、人行横道必须落在所属道路 / 路口可用面内,否则产生阻塞性诊断。
|
||||
- 无效草稿不替换最后一个有效预览。
|
||||
- 每次提交到活动副本是单个可撤销 operation;已保存编辑的撤销追加反向操作或禁用约束,历史不重写。
|
||||
- 普通路口锚定 node,复杂 cluster 锚定 cluster 与 arm,二者不共用低层约束。
|
||||
|
||||
## API 合约
|
||||
|
||||
新增端点:
|
||||
|
||||
| 端点 | 行为 |
|
||||
| --- | --- |
|
||||
| `GET /api/edit-state` | 活动文档、`documentVersion`、revision 元数据、约束状态、handle manifest |
|
||||
| `POST /api/edit-preview` | 传 `previewSeq` + draft operations,返回局部 preview 图层、diagnostics、manifest、`degraded`;不写文件 |
|
||||
| `POST /api/edits` | 校验并保存活动 v2 文档,要求 `expectedDocumentVersion` |
|
||||
| `POST /api/revisions` | 从活动副本创建命名不可变 revision |
|
||||
| `POST /api/revisions/:id/rebase` | 对目标 revision 显式重放,返回各 status 计数与明细 |
|
||||
|
||||
单写者保护:`POST /api/edits` 的 `expectedDocumentVersion` 与服务端不一致时返回 409 与当前版本,不写入。这挡住同机多标签页的静默互相覆盖;多人协作仍延后。
|
||||
|
||||
现有端点的变化(字段只增不改):
|
||||
|
||||
- `/api/state`、`/api/session` 增量返回 `activeRevisionId` 与 `documentVersion`。
|
||||
- `/api/import` 导入成功后自动创建基线 revision,返回其 id。
|
||||
- `/api/overrides` 保持 v1 语义;只有无法用 v1 表达的局部编辑进入 v2。
|
||||
- `/api/junction-clusters` 改写活动副本快照而非外部配置文件。
|
||||
- 所有写入沿用现有 staging 目录原子替换模式。
|
||||
|
||||
## 上线与回滚形态
|
||||
|
||||
客户端编辑能力全程挂在 `directEdit` 开关后,默认关闭,直到交付级验收标准全部通过。开关关闭时工作台行为与当前 main 完全一致:不注册编辑 interaction、不请求 manifest、不创建 handles/ghost/preview 图层。
|
||||
|
||||
服务端是纯增量:新端点独立于既有路由;`active/` 与 `revisions/` 不存在时旧代码路径照常工作;v1 `native-road-overrides.json` 始终保持权威且格式不变。因此任何一步回滚都不会让既有 `import-*` 目录无法打开,也不需要数据迁移回退脚本。
|
||||
|
||||
## 客户端边界
|
||||
|
||||
`workbench/client/index.html` 只加载 `src/main.tsx`,`sendWorkbenchApp()` 优先 `dist/index.html` 并回退到同一个 index.html。因此遗留的 `workbench/client/app.js`(约 1400 行 vanilla)已不是任何入口。本任务只在 React 应用内实现编辑能力,不同步 app.js;删除它另开任务。
|
||||
|
||||
客户端目前没有单元测试运行器(`test:client` 只是 `tsc --noEmit`)。本任务引入 `vitest`(node 环境)覆盖纯逻辑:EditSession 命令栈与 undo/redo、`previewSeq` 乱序丢弃、handle 事件到约束值的投影、米制换算。OpenLayers 地图行为(不重建 Map、图层增量替换)由 `implement.md` 的手测清单覆盖。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "本特性横跨 compiler / server / client 三层,新增 JSONL 文档、API 载荷与配置字段,正是该指南的触发条件"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "米制换算必须复用 src/geometry/lane-geometry.js,v1 overrides 与 ID 校验必须复用而非另建一套"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "现有可复用锚点、feature 回链字段与当前缺口清单"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/joint-solver.md", "reason": "求解阶段的插入点、依赖链顺序与约束不变量来源"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/canvas-integration.md", "reason": "输入 adapter 的候选、ol-ext 探针退出条件与回退优先级"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/revisions.md", "reason": "revision manifest 字段与重导入冲突体验的来源"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/junction-tools.md", "reason": "JunctionTools 的会话边界、预览/应用/保存状态与路口所有权划分"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/data-model-options.md", "reason": "三种数据模型的比较背景与选择理由;kind 枚举以 design.md 为准"}
|
||||
@@ -0,0 +1,107 @@
|
||||
# 实施计划
|
||||
|
||||
每步是一个独立提交,带自己的验证命令、门禁和回滚点。门禁不过就停在该步,不带着已知缺陷进入下一步。
|
||||
|
||||
约束与合约的权威定义在 `design.md`;本文只管执行顺序与验证。
|
||||
|
||||
## 全局回滚策略
|
||||
|
||||
- 任务分支上一步一提交,回滚等于 `git revert` 该提交,不做跨步大回滚。
|
||||
- 客户端编辑能力全程挂在 `directEdit` 开关后,默认关闭,直到第 8 步手测清单通过才默认开启。开关关闭时工作台行为与当前 main 完全一致。
|
||||
- 服务端新增端点与新文件是纯增量:`active/` 与 `revisions/` 不存在时,旧代码路径照常工作。任何一步回滚都不会让既有 import 目录无法打开。
|
||||
- v1 `native-road-overrides.json` 始终保持权威且格式不变,任何步骤都不迁移它。
|
||||
|
||||
## 0. 前置:依赖与测试基础设施
|
||||
|
||||
- 目标:先把验证能力建好,避免后续步骤"写完没法测"。
|
||||
- 范围:`package.json` 增加 `vitest`(固定版本)与 `test:client:unit` 脚本;建立客户端纯逻辑测试目录。不碰任何产品代码。
|
||||
- 验证:`npm run test:client:unit`(空套件通过)、`npm run format:check`、`npm run test`。
|
||||
- 门禁:新脚本可跑通且不影响现有 `test` / `test:client` / `build`。
|
||||
- 回滚点:仅 `package.json` 与测试目录,revert 无副作用。
|
||||
|
||||
## 1. `ol-ext` 限时探针
|
||||
|
||||
- 目标:只回答一个问题——能否复用通用 handle 的命中、pointer 生命周期与视觉反馈。
|
||||
- 范围:隔离探针,不修改生产基线图层,不进依赖清单直到通过。
|
||||
- 验证:手动跑探针页;确认 OL `Map` 未重建、proxy 拖拽稳定、基线 source 未被写入、事件能转成 draft 值。
|
||||
- 门禁:三条同时成立才把 `ol-ext` 固定为依赖。超过一个工作日或任一条不成立,立刻停止排障。
|
||||
- 回滚点:删除探针目录,改用原生 OL `Snap` + 小型 `PointerInteraction` adapter。`HandleManifest -> RoadEditOperation -> RoadConstraint -> preview solver` 合约不变,因此本步失败最多换输入层,不动 schema、求解器或已保存编辑。
|
||||
|
||||
## 2. v2 文档、活动副本与 revision 基础
|
||||
|
||||
- 目标:落地 `native-road-edits/v2`、operations、`anchorSnapshot` 与 5 态重放状态机。
|
||||
- 范围:新增文档读写、`active/` 与 `revisions/` 布局、内容寻址 OSM 副本、`documentVersion`、命名检查点与恢复读取。惰性迁移既有 import 目录,不移动既有文件。v1 overrides 完全不动。
|
||||
- 验证:`npm run test`(新增 fixture:文档往返、`documentVersion` 递增、惰性迁移在只有旧文件的目录上生成 `rev-0001`、内容相同的 OSM 不产生第二份副本)。
|
||||
- 门禁:既有 `workbench-data/import-*` 目录在新代码下可正常打开,且未被改写;schema 校验拒绝越界 `boundaryIndex` 与非法 station。
|
||||
- 回滚点:revert 本步后 `active/` 与 `revisions/` 只是残留目录,旧代码忽略它们照常运行。
|
||||
|
||||
## 3. area config 快照
|
||||
|
||||
- 目标:切断编译对外部区域配置文件的运行期依赖,让 revision 真正可复现。
|
||||
- 范围:活动副本与每个 revision 各写一份 `area-config.snapshot.json`;编译与预览改读快照;`/api/junction-clusters` 改写快照并在响应里返回可复制片段。
|
||||
- 验证:`npm run test` 新增 fixture——冻结 revision 后修改外部 area config,重新编译该 revision 结果不变;快照缺失时给出明确错误而非静默用外部文件。
|
||||
- 门禁:现有 `/api/junction-clusters` 的用户可见行为(返回可复制片段)不退化。
|
||||
- 回滚点:revert 后编译回到读外部配置文件;已写的快照文件被忽略,不影响 v1 链路。
|
||||
|
||||
## 4. 约束求解边界
|
||||
|
||||
- 目标:在 `compileGeometry()` 前插入纯函数 `resolveDirectEditConstraints`,输出 editable profiles、junction plans、handle manifest 与 diagnostics。
|
||||
- 范围:实现 6 个 kind 的求解;interval 默认范围推导(避开两端 reserve);junction reserve 计算;`junction-approach-width` 对道路 profile 的优先级与边界连续过渡。米制换算复用 `src/geometry/lane-geometry.js`,不新增第二套。
|
||||
- 验证:`npm run test` 新增 fixture——每个 kind 的重放、最小车道宽 2.4m、左右外缘不交叉、道路/路口连续、路口面不自交、connector 包含性、`boundaryIndex` 越界转 `stale`、锚点缺失/歧义分别转 `stale`/`conflicted`。
|
||||
- 门禁:求解是纯函数,无文件写入、无网络;`npm run test` 全绿且既有 baseline 输出在无 v2 约束时逐字节不变。
|
||||
- 回滚点:无 v2 约束时该步是恒等变换,因此 revert 前后编译输出一致,可安全单独回退。
|
||||
|
||||
## 5. 预览与编辑 API
|
||||
|
||||
- 目标:草稿预览与持久化分离,预览绝不写文件。
|
||||
- 范围:`GET /api/edit-state`、`POST /api/edit-preview`、`POST /api/edits`、`POST /api/revisions`、`POST /api/revisions/:id/rebase`;`previewSeq` 回显;`degraded` 标记;`expectedDocumentVersion` 前置条件与 409;`/api/state`、`/api/session`、`/api/import` 的增量字段。
|
||||
- 验证:`npm run test` 新增 server integration——预览调用前后目录 mtime 与内容不变;`expectedDocumentVersion` 不匹配返回 409 且不写入;原子写入在中途失败时不留半份文件;rebase 返回各 status 计数;预览与正式编译对同一约束集给出相同几何。
|
||||
- 门禁:预览零写入;409 路径有测试覆盖;现有 `/api/overrides`、`/api/compile`、`/api/state` 的既有字段与行为不变。
|
||||
- 回滚点:新端点是增量,revert 后客户端开关已关闭,工作台回到 v1 行为。
|
||||
|
||||
## 6. 主地图 Road editor
|
||||
|
||||
- 目标:外缘、步行带、车道分隔与区间范围可拖,禁入 junction reserve。
|
||||
- 范围:selection → handle manifest;handles / ghost / preview 三个独立 source;`EditSession` 命令栈与 undo/redo;80ms 防抖与 `pointerup` 最终请求;`previewSeq` 乱序丢弃与 `AbortController` 取消;无效草稿反馈;应用/保存/取消。全部挂在 `directEdit` 开关后。
|
||||
- 验证:`npm run test:client`、`npm run test:client:unit`(命令栈与 undo/redo、乱序响应丢弃、handle 事件到约束值的投影、米制换算的纬度正确性)、`npm run build`。
|
||||
- 门禁:单元测试覆盖上述四类纯逻辑;拖拽期间 `Map` 未重建且只有受影响 source 被替换(手测清单第 1-3 项)。
|
||||
- 回滚点:关掉 `directEdit` 开关即可现场止损,无需回滚代码;必要时 revert 本步。
|
||||
|
||||
## 7. 最小 JunctionTools
|
||||
|
||||
- 目标:单一普通 node 路口的进口宽度、cutback、单角部圆角,闭合预览/应用/取消。
|
||||
- 范围:单 `JunctionRef` 会话的路由与返回主地图的上下文;session draft;完整局部预览;应用合并到活动副本;取消回到进入前状态;切换路口前强制应用或取消。不做 cluster 高级编辑、不做多路口联动。
|
||||
- 验证:`npm run test:client:unit`(会话状态机:draft → 应用 → 主工作区未保存约束;取消后状态复原;未处理草稿时切换被拒绝)、`npm run test`(局部求解与全量编译一致)、`npm run build`。
|
||||
- 门禁:预览包含全部受影响派生对象(道路面、路口面、步行带、车道中心线、connector、停止线、斑马线、标线);拟合失败时返回最后有效几何加结构化诊断,不把非法图形显示成已应用。
|
||||
- 回滚点:路由与开关独立,revert 不影响第 6 步的主地图编辑能力。
|
||||
|
||||
## 8. 全量验证与开关默认开启
|
||||
|
||||
- 目标:跑完交付级验收标准,确认可默认开启 `directEdit`。
|
||||
- 范围:补齐 `prd.md`「验收标准(交付阶段)」逐条证据;开关默认值改为开启。
|
||||
- 验证:`npm run format:check`、`npm run test`、`npm run test:client`、`npm run test:client:unit`、`npm run build`,加下方手测清单。
|
||||
- 门禁:交付级验收标准全部勾选;任一条不过则开关保持关闭,任务不进入 Phase 3。
|
||||
- 回滚点:只改开关默认值,回滚成本为一行。
|
||||
|
||||
## 手测清单
|
||||
|
||||
自动化测试覆盖不到的地图行为,每次进入第 8 步都要跑一遍:
|
||||
|
||||
1. 选中道路 → 出现手柄,浏览器 devtools 中 `Map` 实例未变(拖拽前后同一对象引用)。
|
||||
2. 拖动外缘 → 只有道路面、步行带、车道线、标线、connector 的 source 被替换,底图与未受影响图层无重绘闪烁。
|
||||
3. 连续快速拖拽后松手 → 最终几何与松手位置一致,无回跳(验证乱序丢弃与最终请求)。
|
||||
4. 拖到违反最小车道宽 → 出现阻塞诊断,地图停在最后一个有效预览。
|
||||
5. 手柄落在 junction reserve 内 → 不可拖动并提示进入 `JunctionTools`。
|
||||
6. 进入 `JunctionTools` 调三项 → 预览完整;应用 → 返回主地图仍显示同一几何;取消 → 状态复原。
|
||||
7. 未处理草稿时尝试切换相邻路口 → 被拒绝并提示先应用或取消。
|
||||
8. 保存 → 重新编译 → 几何不变,约束状态 `exact`。
|
||||
9. 重新导入同一份 OSM → 约束全部 `exact`;导入删掉某条道路的 OSM → 相关约束 `stale`,旧 revision 仍可打开复现原结果。
|
||||
10. 两个标签页同时保存 → 后者 409 提示,前者结果保留。
|
||||
11. 关闭 `directEdit` 开关 → 工作台行为与当前 main 一致。
|
||||
|
||||
## 步骤依赖
|
||||
|
||||
0 → 1 可并行于 2;2 → 3 → 4 → 5 是硬顺序;6 依赖 1、4、5;7 依赖 6;8 依赖全部。第 1 步失败不阻塞 2-5,只改变第 6 步的输入 adapter 选择。
|
||||
|
||||
|
||||
|
||||
107
.trellis/tasks/08-26-direct-manipulation-road-editor/prd.md
Normal file
107
.trellis/tasks/08-26-direct-manipulation-road-editor/prd.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 直接操纵道路编辑工作台
|
||||
|
||||
## 目标
|
||||
|
||||
让道路编译工作台支持类似 Drawtonomy 的直接操纵:用户能在地图上选择生成的道路面并通过有语义的控制柄调整细节,同时保留 OSM 作为输入来源、修改可解释且可审计,并能在重新编译或重新导入后可靠地重放。
|
||||
|
||||
## 已确认的事实
|
||||
|
||||
- `src/compile/native-road.js` 从 OSM 生成方向道路、车道、道路面、步行带、路口面、标线和连接路径;这些 GeoJSON 是派生输出,不是持久化编辑源。
|
||||
- 现有 `native-road-overrides/v1` 已支持道路宽度/车道数/步行带开关、路口和车道连接、以及标线样式,且以稳定的 OSM 派生语义 ID 定位目标,并在 OSM 改动后识别失效项。
|
||||
- 编译器使用 staging 目录原子发布输出;工作台已经具有单例 OpenLayers 地图、命令式图层 source 更新、选择高亮和暂存/保存/重新编译链路。
|
||||
- Drawtonomy 的公开 SDK 表明其道路由共享 `point`、`linestring` 和引用左右边界的 `lane` 构成;连接关系和导出几何从该对象图派生。其编辑器核心(手势状态机、选择、历史)不在公开仓库中,不能作为源码依赖。
|
||||
|
||||
## 产品要求
|
||||
|
||||
1. 编辑目标必须是道路语义对象,而不是直接保存最终 GeoJSON 多边形。
|
||||
2. 拖拽时必须即时显示预览,并清楚呈现将被同步影响的道路面、车道、标线和路口对象。
|
||||
3. 每次持久化修改都必须可解释:谁在何处、以什么规则、相对什么输入锚点施加了何种约束。
|
||||
4. OSM 重新导入或编译器升级后,修改应自动重放、明确报告冲突/失效,绝不能静默改写到错误道路。
|
||||
5. 用户必须可以撤销/重做未保存编辑,并能查看、禁用或删除已保存编辑。
|
||||
6. 新架构应复用现有道路参数 overrides、ID 校验、编译和图层更新能力,而不是并行维护第二套地图数据。
|
||||
7. OSM 道路中心线和拓扑保持只读;直接操纵首期仅编辑由它派生的横断面与路口细节。
|
||||
8. 首期必须同时验证道路横断面与路口细节的联合编辑:道路外缘、步行带、车道分隔,以及路口进口、cutback 与转弯角部都属于首批能力。
|
||||
9. 道路横断面拖拽默认生成以拖拽站点为中心的局部区间,并在区间边界平滑过渡回基线;用户可以用范围控制柄修改影响区间。
|
||||
10. 拖拽时浏览器立即更新控制柄、辅助线与半透明 ghost;在短防抖后调用服务端权威预览,服务端使用与正式编译相同的约束求解器返回受影响派生图层。浏览器不维护第二套道路几何算法。
|
||||
11. 工作台必须支持可复现的存盘 revision:冻结一次编译所需的 OSM 输入、区域配置、已有 overrides、直接编辑约束、信号数据和编译身份;重新导入 OSM 创建新 revision,不覆盖旧 revision。
|
||||
12. 导入 OSM 自动创建基线 revision;用户显式保存命名检查点时创建不可变 revision;普通保存和预览只更新活动副本。
|
||||
13. 路口点击编辑进入专用 `JunctionTools` 工作区,而非在主地图叠加完整路口控制面板;该工作区复用活动 revision 与直接编辑约束,并在操作后尽快重新求解生成道路拓扑和相关派生图层。
|
||||
14. `JunctionTools` 的草稿从首次拖拽起即显示即时 ghost 和服务端权威几何预览;“应用到工作区”仅合并有效草稿到主工作区,“保存”才持久化,“取消”丢弃本次会话草稿。
|
||||
15. 一次 `JunctionTools` 会话只编辑一个 node 或 cluster;切换相邻路口前必须应用或取消当前草稿。
|
||||
16. 主地图道路区间编辑仅拥有两个路口之间的内部区间;`JunctionTools` 独占路口保留区内的进口、cutback 与角部约束。求解器保证两侧连续,并以显式 junction approach 约束优先。
|
||||
17. 首个 `JunctionTools` 交付只验证单一普通路口的进口宽度、cutback 和单个角部圆角;必须展示完整拟合预览并能应用回主工作区。
|
||||
18. 约束 kind 枚举必须唯一且与首期范围一一对应;车道分隔必须有自己的约束 kind,不得借用外缘偏移表达。
|
||||
19. 预览必须有明确时序保证:乱序响应不得覆盖较新预览;拖拽防抖与 `pointerup` 最终请求分离;超出延迟预算时降级提示而不是几何闪烁。
|
||||
20. 持久化约束只使用米与归一化 station;坐标与单位换算的分层职责必须固定,客户端不得用投影坐标差充当米。
|
||||
21. 区域配置(含 `nativeRoad.junctionTemplates`)必须随活动副本与 revision 冻结为快照,编译与预览只读快照;否则 revision 不可复现。
|
||||
22. 同机多标签页并发保存必须被显式挡住(版本前置条件加冲突提示),不得静默互相覆盖。多人协作仍延后。
|
||||
23. revision 不自动删除;OSM 副本按内容寻址避免重复;派生产物缓存可清理且清理不影响可复现性。
|
||||
24. 编译器几何版本变化时,约束既不自动失效也不自动改值,必须经用户显式确认重放。
|
||||
25. 编辑能力只进 React 工作台;遗留 vanilla 客户端不同步、不在本任务内删除。
|
||||
26. 客户端纯逻辑必须可自动化测试;地图交互行为以固化在 `implement.md` 的手测清单覆盖。
|
||||
|
||||
## 需要完成的研究与设计
|
||||
|
||||
- 梳理当前模型、输出 feature 属性和现有 overrides 可直接复用的锚点。
|
||||
- 明确 Drawtonomy 的公开对象图、共享几何和派生原则,以及它与 OSM 编译流程不相同的边界。
|
||||
- 比较参数反推、语义几何约束、局部补丁几何三种数据模型,并给出持久化、重放、冲突、撤销和迁移策略。
|
||||
- 在确认编辑边界后,形成交互模型、编译边界、API 合约、版本化 schema 和分阶段实施计划。
|
||||
- 在实现前验证 `ol-ext` 的 Transform interaction 能否仅操作临时代理 feature,并与现有 `ol@10.10.0`、React 生命周期和局部预览稳定协作;验证结果决定是否纳入正式依赖。
|
||||
- `ol-ext` 验证必须是有时间上限、可独立删除的探针;失败仅替换输入 adapter,不得改变约束文档、求解器或编译架构。
|
||||
- `ol-ext` 探针限时一个工作日;必须同时满足“无地图重建、无基线 source 写入、拖拽稳定”,否则停止排障并采用 OpenLayers 原生 proxy + 小型语义 adapter。
|
||||
|
||||
## 任务地图
|
||||
|
||||
本任务是父任务:持有需求集、`design.md` 权威合约、跨子任务验收标准与最终集成验证。实现落在四个子任务,顺序依赖写在各自 `prd.md` / `implement.md`,不由树结构隐含。
|
||||
|
||||
| 子任务 | 交付物 | 对应父 `implement.md` 步骤 | 前置 |
|
||||
| --- | --- | --- | --- |
|
||||
| `08-26-direct-edit-documents` | v2 文档、活动副本、revision、area config 快照、测试基础设施 | 0、2、3 | 无 |
|
||||
| `08-26-direct-edit-solver-api` | `resolveDirectEditConstraints` 与预览/保存/revision/rebase API | 4、5 | documents |
|
||||
| `08-26-direct-edit-map-editor` | `ol-ext` 探针与主地图道路区间编辑 | 1、6 | solver-api 第 2 步(manifest) |
|
||||
| `08-26-direct-edit-junction-tools` | 单一普通路口的进口、cutback、单角部圆角 | 7 | solver-api、map-editor |
|
||||
|
||||
父任务自留第 8 步:跑完交付级验收标准并把 `directEdit` 开关默认开启。
|
||||
|
||||
## 延后项
|
||||
|
||||
- 不在当前前置架构阶段设计或实现高级复合路口模板编排、多个路口的联合会话、控制设施逐个手工布置、协作合并或全面的拓扑创作工具。
|
||||
|
||||
## 首个交付范围
|
||||
|
||||
- 版本化直接编辑文档、活动工作副本和命名 revision 检查点。
|
||||
- 限时的 `ol-ext` 代理 feature 验证及原生 OpenLayers adapter 回退。
|
||||
- 服务端权威预览 API 与客户端即时 ghost。
|
||||
- 主地图的道路内部区间:外缘、步行带、车道分隔和范围控制。
|
||||
- 最小 `JunctionTools`:单一普通路口的进口、cutback、单角部圆角、预览/应用/取消。
|
||||
- 约束重放的精确匹配、待确认和失效状态;旧 revision 永不被重导入覆盖。
|
||||
|
||||
## 验收标准(规划阶段)
|
||||
|
||||
- [x] 已提供当前能力与可复用边界的证据清单。
|
||||
- [x] 已区分 Drawtonomy 的公开事实、可迁移原则和不可验证的编辑器内部实现。
|
||||
- [x] 已确定持久化编辑数据不是最终 GeoJSON,并有版本化、可重放、可失效诊断的数据方案。
|
||||
- [x] 已明确拖拽预览、应用、保存、重新编译、撤销/重做、OSM 重导入和 revision 检查点的行为。
|
||||
- [x] 已定义 `ol-ext` 限时探针、原生 OpenLayers 回退与不改变数据合同的止损规则。
|
||||
- [x] 已定义首个 Road editor 与最小 JunctionTools 的范围、所有权和延后项。
|
||||
- [x] 约束 kind 枚举唯一,且覆盖首期全部 6 项能力(含车道分隔)。
|
||||
- [x] handle manifest、坐标分层、预览时序、area config 快照、单写者保护、留存策略均已在 `design.md` 定义。
|
||||
- [x] 已明确遗留客户端边界与客户端测试基础设施的决定。
|
||||
- [x] 已形成 `design.md` 和 `implement.md`,供用户审阅后再开始实现。
|
||||
|
||||
## 验收标准(交付阶段)
|
||||
|
||||
每条都必须可执行验证,不接受"看起来对了"。
|
||||
|
||||
- [ ] 拖动道路外缘 → 保存 → 重新编译,道路面、步行带、车道线、标线与 connector 一致更新,约束状态为 `exact`。
|
||||
- [ ] 车道分隔手柄可独立调整某条分隔线,且不等价于外缘偏移的副作用。
|
||||
- [ ] 落在 junction reserve 内的道路手柄不可拖动,并给出引导进入 `JunctionTools` 的原因。
|
||||
- [ ] `JunctionTools` 单路口会话可调进口宽度、cutback 与一个角部圆角,预览包含全部受影响派生对象;应用后主地图继续显示同一预览,取消后回到进入前状态。
|
||||
- [ ] 违反最小车道宽 2.4m 或造成路口面自交的草稿返回阻塞性诊断,且不替换最后一个有效预览。
|
||||
- [ ] 快速连续拖拽时乱序预览响应不会覆盖较新结果(可通过注入延迟的测试复现)。
|
||||
- [ ] 同一 OSM 重新导入后,约束按 `exact` / `pending` / `conflicted` / `stale` 分类报告;旧 revision 仍可打开并复现原结果。
|
||||
- [ ] 修改 area config 后,旧 revision 的编译结果不变(证明快照生效)。
|
||||
- [ ] 两个标签页并发保存时,后者收到 409 且不覆盖前者。
|
||||
- [ ] 未保存编辑可撤销/重做;已保存编辑的撤销以反向操作或禁用约束体现,历史不被重写。
|
||||
- [ ] 拖拽与选择过程中 OpenLayers `Map` 未被重建,只有受影响 source 被替换。
|
||||
- [ ] `npm run format:check`、`npm run test`、`npm run test:client`、新增客户端单元测试与 `npm run build` 全绿。
|
||||
@@ -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 VectorLayer:Point/LineString feature,Canvas 画柄与影响区间
|
||||
- EditPointerInteraction:命中 handle -> 投影拖拽方向 -> 更新 draft command
|
||||
- Snap:只对批准的语义锚点/网格 source 生效
|
||||
|
|
||||
v
|
||||
React EditSession(selection、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-23),BSD-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。
|
||||
@@ -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 拆段、路口重算或编译器升级后很难可靠重放。
|
||||
@@ -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 更新临时 constraint,pointer 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、停止线、斑马线和标线。
|
||||
@@ -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。
|
||||
@@ -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 缩减为多个互相冲突的普通路口编辑。
|
||||
@@ -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 优先于道路 profile,solver 在边界自动求连续过渡。
|
||||
|
||||
## 当前阶段的延后边界
|
||||
|
||||
`JunctionTools` 会成为核心竞争力,但当前任务只设计其对象边界、会话、预览和约束合同。高级复合路口模板、多个路口联动编辑、控制设施的单对象创作、协作与全面拓扑创作均延后,避免这些问题遮蔽直接编辑基础链路的验证。
|
||||
@@ -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。
|
||||
|
||||
这一策略以可审阅的检查点承载长期历史,避免把高频拖拽/保存变成难以浏览的版本噪声。
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"id": "direct-manipulation-road-editor",
|
||||
"name": "direct-manipulation-road-editor",
|
||||
"title": "直接操纵道路编辑工作台",
|
||||
"description": "探索类似 Drawtonomy 的直接操纵道路编辑,保持结构化 overrides、可审计性和可重编译性",
|
||||
"status": "planning",
|
||||
"dev_type": "feature",
|
||||
"scope": "road-editor",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [
|
||||
"08-26-direct-edit-documents",
|
||||
"08-26-direct-edit-solver-api",
|
||||
"08-26-direct-edit-map-editor",
|
||||
"08-26-direct-edit-junction-tools",
|
||||
"08-27-junction-dominated-roads"
|
||||
],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
"src/compile/compiler.js",
|
||||
"src/compile/native-road.js",
|
||||
"src/compile/complex-junction.js",
|
||||
"src/compile/layer-manifest.js",
|
||||
"src/geometry/lane-geometry.js",
|
||||
"workbench/server.js",
|
||||
"workbench/client/src/App.tsx",
|
||||
"workbench/client/src/components/MapCanvas.tsx",
|
||||
"workbench/client/src/map/layers.ts",
|
||||
"workbench/client/src/lib/api.ts",
|
||||
"workbench/client/src/types/state.ts",
|
||||
"test/index.js",
|
||||
"test/fixtures.js"
|
||||
],
|
||||
"notes": "父任务:持有需求集、design.md 权威合约、跨子任务验收与第 8 步集成验证。实现落在三个子任务。",
|
||||
"meta": {}
|
||||
}
|
||||
127
.trellis/tasks/archive/2026-08/00-bootstrap-guidelines/prd.md
Normal file
127
.trellis/tasks/archive/2026-08/00-bootstrap-guidelines/prd.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Bootstrap Task: Fill Project Development Guidelines
|
||||
|
||||
**You (the AI) are running this task. The developer does not read this file.**
|
||||
|
||||
The developer just ran `trellis init` on this project for the first time.
|
||||
`.trellis/` now exists with empty spec scaffolding, and this bootstrap task
|
||||
exists under `.trellis/tasks/`. When they want to work on it, they should start
|
||||
this task from a session that provides Trellis session identity.
|
||||
|
||||
**Your job**: help them populate `.trellis/spec/` with the team's real
|
||||
coding conventions. Every future AI session — this project's
|
||||
`trellis-implement` and `trellis-check` sub-agents — auto-loads spec files
|
||||
listed in per-task jsonl manifests. Empty spec = sub-agents write generic
|
||||
code. Real spec = sub-agents match the team's actual patterns.
|
||||
|
||||
Don't dump instructions. Open with a short greeting, figure out if the repo
|
||||
has any existing convention docs (CLAUDE.md, .cursorrules, etc.), and drive
|
||||
the rest conversationally.
|
||||
|
||||
---
|
||||
|
||||
## Status (update the checkboxes as you complete each item)
|
||||
|
||||
- [x] Fill frontend guidelines
|
||||
- [x] Add code examples
|
||||
|
||||
---
|
||||
|
||||
## Spec files to populate
|
||||
|
||||
|
||||
### Frontend guidelines
|
||||
|
||||
| File | What to document |
|
||||
|------|------------------|
|
||||
| `.trellis/spec/frontend/directory-structure.md` | Component/page/hook organization |
|
||||
| `.trellis/spec/frontend/component-guidelines.md` | Component patterns, props conventions |
|
||||
| `.trellis/spec/frontend/hook-guidelines.md` | Custom hook naming, patterns |
|
||||
| `.trellis/spec/frontend/state-management.md` | State library, patterns, what goes where |
|
||||
| `.trellis/spec/frontend/type-safety.md` | TypeScript conventions, type organization |
|
||||
| `.trellis/spec/frontend/quality-guidelines.md` | Linting, testing, accessibility |
|
||||
|
||||
|
||||
### Thinking guides (already populated)
|
||||
|
||||
`.trellis/spec/guides/` contains general thinking guides pre-filled with
|
||||
best practices. Customize only if something clearly doesn't fit this project.
|
||||
|
||||
---
|
||||
|
||||
## How to fill the spec
|
||||
|
||||
### Step 1: Import from existing convention files first (preferred)
|
||||
|
||||
Search the repo for existing convention docs. If any exist, read them and
|
||||
extract the relevant rules into the matching `.trellis/spec/` files —
|
||||
usually much faster than documenting from scratch.
|
||||
|
||||
| File / Directory | Tool |
|
||||
|------|------|
|
||||
| `CLAUDE.md` / `CLAUDE.local.md` | Claude Code |
|
||||
| `AGENTS.md` | Codex / Claude Code / agent-compatible tools |
|
||||
| `.cursorrules` | Cursor |
|
||||
| `.cursor/rules/*.mdc` | Cursor (rules directory) |
|
||||
| `.windsurfrules` | Windsurf |
|
||||
| `.clinerules` | Cline |
|
||||
| `.roomodes` | Roo Code |
|
||||
| `.github/copilot-instructions.md` | GitHub Copilot |
|
||||
| `.vscode/settings.json` → `github.copilot.chat.codeGeneration.instructions` | VS Code Copilot |
|
||||
| `CONVENTIONS.md` / `.aider.conf.yml` | aider |
|
||||
| `CONTRIBUTING.md` | General project conventions |
|
||||
| `.editorconfig` | Editor formatting rules |
|
||||
|
||||
### Step 2: Analyze the codebase for anything not covered by existing docs
|
||||
|
||||
Scan real code to discover patterns. Before writing each spec file:
|
||||
- Find 2-3 real examples of each pattern in the codebase.
|
||||
- Reference real file paths (not hypothetical ones).
|
||||
- Document anti-patterns the team clearly avoids.
|
||||
|
||||
### Step 3: Document reality, not ideals
|
||||
|
||||
**Critical**: write what the code *actually does*, not what it should do.
|
||||
Sub-agents match the spec, so aspirational patterns that don't exist in the
|
||||
codebase will cause sub-agents to write code that looks out of place.
|
||||
|
||||
If the team has known tech debt, document the current state — improvement
|
||||
is a separate conversation, not a bootstrap concern.
|
||||
|
||||
---
|
||||
|
||||
## Quick explainer of the runtime (share when they ask "why do we need spec at all")
|
||||
|
||||
- Every AI coding task spawns two sub-agents: `trellis-implement` (writes
|
||||
code) and `trellis-check` (verifies quality).
|
||||
- Each task has `implement.jsonl` / `check.jsonl` manifests listing which
|
||||
spec files to load.
|
||||
- The platform hook auto-injects those spec files + the task's `prd.md`
|
||||
into every sub-agent prompt, so the sub-agent codes/reviews per team
|
||||
conventions without anyone pasting them manually.
|
||||
- Source of truth: `.trellis/spec/`. That's why filling it well now pays
|
||||
off forever.
|
||||
|
||||
---
|
||||
|
||||
## Completion
|
||||
|
||||
When the developer confirms the checklist items above are done with real
|
||||
examples (not placeholders), guide them to run:
|
||||
|
||||
```bash
|
||||
python3 ./.trellis/scripts/task.py finish
|
||||
python3 ./.trellis/scripts/task.py archive 00-bootstrap-guidelines
|
||||
```
|
||||
|
||||
After archive, every new developer who joins this project will get a
|
||||
`00-join-<slug>` onboarding task instead of this bootstrap task.
|
||||
|
||||
---
|
||||
|
||||
## Suggested opening line
|
||||
|
||||
"Welcome to Trellis! Your init just set me up to help you fill the project
|
||||
spec — a one-time setup so every future AI session follows the team's
|
||||
conventions instead of writing generic code. Before we start, do you have
|
||||
any existing convention docs (CLAUDE.md, .cursorrules, CONTRIBUTING.md,
|
||||
etc.) I can pull from, or should I scan the codebase from scratch?"
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id": "00-bootstrap-guidelines",
|
||||
"name": "00-bootstrap-guidelines",
|
||||
"title": "Bootstrap Guidelines",
|
||||
"description": "Fill in project development guidelines for AI agents",
|
||||
"status": "completed",
|
||||
"dev_type": "docs",
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": null,
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
".trellis/spec/frontend/"
|
||||
],
|
||||
"notes": "First-time setup task created by trellis init (frontend project)",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "逐条核对文档 schema、5 态 status、存储布局与快照规则是否与权威合约一致"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "检查是否出现第二套 hash、第二套 ID 校验或重复的 overrides 读写逻辑"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "核对 v1 overrides 文件格式与既有 import 目录未被改写"}
|
||||
@@ -0,0 +1,18 @@
|
||||
# 设计
|
||||
|
||||
技术合约不在本文重复定义。权威定义见父任务 `.trellis/tasks/08-26-direct-manipulation-road-editor/design.md` 的以下小节:
|
||||
|
||||
- 「约束模型」— `RoadConstraintKind` 6 个取值、`SemanticAnchor` 4 种锚点、`transition`、`boundaryIndex` 范围、`station` 语义。
|
||||
- 「文档、活动副本与 revision」— `RoadEditDocument`、`RoadConstraint`、`ConstraintStatus` 5 态表。
|
||||
- 「存储布局与迁移」— 目录结构、惰性迁移、内容寻址与留存策略。
|
||||
- 「area config 所有权」— 快照规则与 `areaConfigSha256`。
|
||||
- 「编译器几何版本变更」— `recheck` 策略。
|
||||
- 「上线与回滚形态」— 服务端纯增量、v1 文件不动。
|
||||
|
||||
## 本子任务的局部决定
|
||||
|
||||
- 文档读写与 schema 校验放在编译器侧(`src/compile/` 下新增模块),不放在 `workbench/server.js`:预览、正式编译与 CLI 导出都要用它,放服务器会造成第二套实现。
|
||||
- 校验只做结构与取值范围,不做几何可行性。几何不变量(最小车道宽、外缘不交叉等)属求解器职责,见 `direct-edit-solver-api`。
|
||||
- 惰性迁移在读取入口触发,不提供独立迁移命令:既有 import 目录数量少,且迁移是纯增量创建,失败可重试。
|
||||
- `ConstraintStatus` 在本子任务只落地存储与校验;实际重放匹配逻辑属 `direct-edit-solver-api`。本任务的测试用手工构造的 status 值覆盖读写路径。
|
||||
- 内容寻址采用 `sha256`,与 `RoadRevisionManifest.source.osmSha256` 同一摘要,避免两套 hash。
|
||||
@@ -0,0 +1,5 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "父任务权威合约:约束模型、文档 schema、存储布局与迁移、area config 快照、编译器版本策略"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/revisions.md", "reason": "RoadRevisionManifest 字段来源与不可变 revision 的设计理由"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "现有 ID 语义、v1 overrides 校验与 stale diagnostics 的可复用边界"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "sha256 摘要、ID 校验与 overrides 应用必须复用既有实现,不得新建第二套"}
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "新增 JSONL 文档与配置字段跨编译器/服务端/客户端,属该指南触发条件"}
|
||||
@@ -0,0 +1,40 @@
|
||||
# 实施计划
|
||||
|
||||
对应父任务 `implement.md` 的第 0、2、3 步。每步一个提交,门禁不过就停。
|
||||
|
||||
## 1. 测试基础设施
|
||||
|
||||
- 目标:先建验证能力,避免后续"写完没法测"。
|
||||
- 范围:`package.json` 增加固定版本 `vitest` 与 `test:client:unit` 脚本;建立客户端纯逻辑测试目录。不碰产品代码。
|
||||
- 验证:`npm run test:client:unit`(空套件通过)、`npm run format:check`、`npm run test`、`npm run test:client`、`npm run build`。
|
||||
- 门禁:新脚本可跑通且不影响任何既有脚本。
|
||||
- 回滚点:仅 `package.json` 与测试目录,revert 无副作用。
|
||||
|
||||
## 2. v2 文档与 schema 校验
|
||||
|
||||
- 目标:`native-road-edits/v2` 的读写与结构校验。
|
||||
- 范围:`src/compile/` 下新增文档模块:6 个 kind、4 种锚点、`anchorSnapshot`、`ConstraintStatus`、`documentVersion`、`operations`。仅结构与取值范围校验。
|
||||
- 验证:`npm run test` 新增 fixture——文档往返无损;`boundaryIndex` 非整数或 `<1` 被拒;非法 station(超出 `0..1`、`start>=end`)被拒;未知 kind 被拒;kind 与锚点类型不匹配被拒;provenance 指向未记录的 operation 被拒;每次写入 `documentVersion` 递增 1。
|
||||
- 注意:`boundaryIndex >= laneCount` **不**属于 schema 校验——车道数是模型信息,schema 层无从得知。按 `design.md`,该越界在重放时由求解器判定并转 `stale`,归 `direct-edit-solver-api`。
|
||||
- 门禁:校验错误信息能指出具体约束 id 与字段,不是笼统失败。
|
||||
- 回滚点:纯新增模块,无调用方,revert 无影响。
|
||||
|
||||
## 3. 存储布局、惰性迁移与 revision
|
||||
|
||||
- 目标:`active/`、`revisions/`、内容寻址 OSM 副本落地,既有目录可平滑接入。
|
||||
- 范围:目录布局读写;读取入口触发的惰性迁移,把当前 `source.osm` + overrides + signals + area config 冻结为 `rev-0001`;命名检查点创建;revision 恢复读取;`RoadRevisionManifest` 写入。
|
||||
- 验证:`npm run test` 新增 fixture——只含旧文件的目录被打开后生成 `rev-0001` 且既有文件字节不变;内容相同的 OSM 重复导入不产生第二份副本;命名检查点冻结后可完整恢复读取;manifest digest 与实际文件一致。
|
||||
- 门禁:迁移对既有 `workbench-data/import-*` 只增不改;revert 后旧代码仍能打开这些目录。
|
||||
- 回滚点:revert 后 `active/` 与 `revisions/` 只是残留目录,旧代码忽略它们。
|
||||
|
||||
## 4. area config 快照
|
||||
|
||||
- 目标:切断编译对外部区域配置文件的运行期依赖。
|
||||
- 范围:活动副本与每个 revision 各写一份 `area-config.snapshot.json`;编译改读快照;`base.areaConfigSha256` 指向快照。
|
||||
- 验证:`npm run test` 新增 fixture——冻结 revision 后修改外部 area config,重新编译该 revision 结果不变;快照缺失时报明确错误而非静默读外部文件。
|
||||
- 门禁:无 v2 文档时编译输出与快照一致。注意:仓库现有 `test/baseline/*.json` 已过期(`contract` 为 `native-road-package/v1`,当前为 `v1.1`,18 个文件 hash 全部不符),且 `test/fixtures.js:26` 只断言文件存在、从不比对内容,仓库内也没有生成器。因此本步须先从当前 main 重新生成快照并接上真实比对,再谈"不变"。
|
||||
- 回滚点:revert 后编译回到读外部配置文件;已写快照被忽略。
|
||||
|
||||
## 步骤依赖
|
||||
|
||||
1 → 2 → 3 → 4 是硬顺序。第 4 步的 baseline 比对是本子任务能否交付的总门禁:无 v2 约束时编译输出必须与当前 main 完全一致。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 直接编辑文档与 revision 基础
|
||||
|
||||
父任务:`.trellis/tasks/08-26-direct-manipulation-road-editor`。需求来源与权威合约在父任务的 `prd.md` / `design.md`。
|
||||
|
||||
## 目标
|
||||
|
||||
建立 `native-road-edits/v2` 文档、活动工作副本、不可变 revision 与 area config 快照,使后续求解、预览和编辑能力有可复现的持久化基础。本子任务不含任何 UI,也不含约束求解。
|
||||
|
||||
## 顺序依赖
|
||||
|
||||
无前置。是 `direct-edit-solver-api`、`direct-edit-map-editor`、`direct-edit-junction-tools` 三者的前置:它们依赖本任务定义的文档 schema、`documentVersion` 与快照读取路径。
|
||||
|
||||
## 范围
|
||||
|
||||
- `vitest` 依赖与 `test:client:unit` 脚本(后续子任务的验证基础)。
|
||||
- `native-road-edits/v2` 文档读写与 schema 校验:6 个约束 kind、4 种锚点、`anchorSnapshot`、5 态 `ConstraintStatus`、`documentVersion`。
|
||||
- `workbench-data/import-<id>/` 内的 `active/`、`revisions/`、内容寻址 `osm/<sha256>.osm` 布局。
|
||||
- 既有 import 目录的惰性迁移:首次打开时冻结 `rev-0001`,不移动或重写既有文件。
|
||||
- `area-config.snapshot.json` 的写入与读取;编译与预览改读快照。
|
||||
- 命名检查点创建与 revision 恢复读取。
|
||||
|
||||
## 不做
|
||||
|
||||
- `resolveDirectEditConstraints` 与任何几何求解(属 `direct-edit-solver-api`)。
|
||||
- 预览、保存、rebase 的 HTTP 端点(属 `direct-edit-solver-api`)。
|
||||
- 任何客户端改动。
|
||||
- v1 `native-road-overrides.json` 的迁移或格式变更。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] `vitest` 与 `test:client:unit` 可跑通空套件,且不影响既有 `test` / `test:client` / `build`。
|
||||
- [ ] v2 文档往返读写无损;schema 校验拒绝越界 `boundaryIndex`、非法 station 与未知 kind。
|
||||
- [ ] 每次成功写入使 `documentVersion` 递增 1。
|
||||
- [ ] 只含旧文件的既有 `workbench-data/import-*` 目录,在新代码下可正常打开且未被改写,并生成 `rev-0001`。
|
||||
- [ ] 内容相同的 OSM 重复导入不产生第二份副本。
|
||||
- [ ] 冻结 revision 后修改外部 area config,重新编译该 revision 结果不变。
|
||||
- [ ] 快照缺失时给出明确错误,不静默回退到读外部配置文件。
|
||||
- [ ] `npm run format:check`、`npm run test`、`npm run test:client`、`npm run build` 全绿。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "direct-edit-documents",
|
||||
"name": "direct-edit-documents",
|
||||
"title": "直接编辑文档与 revision 基础",
|
||||
"description": "native-road-edits/v2 文档、活动副本、不可变 revision、area config 快照与惰性迁移",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-27",
|
||||
"branch": "feat/direct-edit-documents",
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-26-direct-manipulation-road-editor",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "核对坐标分层(禁止用 3857 坐标差当米)、预览时序仲裁位置与开关关闭时的零残留要求"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/canvas-integration.md", "reason": "核对基线图层保持只读、手柄只带 handleId、未把地理坐标直接写成道路 polygon"}
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "检查客户端是否出现第二份约束模型或对 manifest 字段的局部 cast"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "检查是否重复实现图层更新、选择高亮或米制换算"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "核对 Map 单例未被重建、既有图层可见性与选择行为未回归"}
|
||||
@@ -0,0 +1,22 @@
|
||||
# 设计
|
||||
|
||||
技术合约不在本文重复定义。权威定义见父任务 `.trellis/tasks/08-26-direct-manipulation-road-editor/design.md` 的以下小节:
|
||||
|
||||
- 「Handle manifest」— 客户端读取的字段与 `reserves` 禁入区语义。
|
||||
- 「坐标与单位分层」— 客户端渲染用 EPSG:3857,指针位移换米必须先回 4326 再用球面距离。
|
||||
- 「预览时序与延迟预算」— 80ms 防抖、`pointerup` 最终请求、`previewSeq` 乱序丢弃、`degraded` 处理。
|
||||
- 「交互与预览」「编辑所有权」— 只读基线图层、三个独立 source、道路区间与 junction reserve 的边界。
|
||||
- 「客户端边界」— 遗留 `app.js` 不在范围、`vitest` 覆盖范围。
|
||||
- 「上线与回滚形态」— `directEdit` 开关默认关闭时的行为。
|
||||
|
||||
输入 adapter 的候选、探针退出条件与回退优先级见父任务 `research/canvas-integration.md`。
|
||||
|
||||
## 本子任务的局部决定
|
||||
|
||||
- 三个 source 严格分离:`editHandles`(手柄)、`editGhost`(客户端即时轮廓与辅助线)、`editPreview`(服务端权威几何)。基线图层始终只读,绝不写入。
|
||||
- 手柄 feature 的 properties 只带 `handleId`,语义一律回 manifest 查。这样客户端没有第二份约束模型。
|
||||
- ghost 只画手柄、辅助线与半透明预估轮廓,不承担权威几何。服务端预览到达后替换 `editPreview`,ghost 随即淡出。
|
||||
- `EditSession` 是纯逻辑对象,不持有 OpenLayers 引用:命令栈、undo/redo、`previewSeq` 仲裁都可在 node 环境下单测。OL 只作为它的输入输出适配层。
|
||||
- `previewSeq` 仲裁放在 `EditSession` 而非请求层:请求层只负责发与取消,丢弃决策要可测。
|
||||
- 探针独立成可删除目录,不进 `MapCanvas`。探针结论只影响"手柄命中与拖拽由谁实现",不影响上面任何一条。
|
||||
- 开关关闭时不注册 interaction、不请求 manifest、不创建三个 source,确保零残留开销。
|
||||
@@ -0,0 +1,6 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "父任务权威合约:handle manifest 字段、坐标分层、预览时序、编辑所有权、客户端边界、directEdit 开关"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/canvas-integration.md", "reason": "ol-ext 探针的退出条件、回退优先级,以及为何不引入第二个 Canvas/视口"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/joint-solver.md", "reason": "EditSession 的交互序列与浏览器 ghost 的职责边界"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "现有单例 Map、source registry 与选择高亮的可复用点"}
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "manifest 字段从服务端到客户端类型的流转,避免局部 cast 与重复状态"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "图层与 source 更新必须复用现有 layers.ts 模式,米制换算不得重写"}
|
||||
@@ -0,0 +1,49 @@
|
||||
# 实施计划
|
||||
|
||||
对应父任务 `implement.md` 的第 1、6 步。每步一个提交,门禁不过就停。
|
||||
|
||||
前置:`direct-edit-solver-api` 第 2 步(handle manifest)已可用。完整验收需其预览与保存端点就绪。
|
||||
|
||||
## 1. `ol-ext` 限时探针
|
||||
|
||||
- 目标:只回答一个问题——能否复用通用 handle 的命中、pointer 生命周期与视觉反馈。
|
||||
- 范围:隔离探针目录,不修改生产基线图层,不进依赖清单直到通过。
|
||||
- 验证:手动跑探针页,确认 OL `Map` 未重建、proxy 拖拽稳定、基线 source 未被写入、事件能转成 draft 值。
|
||||
- 门禁:三条同时成立才把 `ol-ext` 固定为依赖。超过一个工作日或任一条不成立,立刻停止排障。
|
||||
- 回滚点:删除探针目录,改用原生 OL `Snap` + 小型 `PointerInteraction` adapter。数据合约不变,本步失败只换输入层。
|
||||
|
||||
## 2. `EditSession` 纯逻辑
|
||||
|
||||
- 目标:先把可单测的部分做完,与地图解耦。
|
||||
- 范围:命令栈与 undo/redo;`previewSeq` 仲裁;handle 事件到约束值的投影;指针位移到米的换算。不含任何 OpenLayers 引用。
|
||||
- 验证:`npm run test:client:unit` —— 命令栈压入/撤销/重做序列正确;乱序响应(后发先到)被丢弃且不覆盖较新预览;投影在四种 kind 上给出预期约束值;同一像素位移在低纬与高纬得到一致的米值(纬度缩放正确)。
|
||||
- 门禁:四类纯逻辑全部有测试;`EditSession` 不 import `ol`。
|
||||
- 回滚点:纯新增模块,无调用方,revert 无影响。
|
||||
|
||||
## 3. 手柄渲染与开关
|
||||
|
||||
- 目标:选中道路后能看到正确的手柄,且禁入区可见可解释。
|
||||
- 范围:`directEdit` 开关(默认关闭);selection → manifest 读取;`editHandles` source 渲染;reserve 内手柄置灰并带原因提示。只读不可拖。
|
||||
- 验证:`npm run test:client`、`npm run build`;手测——选中道路出现手柄,拖拽前后 `Map` 为同一实例;reserve 内手柄置灰并提示进入 `JunctionTools`;关闭开关后无手柄、无 manifest 请求。
|
||||
- 门禁:`Map` 未被重建;基线 source 未被写入。
|
||||
- 回滚点:开关关闭即止损。
|
||||
|
||||
## 4. 拖拽、ghost 与预览
|
||||
|
||||
- 目标:四类能力真正可拖,预览权威且不闪烁。
|
||||
- 范围:外缘偏移、步行带宽度、车道分隔位置、区间范围控制;`editGhost` 即时反馈;80ms 防抖与 `pointerup` 最终请求;`AbortController` 取消在途请求;`editPreview` 替换;无效草稿诊断与保留最后有效预览。
|
||||
- 验证:`npm run test:client:unit`、`npm run test:client`、`npm run build`;手测——拖动外缘时道路面/步行带/车道线/标线/connector 一致更新且只有受影响 source 被替换;车道分隔可独立调整;区间范围手柄改变影响区间且两端平滑;连续快速拖拽松手后无回跳;违反不变量时停在最后有效预览并显示诊断。
|
||||
- 门禁:只有受影响 source 被替换,无全图层重建;无回跳。
|
||||
- 回滚点:按能力分批提交,可单独回退某一类手柄。
|
||||
|
||||
## 5. 应用、保存与撤销
|
||||
|
||||
- 目标:闭合编辑生命周期。
|
||||
- 范围:应用/保存/取消;undo/redo 接到命令栈;保存走 `expectedDocumentVersion`;409 冲突提示。
|
||||
- 验证:`npm run test:client:unit`(保存冲突分支)、`npm run test:client`、`npm run build`;手测——未保存编辑可撤销/重做;保存后重新编译几何不变且状态 `exact`;两标签页同时保存后者收到 409 提示且前者结果保留。
|
||||
- 门禁:撤销不重写已保存历史;409 有明确用户提示而非静默失败。
|
||||
- 回滚点:revert 后回到第 4 步的预览-only 状态。
|
||||
|
||||
## 步骤依赖
|
||||
|
||||
1 与 2 可并行;3 依赖 1 的结论与 2;4 依赖 3;5 依赖 4。第 1 步失败不阻塞 2-5,只改变第 3、4 步的命中与拖拽实现方式。
|
||||
@@ -0,0 +1,57 @@
|
||||
# 主地图道路区间编辑器
|
||||
|
||||
父任务:`.trellis/tasks/08-26-direct-manipulation-road-editor`。需求来源与权威合约在父任务的 `prd.md` / `design.md`。
|
||||
|
||||
## 目标
|
||||
|
||||
在主地图上实现道路内部区间的直接操纵:外缘、步行带、车道分隔与区间范围可拖,拖拽即时有 ghost,服务端返回权威预览,改动可撤销、可保存。
|
||||
|
||||
## 顺序依赖
|
||||
|
||||
前置:`direct-edit-solver-api` 的第 2 步(handle manifest)交付后即可开工;完整验收需要其预览与保存端点全部就绪。
|
||||
后继:`direct-edit-junction-tools` 复用本任务的 `EditSession`、ghost/preview 图层与命令栈。
|
||||
|
||||
## 范围
|
||||
|
||||
- `ol-ext` Transform 的限时隔离探针,以及不通过时的原生 OpenLayers 回退。
|
||||
- selection → handle manifest 的读取与渲染;handles / ghost / preview 三个独立 source。
|
||||
- 四类可拖能力:外缘偏移、步行带宽度、车道分隔位置、区间范围控制。
|
||||
- `EditSession`:命令栈、undo/redo、应用/保存/取消。
|
||||
- 80ms 防抖预览、`pointerup` 强制最终请求、`previewSeq` 乱序丢弃、`AbortController` 取消在途请求。
|
||||
- 无效草稿的诊断反馈,保留最后一个有效预览。
|
||||
- `directEdit` 开关,默认关闭。
|
||||
|
||||
## 不做
|
||||
|
||||
- 路口 reserve 内的任何编辑(属 `direct-edit-junction-tools`)。
|
||||
- 遗留 `workbench/client/app.js` 的同步或删除。
|
||||
- OSM 中心线与拓扑的编辑。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 选中道路后出现手柄,拖拽前后 OpenLayers `Map` 为同一实例,未被重建。
|
||||
- [ ] 拖动外缘 → 道路面、步行带、车道线、标线、connector 一致更新,只有受影响 source 被替换。
|
||||
- [ ] 车道分隔手柄可独立调整某条分隔线,效果不等价于外缘偏移的副作用。
|
||||
- [ ] 区间范围手柄可修改影响区间,两端平滑过渡回基线。
|
||||
- [ ] 落在 junction reserve 内的手柄不可拖动,并提示进入 `JunctionTools`。
|
||||
- [ ] 连续快速拖拽后松手,最终几何与松手位置一致,无回跳。
|
||||
- [ ] 违反不变量的草稿显示阻塞诊断,地图停在最后一个有效预览。
|
||||
- [ ] 未保存编辑可撤销/重做;保存后重新编译几何不变,约束状态 `exact`。
|
||||
- [ ] 客户端单元测试覆盖:命令栈与 undo/redo、`previewSeq` 乱序丢弃、handle 事件到约束值的投影、米制换算在不同纬度的正确性。
|
||||
- [ ] 关闭 `directEdit` 开关后,工作台行为与当前 main 一致。
|
||||
- [ ] `npm run format:check`、`npm run test`、`npm run test:client`、`npm run test:client:unit`、`npm run build` 全绿。
|
||||
|
||||
## 交付状态(2026-08-28 收口)
|
||||
|
||||
11 条里 9 条通过,2 条**显式接受为未通过**,不勾选:
|
||||
|
||||
| 条目 | 结论 |
|
||||
| --- | --- |
|
||||
| 区间范围手柄可修改影响区间,两端平滑过渡回基线 | **阻塞**。`compileGeometry()` 不读 `profile.interval` / `profile.transitions`,每次编辑都作用于整条路。纯逻辑(`intervalRangeHandles`、`projectIntervalEnd`、`coordinateAtStation`)已交付并单测,UI 由 `intervalEditingSupported = false` 隐藏——拖了没效果的控件比没有控件更糟。详见 `research/interval-not-applied.md`,含实现路径。 |
|
||||
| 落在 junction reserve 内的手柄不可拖动,并提示进入 `JunctionTools` | **实现完成但无法在用户数据上演示**。判据(可编辑带 < 道路宽度)已实现,`fengshu-er-road.osm` 上实测 6 段置灰共 35 个手柄;用户导入的片区 125 个手柄全部可编辑,因为不存在被两个路口填满的短路段。由 `test/direct-edit-solver.js` 的 sandwich fixture 覆盖。 |
|
||||
|
||||
其余 9 条:1、2、3、6、7、10 由用户浏览器手测确认;8 的保存与重放由用户手测加 `test/workbench-edit-api.js` 断言;9 为 97 个客户端单测;11 为门禁。
|
||||
|
||||
本任务实施期间修掉的三个缺陷,均为跨层接线而非逻辑错误,已写入 spec:
|
||||
道路手柄左右放错边、约束未随 operation 发送、`/api/import` 未安装 `compileFresh`。
|
||||
后者早于直接编辑存在,一直让 UI 导入的会话无法重新生成。
|
||||
@@ -0,0 +1,63 @@
|
||||
# 区间范围手柄由客户端合成
|
||||
|
||||
第 4 步的实现决定,与 `design.md`「Handle manifest」小节的字面规则有偏离,故单独记录。
|
||||
决定人:dingkang,2026-08-27。
|
||||
|
||||
## 偏离了什么
|
||||
|
||||
`design.md` 写明:
|
||||
|
||||
> 服务端从同一语义模型生成手柄清单,**客户端不自行推导手柄位置或可拖方向**。
|
||||
|
||||
而区间范围手柄(第 4 类可拖能力)由客户端 `selection.ts` 的 `intervalRangeHandles()` 合成,
|
||||
位置由 `meters.ts` 的 `coordinateAtStation()` 从中心线插值得到。
|
||||
|
||||
## 为什么这样定
|
||||
|
||||
**1. manifest schema 装不下它。**
|
||||
`EditHandle.kind` 的类型是 `RoadConstraintKind`,而 `design.md` 的 kind 表明确声明
|
||||
「这 6 个 kind 与 PRD 首期范围一一对应,没有多余项也没有缺口」。
|
||||
范围手柄不是一种约束——它改的是既有约束 `anchor` 的 `startStation` / `endStation`。
|
||||
要下发就得加第 7 个求解器根本不认的假 kind,或加一个平行数组。为派生数据改合约不划算。
|
||||
|
||||
**2. 它的位置是服务端已发数据的纯函数。**
|
||||
区间来自服务端算好并下发的 `anchor.startStation` / `endStation`,
|
||||
合法窗口来自 `manifest.reserves`,拖拽轴是道路切线。
|
||||
服务端下发等于把自己刚发的东西再算一遍回显,正是 `cross-layer-thinking-guide.md`
|
||||
警告的「derived state 另立第二个游标」。
|
||||
|
||||
**3. 决定性的一条:客户端本来就需要 station → 坐标的插值。**
|
||||
`design.md` 要求 ghost 画「半透明预估轮廓」,即在道路上标出受影响的区间带。
|
||||
画这条带子必须把 station 插值成坐标,**与范围手柄由谁产出无关**。
|
||||
`coordinateAtStation()` 因此是客户端的既有需求;有了它,合成范围手柄几乎免费,
|
||||
服务端改动买不到任何东西。
|
||||
|
||||
**4. `projectIntervalEnd()` 已交付并测试**,签名恰好就是这些输入。
|
||||
|
||||
## 为什么认为符合规则的意图
|
||||
|
||||
那条规则防的是客户端发明**语义**——哪个约束、哪个方向、什么范围合法。
|
||||
范围手柄一样都没发明:
|
||||
|
||||
| 语义 | 来源 |
|
||||
| --- | --- |
|
||||
| 合法窗口 | `manifest.reserves`(服务端) |
|
||||
| 区间当前值 | 约束 `anchor` 的 station(服务端) |
|
||||
| 拖拽轴 | 道路中心线切线(编译模型,服务端) |
|
||||
| 屏幕位置 | 客户端插值 ← **仅此一项是派生的** |
|
||||
|
||||
客户端只做「把服务端已选定的 station 插值成屏幕位置」这一件事。
|
||||
|
||||
## 已接受的代价
|
||||
|
||||
`coordinateAtStation()` 是 `src/compile/direct-edit-solver.js` 里 `coordinateAt()` 的
|
||||
第二份实现。二者都用弧长插值,但分属不同 runtime(CommonJS 服务端 / ESM 浏览器),
|
||||
且客户端那份只服务于 ghost 渲染,不参与任何持久化或求解。
|
||||
`meters.ts` 的注释已标明这层关系,防止后来者误以为可以随意改动其中一份。
|
||||
|
||||
## 若要改回服务端下发
|
||||
|
||||
新增 manifest 字段 `rangeHandles: IntervalRangeHandle[]`(不要塞进 `handles`),
|
||||
由服务端用既有 `coordinateAt()` 算位置。客户端删掉 `intervalRangeHandles()` 即可,
|
||||
`projectIntervalEnd()` 与拖拽链路不受影响。
|
||||
`coordinateAtStation()` 仍需保留,因为 ghost 的区间带还要用。
|
||||
@@ -0,0 +1,60 @@
|
||||
# 几何编译器不施加 `road-interval` 区间
|
||||
|
||||
第 4 步手测时发现:区间范围手柄拖动有 ghost 反馈,但松手后几何毫无变化。
|
||||
经排查这是服务端的能力缺口,不是客户端拖拽逻辑的问题。
|
||||
|
||||
## 事实
|
||||
|
||||
`grep "\.interval\b" src/compile/native-road.js` 返回空。
|
||||
|
||||
求解器写入了区间与过渡,几何阶段一个都不读:
|
||||
|
||||
| profile 字段 | solver 写入 | `compileGeometry()` 读取 |
|
||||
| --- | --- | --- |
|
||||
| `edgeOffsets.left/right` | ✅ | ✅ `centerlineShift`(native-road.js:825) |
|
||||
| `widthMeters` | ✅ | ✅ native-road.js:832 |
|
||||
| `sidewalkWidths.left/right` | ✅ | ✅ 真实宽度,`sidewalkRing`(native-road.js:1666-1671) |
|
||||
| `laneDividerOffsets` | ✅ | ✅ native-road.js:1946 |
|
||||
| **`interval`** | ✅ direct-edit-solver.js:274-277、418 | ❌ **零消费者** |
|
||||
| **`transitions`** | ✅ direct-edit-solver.js:293、303、322 | ❌ **零消费者** |
|
||||
|
||||
## 后果
|
||||
|
||||
1. **每一次直接编辑都作用于整条路。** `anchor.startStation` / `endStation` 被存储、被
|
||||
`validateEditDocument()` 校验、被带进 `resolveDirectEditConstraints()` 的结果,然后被忽略。
|
||||
2. **区间范围手柄在几何上不可能有效果。** 客户端的 `intervalRangeHandles()`、
|
||||
`projectIntervalEnd()`、`coordinateAtStation()` 都正确且有单测,但下游无人接收。
|
||||
3. **`transition: 'smoothstep' | 'linear'` 是死字段。** 父任务 design.md 承诺
|
||||
「作用于 interval 两端回归基线的过渡段」,实际没有任何过渡。
|
||||
|
||||
## 已采取的处置
|
||||
|
||||
`workbench/client/src/edit/flag.ts` 增加 `intervalEditingSupported = false`,
|
||||
范围手柄的 UI 据此隐藏。纯逻辑与测试全部保留。
|
||||
|
||||
理由:一个拖起来有反馈、松手却没效果的控件比没有这个控件更糟——它会持续产生
|
||||
bug 报告,并让人怀疑整个编辑器的其余部分。区间生效的那个提交把这个常量翻成 `true` 即可,
|
||||
客户端不需要其他改动。
|
||||
|
||||
## 对验收标准的影响
|
||||
|
||||
父任务 prd.md 的这条**当前无法通过**:
|
||||
|
||||
> 区间范围手柄可修改影响区间,两端平滑过渡回基线。
|
||||
|
||||
`08-26-direct-edit-map-editor/prd.md` 的同名条目同理。这不是客户端欠工,
|
||||
而是需要 `compileGeometry()` 具备按 station 施加横断面 profile 的能力。
|
||||
|
||||
## 若要实现(未排期)
|
||||
|
||||
用户 2026-08-27 判断影响不大,故未开任务,仅记录。真要做时的形状:
|
||||
|
||||
1. `native-road.js` 的横断面生成需要接受「沿中心线变化的 profile」而非单一常量宽度。
|
||||
目前 `applyDirectEditProfiles()`(约 818-838 行)返回的是整条路一个 `widthMeters`
|
||||
和一次 `offsetLine()` 整体平移,没有沿 station 变化的余地。
|
||||
2. 区间两端按 `transitions[kind]` 做 smoothstep / linear 插值回基线值。
|
||||
3. 车道线、标线、connector、步行带都由横断面派生,必须一并跟随,否则会脱节
|
||||
(父任务 research/joint-solver.md 的依赖链)。
|
||||
4. `test/fixtures` 基线会变化,需要显式 update-baseline 并人工核对。
|
||||
|
||||
这是 `compileGeometry` 的实质改动,属已归档的 `direct-edit-solver-api` 任务范围。
|
||||
@@ -0,0 +1,73 @@
|
||||
# ol-ext Transform 探针结论
|
||||
|
||||
`implement.md` 第 1 步的门禁结果。人工验证由 dingkang 在浏览器完成,2026-08-27。
|
||||
|
||||
## 门禁结果:三条全过
|
||||
|
||||
| 门禁 | 结果 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| OL `Map` 未重建 | 通过 | 实例 #2 / 累计构造 2 次(StrictMode 双挂载),2438 次 React 渲染下未重建 |
|
||||
| 基线 source 未被写入 | 通过 | 60 次拖拽,写入 0 次,几何指纹未变 |
|
||||
| proxy 拖拽稳定且事件能转成 draft 值 | 通过 | 1377 个 `translating` 事件;跟手连续、不跳数;松手不回弹;反复几十次不失手、不报错 |
|
||||
|
||||
投影链路同时被验证:钳位范围内 `raw 3.924 / draft 3.924` 完全相等,说明
|
||||
`signedMetersAlongAxis()` → `projectHandleValue()` 的换算与钳位都正确。
|
||||
|
||||
## 但不把 ol-ext 用于道路手柄
|
||||
|
||||
门禁通过只意味着 ol-ext **可以**用(决策表第一行的「可选依赖」),不意味着它是更好的选择。
|
||||
探针同时暴露了决策表第三行的情况,因此按该行处置。
|
||||
|
||||
### 决定性证据:手柄与约束值脱钩
|
||||
|
||||
```
|
||||
translateend -> raw -24.146 / draft -5.400
|
||||
```
|
||||
|
||||
`Transform` 的 translate 分支按原始 delta 调 `geometry.translate()`,不知道也不关心我们的钳位。
|
||||
于是手柄被拖到 -24.1 米处,而约束只到 -5.4 米——手柄停在道路永远不会变成的位置上。
|
||||
|
||||
生产要求相反:手柄必须贴着钳位边界停下,位置由**约束值反算**而来,而不是跟随光标。
|
||||
这意味着位置更新必须由我们自己拥有。用 ol-ext 就得每帧撤销它刚做的 translate;
|
||||
自己写 `PointerInteraction` 则直接不移动过界。
|
||||
|
||||
### 其余理由
|
||||
|
||||
- 我们实际用到的只有「Point proxy 上的 translate + start/move/end 生命周期 + hitTolerance 命中」。
|
||||
这些 `ol/interaction/Translate` 原生就有,且自带一等 TypeScript 类型。
|
||||
ol-ext 的增量价值是 bounding box 的 scale / stretch / rotate —— 而道路法线偏移、
|
||||
区间范围、路口 cutback 都用不上它,`canvas-integration.md` 早已指出这点。
|
||||
- ol-ext 4.0.38 不带类型,也没有 `@types/ol-ext`。采用它就要长期自己维护一份声明文件。
|
||||
探针期间已经踩到一次:它的自定义事件名不在 OL 的事件类型联合里,
|
||||
`on` / `un` 无法直接声明在类上,只能另设接口做一次转换。
|
||||
- 每个 mousemove 都会走 `handleMoveEvent_ → getFeatureAtPixel_ → forEachFeatureAtPixel`,
|
||||
触发 OL 的 canvas 回读(控制台 `Canvas2D getImageData` 警告即来自此)。
|
||||
这不是崩溃原因,但属于固有开销。
|
||||
|
||||
## 探针期间发现的、与 ol-ext 无关的教训
|
||||
|
||||
浏览器白屏一度被误当成 ol-ext 的稳定性问题,实际是探针自身的 bug:
|
||||
|
||||
```
|
||||
at projectHandleValue (projection.ts:49)
|
||||
at ProbeMap.tsx:133
|
||||
at basicStateReducer → updateReducer → useState
|
||||
```
|
||||
|
||||
几何计算被写在了 `setState` 的 updater 函数里。React 会延迟、且在 StrictMode 下重复调用
|
||||
updater;真正执行时闭包里的 `start` 已被 `translateend` 置为 `null`,于是 `toLonLat(null)` 抛错。
|
||||
`start!` 的非空断言正是把运行时问题藏过类型检查的地方。
|
||||
|
||||
**教训(适用于第 3、4 步)**:绝不在 `setState` updater 内做几何计算或读取实时 OL 状态。
|
||||
先在事件处理器里算出普通值,再传进 updater。
|
||||
|
||||
另一条:逐个 `translating` 事件更新 React state 会产生上千次渲染。探针用
|
||||
`requestAnimationFrame` 合并后才可用。生产要更进一步——ghost 直接写自己的 OL source,
|
||||
绝不为每次指针移动重渲染 React 树。
|
||||
|
||||
## 结论
|
||||
|
||||
按回退方案 1 推进:原生 OL `Snap` + 小型 `PointerInteraction` adapter,位置由约束值反算。
|
||||
|
||||
`HandleManifest → RoadEditOperation → RoadConstraint → preview solver` 数据合约不变,
|
||||
第 2 步已交付的 `EditSession` / `projection` / `meters` 全部保留,探针已验证它们可用。
|
||||
@@ -0,0 +1,70 @@
|
||||
# reserve 内手柄在当前服务端实现下不可达
|
||||
|
||||
第 3 步人工验证时发现:点遍所有道路都看不到灰色(不可拖)手柄。经实测确认这是**正确行为**,不是漏测。
|
||||
|
||||
## 实测数据
|
||||
|
||||
`test/fixtures/fengshu-er-road.osm`,24 条方向道路、32 个 reserve:
|
||||
|
||||
```
|
||||
road handles : 126
|
||||
road NOT editable : 0
|
||||
with disabledReason : 0
|
||||
junction handles : 96 (JunctionTools 所有,主地图已过滤)
|
||||
```
|
||||
|
||||
## 为什么不可达
|
||||
|
||||
`makeRoadHandles()` 里 `editable: false` 的唯一来源是 `unavailable`:
|
||||
|
||||
```js
|
||||
const start = Math.min(1, startReserve);
|
||||
const end = Math.max(0, 1 - endReserve);
|
||||
const unavailable = start >= end;
|
||||
```
|
||||
|
||||
而 `junctionReserves()` 给每端的保留区比例有硬上限:
|
||||
|
||||
```js
|
||||
const fraction = Math.min(0.45, cutback / length);
|
||||
```
|
||||
|
||||
于是 `startReserve ≤ 0.45`、`endReserve ≤ 0.45`,即 `start ≤ 0.45` 且 `end ≥ 0.55`。
|
||||
`start >= end` 结构上永远为假,`unavailable` 分支是死代码。
|
||||
|
||||
实测输出里能直接看到上限生效的样子——两端都撞到 0.45 的短路段:
|
||||
|
||||
```
|
||||
segment:way/858770822/1 window=[0.450,0.550]
|
||||
segment:way/117947564/0 window=[0.450,0.550]
|
||||
segment:way/851989492/2 window=[0.450,0.550]
|
||||
```
|
||||
|
||||
另外手柄的 station 取自 `(start + end) / 2`,即无保留区窗口的正中,
|
||||
所以三类道路手柄的位置也永远不会落进 reserve。
|
||||
|
||||
## 对验收标准的影响
|
||||
|
||||
prd.md 的「落在 junction reserve 内的手柄不可拖动,并提示进入 JunctionTools」
|
||||
对当前三类道路手柄是**空真**:它们永远不落在 reserve 内,所以约束自动成立。
|
||||
|
||||
客户端一侧的置灰与原因提示路径有单测覆盖(`selection.test.ts`:disabled 手柄被保留、
|
||||
`disabledReasonOf` 始终给出原因),只是服务端从不产生这种输入。因此第 3 步不需要改动。
|
||||
|
||||
reserve 边界真正变得用户可见是在第 4 步的**区间范围手柄**:它可以被拖向 reserve 边界,
|
||||
由 `projectIntervalEnd()` 钳到 `window.minStation` / `maxStation`。届时这条边界才有可演示的行为。
|
||||
|
||||
## 留给用户决定的设计问题
|
||||
|
||||
那个死分支暗示原作者期望「短路段应完全归 JunctionTools」。但 0.45 的上限把结果改成了:
|
||||
一条几何上被两个大路口主导的短路,仍然会在正中间得到一条仅占全长 10% 的可编辑带
|
||||
(上面 `[0.450,0.550]` 那几条)。
|
||||
|
||||
两种取向都讲得通,需要产品判断:
|
||||
|
||||
- 保持现状:任何道路都留一条可编辑带,哪怕很窄。
|
||||
- 改为:当 `cutback / length` 两端之和超过某阈值时,整条路标 `editable: false`,
|
||||
引导用户进入 JunctionTools。这会让死分支复活,也让验收标准变成可演示的。
|
||||
|
||||
改动落在 `src/compile/direct-edit-solver.js`(属已归档的 `direct-edit-solver-api` 任务范围),
|
||||
不在本任务范围内,故此处只记录不实施。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "direct-edit-map-editor",
|
||||
"name": "direct-edit-map-editor",
|
||||
"title": "主地图道路区间编辑器",
|
||||
"description": "ol-ext 限时探针与主地图外缘/步行带/车道分隔/区间范围拖拽",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-28",
|
||||
"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": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "逐字段核对 handle manifest、API 合约、409 前置条件与 degraded 标记是否与权威合约一致"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/joint-solver.md", "reason": "逐条核对约束不变量:最小车道宽 2.4m、外缘不交叉、路口连续、路口面不自交、connector 包含性"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "检查是否出现第二套米制换算或重复的 overrides 应用逻辑"}
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "检查约束 kind 与 manifest 字段在编译器、服务端响应之间是否各写一套"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "核对既有 /api/overrides、/api/compile、/api/state 行为与 baseline 输出未回归"}
|
||||
@@ -0,0 +1,22 @@
|
||||
# 设计
|
||||
|
||||
技术合约不在本文重复定义。权威定义见父任务 `.trellis/tasks/08-26-direct-manipulation-road-editor/design.md` 的以下小节:
|
||||
|
||||
- 「架构边界」— `resolveDirectEditConstraints` 在 `compileGeometry()` 之前的插入位置与数据流。
|
||||
- 「约束模型」— 6 个 kind 与各自的锚点、value、单位。
|
||||
- 「Handle manifest」— `HandleManifest` / `EditHandle` / `JunctionReserve` 字段。
|
||||
- 「坐标与单位分层」— 服务端在局部米制框架求解,传输一律 EPSG:4326 加方位角。
|
||||
- 「预览时序与延迟预算」— `previewSeq` 回显、`degraded` 标记、p95 ≤ 300ms。
|
||||
- 「编辑所有权」「不变量」— 两域划分与必须验证的几何不变量。
|
||||
- 「API 合约」— 5 个新端点、409 前置条件、既有端点的增量字段。
|
||||
- 「编译器几何版本变更」— `recheck` 策略。
|
||||
|
||||
## 本子任务的局部决定
|
||||
|
||||
- 求解器是纯函数,放在 `src/compile/` 下独立模块,不依赖 `fs` 与 `http`:预览、正式编译与 CLI 导出共用同一实现,这是"浏览器不维护第二套几何算法"的落点。
|
||||
- 米制换算全部走 `src/geometry/lane-geometry.js` 现有的 `metersAt()` / `haversineMeters()` / `projectedDistanceAlong()` / `lateralOffsetFrom()`。禁止新增换算函数;缺能力就扩展该模块。
|
||||
- handle manifest 与 diagnostics 是求解器的返回值,不是编译产物文件:预览不写盘,正式编译才落 GeoJSON。
|
||||
- `axisAzimuth` 由服务端算好,客户端只做投影。这样"哪个方向可拖"是语义决定而非 UI 猜测。
|
||||
- 重放匹配与求解分两个阶段:先把约束解析成 `exact` / `recheck` / `pending` / `conflicted` / `stale`,只有前两态进入求解。这样诊断与几何互不污染。
|
||||
- `POST /api/edit-preview` 复用现有请求体读取与 `sendJson` 工具,不引入新的 HTTP 框架。
|
||||
- 端点顺序:先 `GET /api/edit-state` 与 `POST /api/edit-preview`(只读/无写入,风险最低),再 `POST /api/edits` 与 revision 写入端点。
|
||||
@@ -0,0 +1,7 @@
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/design.md", "reason": "父任务权威合约:约束模型、handle manifest 字段、坐标分层、预览时序、不变量、API 合约"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/joint-solver.md", "reason": "求解阶段插入点、图层依赖链顺序与约束不变量的完整来源"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/current-system.md", "reason": "compileGeometry 现有输入输出、feature 回链字段与 v1 overrides 复用点"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/data-model-options.md", "reason": "分层求解规则与重放匹配策略的推导过程"}
|
||||
{"file": ".trellis/tasks/08-26-direct-manipulation-road-editor/research/junction-tools.md", "reason": "junction reserve 所有权划分与 approach 约束优先级的依据"}
|
||||
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "米制换算必须复用 src/geometry/lane-geometry.js,禁止新增第二套换算函数"}
|
||||
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "新增 API 载荷与 manifest 字段跨编译器/服务端/客户端三层"}
|
||||
@@ -0,0 +1,57 @@
|
||||
# 实施计划
|
||||
|
||||
对应父任务 `implement.md` 的第 4、5 步。每步一个提交,门禁不过就停。
|
||||
|
||||
前置:`direct-edit-documents` 已交付(v2 文档 schema、活动副本、area config 快照可读)。
|
||||
|
||||
## 1. 求解器骨架与恒等性
|
||||
|
||||
- 目标:插入 `resolveDirectEditConstraints` 阶段,且在无 v2 约束时是恒等变换。
|
||||
- 范围:`src/compile/` 新增纯函数模块;在 `compileGeometry()` 前接入;输出 editable profiles、junction plans、handle manifest、diagnostics 四件套的空实现。
|
||||
- 验证:`npm run test`,与 `direct-edit-documents` 建立的快照逐字节比对无差异。
|
||||
- 门禁:无 v2 约束时编译输出与快照完全一致。这条不过不许继续。快照必须是 `direct-edit-documents` 从当前 main 重新生成的那份,不是仓库原有的过期 `test/baseline/*.json`。
|
||||
- 回滚点:恒等变换,可安全单独 revert。
|
||||
|
||||
## 2. junction reserve 与 handle manifest
|
||||
|
||||
- 目标:先产出"能拖什么、不能拖什么",为客户端解锁并行开发。
|
||||
- 范围:junction reserve 计算;6 个 kind 的 handle 生成,含 `position`、`axisAzimuth`、`value.min/max`、`affects`、`editable` 与 `disabledReason`。
|
||||
- 验证:`npm run test` 新增 fixture——reserve 内的道路 handle 一律 `editable: false` 且带原因;`affects` 覆盖道路面、步行带、车道线、标线与 connector;`axisAzimuth` 与道路法线/切线方向一致。
|
||||
- 门禁:manifest 字段与父任务 `design.md`「Handle manifest」逐字段一致,客户端可据此开工。
|
||||
- 回滚点:纯新增返回值,无调用方时 revert 无影响。
|
||||
|
||||
## 3. 六个 kind 的求解与不变量
|
||||
|
||||
- 目标:约束真正改变几何,且违规时给阻塞性诊断。
|
||||
- 范围:`road-edge-offset`、`road-sidewalk-width`、`road-lane-divider`、`junction-approach-width`、`junction-cutback`、`junction-corner-radius`;interval 默认范围推导(避开两端 reserve);`smoothstep` 过渡;`junction-approach-width` 优先级与两域边界连续。
|
||||
- 验证:`npm run test` 新增 fixture——每个 kind 一条重放用例;最小车道宽 2.4m、左右外缘不交叉、道路/路口连续、路口面不自交、connector 包含性各一条违规用例返回阻塞诊断。
|
||||
- 门禁:违规一律诊断,绝不静默修复或钳制到合法值。
|
||||
- 回滚点:按 kind 分批提交,可单独回退某个 kind。
|
||||
|
||||
## 4. 重放匹配与状态机
|
||||
|
||||
- 目标:OSM 重导入后约束的 5 态分类可靠。
|
||||
- 范围:精确匹配(`roadId` / `segmentId` / `nodeId`)优先;失败才用 `anchorSnapshot` 的 OSM node、距离与切线找候选;编译器版本不一致时全部标 `recheck`。
|
||||
- 验证:`npm run test` 新增 fixture——同一 OSM 全 `exact`;删掉某条道路后相关约束 `stale`;`boundaryIndex` 越界 `stale`;候选歧义或偏差超阈值 `conflicted`;版本变化全 `recheck` 且值未变。
|
||||
- 门禁:`pending` / `conflicted` / `stale` 一律不进入求解,不存在静默模糊应用的路径。
|
||||
- 回滚点:匹配模块独立,revert 后退回仅精确匹配。
|
||||
|
||||
## 5. 只读端点
|
||||
|
||||
- 目标:先上无写入风险的两个端点。
|
||||
- 范围:`GET /api/edit-state`、`POST /api/edit-preview`;`previewSeq` 回显、`degraded` 标记;复用现有请求体读取与 `sendJson`。
|
||||
- 验证:`npm run test` 新增 server integration——预览调用前后工作区目录内容与 mtime 不变;`previewSeq` 原样回显;预览与正式编译对同一约束集给出相同几何;超预算时 `degraded: true` 而非报错。
|
||||
- 门禁:预览零写入,有测试证明。
|
||||
- 回滚点:新增路由,revert 后其余端点不受影响。
|
||||
|
||||
## 6. 写入端点与并发保护
|
||||
|
||||
- 目标:保存、revision 与 rebase 落地,并挡住多标签页覆盖。
|
||||
- 范围:`POST /api/edits`(要求 `expectedDocumentVersion`)、`POST /api/revisions`、`POST /api/revisions/:id/rebase`;`/api/state`、`/api/session`、`/api/import` 增量字段。
|
||||
- 验证:`npm run test` 新增 server integration——版本不匹配返回 409 且不写入;原子写入中途失败不留半份文件;rebase 返回各 status 计数与明细;`/api/import` 成功后返回基线 revision id;既有 `/api/overrides`、`/api/compile`、`/api/state` 字段与行为不变。
|
||||
- 门禁:409 路径与原子失败路径都有测试覆盖;既有端点无回归。
|
||||
- 回滚点:写入端点独立,revert 后回到只读预览。
|
||||
|
||||
## 步骤依赖
|
||||
|
||||
1 → 2 → 3 硬顺序;4 可与 3 并行;5 依赖 2;6 依赖 5 与 `direct-edit-documents`。第 2 步交付后客户端子任务即可并行开工。
|
||||
@@ -0,0 +1,42 @@
|
||||
# 约束求解器与编辑 API
|
||||
|
||||
父任务:`.trellis/tasks/08-26-direct-manipulation-road-editor`。需求来源与权威合约在父任务的 `prd.md` / `design.md`。
|
||||
|
||||
## 目标
|
||||
|
||||
把约束解析从几何生成中抽成纯函数边界,并用它同时支撑草稿预览与正式编译;对外提供预览、保存、revision 与 rebase 的服务端能力。本子任务不含任何 UI。
|
||||
|
||||
## 顺序依赖
|
||||
|
||||
前置:`direct-edit-documents`(需要 v2 文档 schema、`documentVersion`、活动副本与 area config 快照读取)。
|
||||
后继:`direct-edit-map-editor` 与 `direct-edit-junction-tools` 依赖本任务的 handle manifest 与预览 API。
|
||||
|
||||
## 范围
|
||||
|
||||
- `resolveDirectEditConstraints` 纯函数:输入基线模型 + v1 覆盖 + v2 约束,输出 editable profiles、junction plans、handle manifest、diagnostics。
|
||||
- 6 个约束 kind 的求解;interval 默认范围推导(避开两端 junction reserve);junction reserve 计算。
|
||||
- `junction-approach-width` 对道路 profile 的优先级,以及两域边界的连续过渡。
|
||||
- 约束重放匹配:精确匹配优先,失败才按 `anchorSnapshot` 找候选,产出 5 态 `ConstraintStatus`。
|
||||
- 端点:`GET /api/edit-state`、`POST /api/edit-preview`、`POST /api/edits`、`POST /api/revisions`、`POST /api/revisions/:id/rebase`。
|
||||
- `previewSeq` 回显、`degraded` 标记、`expectedDocumentVersion` 前置条件与 409。
|
||||
- `/api/state`、`/api/session`、`/api/import` 的增量字段。
|
||||
|
||||
## 不做
|
||||
|
||||
- 任何客户端改动、手柄渲染或拖拽交互。
|
||||
- cluster 的高级求解(只保证 cluster 不被当成多个普通路口处理)。
|
||||
- 多人协作的并发合并策略。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 求解是纯函数:无文件写入、无网络调用,可用单元 fixture 直接驱动。
|
||||
- [ ] 无 v2 约束时编译输出与当前 main 逐字节一致(baseline fixture 比对)。
|
||||
- [ ] 6 个 kind 各有重放 fixture;预览与正式编译对同一约束集给出相同几何。
|
||||
- [ ] 违反最小车道宽 2.4m、左右外缘交叉、路口面自交、connector 越界时返回阻塞性诊断而非静默修复。
|
||||
- [ ] `boundaryIndex` 越界转 `stale`;锚点缺失转 `stale`;候选歧义或偏差超阈值转 `conflicted`。
|
||||
- [ ] 编译器几何版本不一致时约束全部标 `recheck`,不自动改值也不自动失效。
|
||||
- [ ] `POST /api/edit-preview` 调用前后工作区目录内容与 mtime 不变。
|
||||
- [ ] `expectedDocumentVersion` 不匹配返回 409 且不写入;原子写入中途失败不留半份文件。
|
||||
- [ ] rebase 返回各 status 的计数与逐条明细。
|
||||
- [ ] 既有 `/api/overrides`、`/api/compile`、`/api/state` 的字段与行为不变。
|
||||
- [ ] `npm run format:check`、`npm run test`、`npm run test:client`、`npm run build` 全绿。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "direct-edit-solver-api",
|
||||
"name": "direct-edit-solver-api",
|
||||
"title": "约束求解器与编辑 API",
|
||||
"description": "resolveDirectEditConstraints 纯函数边界,以及预览/保存/revision/rebase API",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-27",
|
||||
"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": {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"_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."}
|
||||
{"file":".trellis/spec/frontend/index.md","reason":"Review frontend quality, component, hook, and state conventions."}
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"Verify API-to-React-to-OpenLayers data flow and cleanup boundaries."}
|
||||
{"file":".trellis/spec/guides/code-reuse-thinking-guide.md","reason":"Check that migration does not leave duplicate DOM and React implementations or duplicate map logic."}
|
||||
@@ -0,0 +1,32 @@
|
||||
# Technical Design
|
||||
|
||||
## Frontend Boundary
|
||||
|
||||
Create a Vite app under `workbench/client/` (or a clearly scoped `workbench/frontend/` directory) with React 19 and TypeScript. Keep Node/server code CommonJS and keep `/api/*` payloads unchanged. The production build should emit a deterministic directory consumed by `workbench/server.js`; development mode may use a Vite proxy to the workbench API.
|
||||
|
||||
## OpenLayers Migration Strategy
|
||||
|
||||
- Import OpenLayers modules directly from the installed `ol` package in TypeScript; remove the runtime `/vendor/ol/*.js` import map from the React build.
|
||||
- Keep one `Map` instance per mounted map component. A `useOpenLayersMap` hook creates it once after the container ref is available, registers interactions/listeners, and calls `map.setTarget(undefined)` plus listener cleanup on unmount.
|
||||
- Keep the layer registry in an adapter module/hook (`map/layers.ts`), created once with stable `VectorSource` and `VectorLayer` instances. React state changes update sources, styles, visibility, and selection overlays through explicit adapter methods rather than recreating the map.
|
||||
- Use typed GeoJSON/read helpers at the API boundary; preserve EPSG:4326 to EPSG:3857 conversion and current fit/selection behavior.
|
||||
- OpenLayers event callbacks publish typed selection/pointer events to React state. React panels render from that state and issue API mutations; they do not query or mutate DOM nodes owned by OpenLayers.
|
||||
- Import `ol/ol.css` from the Vite entry and keep map container dimensions in application CSS.
|
||||
|
||||
## UI and State
|
||||
|
||||
- Use shadcn/ui primitives for buttons, inputs, checkboxes, select, tabs/segmented filters, dialogs, sheets, alerts, and toast feedback. Keep the existing dense three-column workbench layout.
|
||||
- Define API/state types in `src/types/` based on the actual `/api/state` and mutation payloads. Use a small typed client in `src/lib/api.ts`.
|
||||
- Use React hooks/context for session and editor state. Keep transient map selection and staged overrides separate from server state; avoid introducing a large state library unless the migration proves one necessary.
|
||||
- Split components by existing workflow boundaries: `ImportScreen`, `WorkbenchShell`, `LayerPanel`, `DiagnosticsPanel`, `MapCanvas`, `RoadInspector`, `SignalEditor`, and shared `ui/` primitives.
|
||||
|
||||
## Static Hosting
|
||||
|
||||
- Update `workbench/server.js` to serve Vite `index.html` and hashed assets in production, with a safe fallback for the SPA route while retaining `/vendor/*` only for compatibility during transition.
|
||||
- Keep `npm run road:workbench` self-contained for production assets. Add Vite dev/proxy scripts without requiring the Node workbench server to become an ESM application.
|
||||
|
||||
## Risks and Compatibility
|
||||
|
||||
- OpenLayers owns imperative objects and must not be recreated on every React render; the hook/adapter boundary is mandatory.
|
||||
- Existing style functions close over selected road state. Convert them to adapter-managed mutable selection refs or call `layer.changed()` after selection updates.
|
||||
- The first migration should preserve behavior and layout; visual redesign is explicitly deferred.
|
||||
@@ -0,0 +1,4 @@
|
||||
{"_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."}
|
||||
{"file":".trellis/spec/frontend/index.md","reason":"Load the project frontend conventions before introducing Vite, React, TypeScript, and shadcn/ui."}
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"The migration changes the browser build, static hosting, API client, and imperative map lifecycle together."}
|
||||
{"file":".trellis/spec/guides/code-reuse-thinking-guide.md","reason":"Reuse current OpenLayers styles, API payloads, and editor behavior instead of duplicating domain logic."}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Implementation Plan
|
||||
|
||||
1. Add Vite, React 19, TypeScript, Tailwind/shadcn/ui dependencies and scripts; establish `tsconfig`, Vite config, aliases, CSS variables, and production output location.
|
||||
2. Define typed API/state contracts and a fetch client from the existing `/api/*` payloads; add focused serialization tests.
|
||||
3. Build the OpenLayers adapter and `useOpenLayersMap` hook. Port the current layer registry, styles, source updates, selection interaction, fit behavior, and cleanup tests before moving inspector UI.
|
||||
4. Port the import-first flow and shell/layout to React, then migrate layer controls, summaries, diagnostics, road inspector, connection editor, centerline editor, signal editor, and save/compile actions.
|
||||
5. Replace the server's legacy `/app.js`/`/app.css` production serving with Vite build output while retaining a compatibility path only where needed for rollback.
|
||||
6. Remove the old DOM-driven entry after parity is demonstrated; keep OpenLayers vendor serving only if legacy mode still needs it.
|
||||
7. Add browser/component smoke coverage for no-session import, active-session map render, a road selection/edit, and compile/export request wiring.
|
||||
8. Run `npm run build`, frontend tests, existing `npm test`, and a manual workbench smoke check at desktop/mobile widths; fix parity regressions.
|
||||
|
||||
Validation commands:
|
||||
|
||||
- `npm run build`
|
||||
- `npm run format:check`
|
||||
- frontend test command added by this task
|
||||
- `npm test`
|
||||
- `node --check workbench/server.js`
|
||||
|
||||
Rollback point: retain the legacy client in a separate compatibility path until the React build serves the same API workflow and OpenLayers interaction checks pass.
|
||||
@@ -0,0 +1,43 @@
|
||||
# 迁移工作台到 React 前端
|
||||
|
||||
## Goal
|
||||
|
||||
将现有原生 HTML/CSS/JS 工作台迁移到 Vite、React 19、TypeScript 和 shadcn/ui,获得可维护、可扩展且具备类型安全的成熟前端工程结构,同时保留当前 OSM 导入、地图查看、道路参数编辑、诊断、信号灯编辑、重新编译和道路包导出能力。
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- 当前前端位于 `workbench/client/index.html`、`workbench/client/app.js` 和 `workbench/client/app.css`。
|
||||
- `workbench/client/app.js` 使用 OpenLayers 浏览器模块,并动态创建部分控制面板和图层开关。
|
||||
- `workbench/server.js` 提供静态资源和 `/api/state`、`/api/import`、`/api/overrides`、`/api/traffic-signals`、`/api/compile`、`/api/export.zip` 等接口。
|
||||
- 当前项目是 CommonJS Node 包,尚无前端构建脚本、TypeScript 配置、Vite 配置或 shadcn/ui 依赖。
|
||||
- 地图核心继续使用已有的 OpenLayers 10;本任务是前端工程迁移,不替换地图引擎。
|
||||
|
||||
## Requirements
|
||||
|
||||
1. 建立独立、可构建的 Vite React 19 TypeScript 前端入口并迁移工作台代码。
|
||||
2. 配置 shadcn/ui 所需的 CSS 变量、组件基础设施和图标/交互约定,优先使用其组件承载按钮、表单、面板、弹层和提示。
|
||||
3. 将页面状态、API 调用、地图图层/选择逻辑和编辑表单拆分为 React 组件、hooks、类型和服务模块,避免主要业务 UI 继续依赖全局 `querySelector` 和动态 `innerHTML`。
|
||||
4. 保留现有服务端 API 契约和 OSM 导入工作流;必要时只调整静态资源托管和开发/生产构建入口。
|
||||
5. 保留 OpenLayers 地图的现有图层、样式、选择和交互行为,并确保状态变化正确驱动地图与检查器更新。
|
||||
6. 提供开发模式和生产构建命令;Node 工作台服务能提供构建后的前端资源。
|
||||
7. 为核心 API 客户端、状态转换和至少一个关键交互增加 TypeScript/自动化验证。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `npm run build` 能在干净依赖安装后生成前端生产构建产物。
|
||||
- `npm run dev`(或等价命令)能启动 Vite 开发服务器并加载工作台页面。
|
||||
- `npm run road:workbench` 使用构建产物时仍能打开工作台;无活动工作区时显示 OSM 导入界面,有活动工作区时显示地图和编辑器。
|
||||
- OSM 导入、图层开关、道路选择/参数暂存、诊断筛选、交通信号编辑、保存、重新编译和 ZIP 导出均保持可用。
|
||||
- 页面不再通过原生 `querySelector`/`innerHTML` 组织主要业务 UI;业务状态和 API 数据具有明确 TypeScript 类型。
|
||||
- 现有 Node 编译器测试继续通过;新增前端构建和关键工作流验证通过。
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- 不修改道路编译算法、OSM 解析规则、GeoJSON 数据结构或现有 HTTP API 语义。
|
||||
- 不更换 OpenLayers 为其他地图引擎。
|
||||
- 不引入认证、多用户协作、服务端渲染或远程部署平台。
|
||||
- 不在本任务内重新定义道路参数模型或增加新的业务功能。
|
||||
|
||||
## Open Product Decision
|
||||
|
||||
迁移是否同时允许明显的视觉和信息架构重设计?推荐第一阶段以“功能和工作流等价 + shadcn/ui 统一视觉”为目标,保留现有三栏地图工作台布局;这样可以把风险集中在工程迁移,后续再单独做 UX 重构。若本任务同时重做布局,交付周期和回归范围会显著增加。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "react-workbench-migration",
|
||||
"name": "react-workbench-migration",
|
||||
"title": "迁移工作台到 React 前端",
|
||||
"description": "将现有原生 HTML/CSS/JS 工作台迁移到 Vite、React 19、TypeScript 和 shadcn/ui,保持现有工作流能力",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,15 @@
|
||||
# 修复场景效果预览
|
||||
|
||||
## Goal
|
||||
|
||||
恢复 React 工作台的场景效果:开启后道路面显示无边线的深灰沥青效果,步行带显示灰色填充;关闭后回到编辑样式。
|
||||
|
||||
## Cause
|
||||
|
||||
`MapCanvas` 只在挂载时创建 OpenLayers 图层,其 style callback 捕获了首次渲染的 `scene` 值,后续切换无法读取当前状态。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- 切换场景效果后,原生道路面和步行带立即切换到对应场景样式。
|
||||
- 切换不重建 `Map`、图层或 feature source。
|
||||
- `npm run format:check`、`npm run test:client` 和 `npm run build` 通过。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "scene-preview-fix",
|
||||
"name": "scene-preview-fix",
|
||||
"title": "修复场景效果预览",
|
||||
"description": "修复 React OpenLayers 图层样式捕获初始 scene 状态导致沥青场景效果未生效的问题",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"Verify upload-to-compile data flow and session boundary."}
|
||||
{"file":".trellis/spec/guides/code-reuse-thinking-guide.md","reason":"Verify no duplicate compiler or parser logic was introduced."}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Technical Design
|
||||
|
||||
## Boundaries
|
||||
|
||||
- `bin/road-workbench.js` owns process startup and creates an import session/workspace.
|
||||
- `workbench/server.js` owns HTTP routes and session-local files; it continues to call `compileInput()` for compilation.
|
||||
- `workbench/client/index.html` and `workbench/client/app.js` add the upload/bootstrap state only; existing editing panels remain unchanged.
|
||||
- `src/compile/compiler.js` and `src/osm.js` remain the source of truth for input validation and OSM processing.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. CLI starts a workbench with an optional existing `--input` for compatibility, or with no input for the new import-first flow.
|
||||
2. The browser submits multipart OSM content (bounded by a server upload limit) to a new session/import endpoint.
|
||||
3. The server validates the filename/content, creates a unique workspace directory under a configured workbench data root, and writes:
|
||||
- `source.osm`
|
||||
- `native-road-overrides.json` with the existing empty schema
|
||||
- `native-traffic-signals.json` with the existing empty schema
|
||||
- `RoadCompilerInput.json` using conservative defaults and workspace-relative output paths
|
||||
4. The server calls `compileInput()` and swaps the active session from the import screen to the existing state/map view.
|
||||
5. Existing routes operate against the active session's `area` object. A failed import/compile removes only the new staging directory and leaves any prior active session untouched.
|
||||
|
||||
## Defaults and Compatibility
|
||||
|
||||
- Area id is derived from a sanitized user-provided name or uploaded basename, with a unique suffix when necessary.
|
||||
- `options.edgeLines` and `options.junctionTemplates` use the same shape required by `validateInput()`; no new compiler options are introduced.
|
||||
- Existing `--input RoadCompilerInput.json` startup remains supported by adapting it into the same active-session context.
|
||||
- The server binds to localhost as it does today and does not add authentication or remote persistence.
|
||||
|
||||
## Error and Recovery
|
||||
|
||||
- Reject empty/non-XML uploads, oversized bodies, malformed OSM, and compile failures with JSON errors suitable for the browser.
|
||||
- Stage all files before switching the active session; cleanup on failure.
|
||||
- Keep workspace directories recoverable on disk; do not delete an existing user workspace during a new import.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Unit/integration tests for workspace initialization, default input generation, upload size/content validation, and failed-import cleanup.
|
||||
- HTTP smoke test for import -> state -> compile/export using the existing fixture OSM.
|
||||
- Existing compiler and fixture tests remain the regression gate.
|
||||
@@ -0,0 +1,2 @@
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"Import spans CLI, HTTP server, filesystem workspace, compiler, and browser state."}
|
||||
{"file":".trellis/spec/guides/code-reuse-thinking-guide.md","reason":"Reuse existing compiler input validation, atomic JSON writes, and workbench state routes."}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Implementation Plan
|
||||
|
||||
1. Read frontend/backend project specs and map the current workbench startup contract.
|
||||
2. Extract small, testable helpers for session workspace creation, default `RoadCompilerInput`, and bounded OSM request handling.
|
||||
3. Repair `bin/road-workbench.js` to support import-first startup while adapting legacy `--input` files.
|
||||
4. Add server endpoints for upload/bootstrap and active-session state, preserving all existing edit routes.
|
||||
5. Add a minimal import screen and transition in the existing client; keep the map/editor UI intact.
|
||||
6. Add tests for helper validation and an HTTP import smoke path using `test/fixtures/fengshu-er-road.osm`.
|
||||
7. Run `npm test`, targeted workbench tests, and manual localhost smoke checks; fix issues found.
|
||||
8. Run Trellis quality check, update relevant specs if a durable convention is discovered, then commit.
|
||||
|
||||
Validation commands:
|
||||
|
||||
- `npm test`
|
||||
- `node --check bin/road-workbench.js`
|
||||
- `node --check workbench/server.js`
|
||||
- targeted workbench test command added by this task
|
||||
|
||||
Rollback points: CLI/server changes can be reverted independently of compiler sources; workspace staging ensures an import failure does not alter an existing session.
|
||||
40
.trellis/tasks/archive/2026-08/08-26-web-osm-import/prd.md
Normal file
40
.trellis/tasks/archive/2026-08/08-26-web-osm-import/prd.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Web OSM 导入工作流
|
||||
|
||||
## Goal
|
||||
|
||||
让用户无需预先编写 `RoadCompilerInput.json` 或宿主区域配置,即可通过 Web 工作台上传一个 `.osm` 文件,创建一次可编译的工作区并开始查看、调整和导出道路结果。
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- `src/compile/compiler.js` 已提供完整的 `compileInput()`,负责读取 OSM、加载 overrides、生成原生道路图层、诊断和交通信号运行时资产。
|
||||
- `src/osm.js` 已提供 OSM XML 解析;`src/compile/native-road.js` 已提供道路模型、几何编译和 overrides 校验。
|
||||
- `workbench/server.js` 已提供地图状态、覆盖项保存、交通信号编辑/生成、重新编译和 ZIP 导出接口,但入口假定已有 `area`、配置加载器和编译回调。
|
||||
- `workbench/client/` 已有完整的 OpenLayers 编辑界面,当前通过 `/api/state` 加载既有编译结果。
|
||||
- `bin/road-workbench.js` 当前要求 `--input <RoadCompilerInput.json>`,却把 `{ input, inputFile, port }` 传给期待另一种上下文的 `startWorkbench()`,无法独立启动现有工作台。
|
||||
|
||||
## Requirements
|
||||
|
||||
1. 工作台启动后提供 OSM 文件导入入口;成功导入后自动建立编译所需的工作区文件和默认参数。
|
||||
2. 导入流程复用现有 `compileInput()` 与已有编辑 API,不复制道路解析或几何编译逻辑。
|
||||
3. 导入后自动执行首次编译,并让现有地图、诊断、覆盖项、交通信号编辑和 ZIP 导出继续可用。
|
||||
4. 导入失败时返回可理解的错误,不破坏当前已加载的工作区。
|
||||
5. 保留通过现有 `RoadCompilerInput.json` 启动工作台的兼容路径(若当前入口契约可修复则继续支持)。
|
||||
6. 默认参数应明确、可追溯,并允许用户在首次编译后通过已有工作台控件调整;本任务不重新设计道路算法或参数模型。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- 用户运行工作台命令并打开页面,可以选择 `.osm` 文件并提交。
|
||||
- 服务端保存上传内容,生成有效的 overrides、traffic-signals、输出目录和 `options`,然后完成一次 `compileInput()`;页面显示道路图层和编译诊断。
|
||||
- `/api/state`、`/api/overrides`、`/api/traffic-signals`、`/api/compile`、`/api/export.zip` 在导入工作区中均正常工作。
|
||||
- 非法文件、空文件、超过限制的上传或编译错误不会留下半成品工作区,并在页面显示错误。
|
||||
- 现有测试继续通过,并新增覆盖入口/上传/初始化链路的自动化测试。
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- 修改 OSM 解析规则、道路几何算法、交通信号生成算法或导出包格式。
|
||||
- 多用户认证、远程持久化、数据库、云端 OSM 下载和在线协作。
|
||||
- 重新设计现有工作台地图编辑 UI。
|
||||
|
||||
## Key Decision
|
||||
|
||||
每次导入创建独立的本地工作区目录并在当前工作台会话中使用,避免覆盖已有区域配置。工作区保留在磁盘上,后续可通过兼容的 `--input` 方式恢复。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "web-osm-import",
|
||||
"name": "web-osm-import",
|
||||
"title": "Web OSM 导入工作流",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,18 @@
|
||||
# 优化工作台图层与场景标线
|
||||
|
||||
## Goal
|
||||
|
||||
移除不再需要的高德规整路口参考图层,提供可读的编译概览,并恢复场景模式下双向道路中心线的可辨识渲染。
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- 科技园一路的三个双向路段在 `center_lines.geojson` 中都有完整的黄色虚线 feature;不是编译缺失。
|
||||
- React adapter 将原版中心线的属性驱动样式简化为固定样式,造成场景模式下辨识度不正确。
|
||||
- 高德参考层仅在 React 前端图层 registry、图层开关和状态装载中使用;不影响编译。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- 工作台不显示或加载高德规整路口参考图层。
|
||||
- 场景模式下保留中心线,并按线的颜色和虚/实线属性清晰渲染。
|
||||
- 编译概览以中文分组指标和诊断摘要展示,不显示 schema、note、null 或 `[object Object]`。
|
||||
- 格式化、类型检查和生产构建通过。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "workbench-layer-scene-polish",
|
||||
"name": "workbench-layer-scene-polish",
|
||||
"title": "优化工作台图层与场景标线",
|
||||
"description": "移除高德规整路口参考,优化编译概览,并修复场景模式下科技园一路中心线缺失",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,16 @@
|
||||
# 修复工作台选择与保存回归
|
||||
|
||||
## Goal
|
||||
|
||||
道路选择只更新选择高亮,不重置地图数据或视图;保存修改后可靠保留并重新编译 overrides。
|
||||
|
||||
## Confirmed Cause
|
||||
|
||||
`MapCanvas` 使用 `[state, selected]` 更新全量 source 和 fit view,选择道路会触发全图重载。Road override 的前后端 schema 一致,因此保存回归需在 UI 操作后验证其状态同步和编译调用。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- 选择道路不会清空 source、重新 fit 地图或改变视图。
|
||||
- 仅选择高亮 layer 更新;选择相关 OSM 线条样式仍刷新。
|
||||
- 暂存、保存、重新编译的 overrides 正确发送并以服务端响应更新状态。
|
||||
- 格式化、类型检查和构建通过。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "workbench-selection-save-fix",
|
||||
"name": "workbench-selection-save-fix",
|
||||
"title": "修复工作台选择与保存回归",
|
||||
"description": "修复道路选择触发全图层重建,并排查修复 React 工作台保存 overrides 回归",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,68 @@
|
||||
# 实施计划
|
||||
|
||||
轻量任务,一个提交即可。门禁不过就停。
|
||||
|
||||
前置:无。客户端置灰渲染路径已由 `08-26-direct-edit-map-editor` 第 3 步交付并有单测覆盖,
|
||||
本任务只补服务端输入。
|
||||
|
||||
## 关键决定:置灰只影响手柄,不影响求解
|
||||
|
||||
`editable: false` 只改变 handle manifest,**不改变 `solveConstraints()` 的行为**。
|
||||
落在被置灰 segment 上的既有约束仍然照常应用、照常参与几何编译。
|
||||
|
||||
理由:否则本次改动会让升级后的编译静默丢掉用户已保存的编辑——一个 UI 可编辑性的判断
|
||||
不该回溯否决已持久化的意图。用户想撤销这类编辑,走 undo / 禁用约束的正常路径。
|
||||
|
||||
这也让「几何输出逐字节不变」这条验收标准成立,从而使本步骤可安全回滚。
|
||||
|
||||
## 1. 判据与置灰
|
||||
|
||||
- 目标:可编辑带短于道路宽度的 segment,其全部 `road-*` 手柄 `editable: false`。
|
||||
- 位置:`src/compile/direct-edit-solver.js` 的 `makeRoadHandles()`。
|
||||
该函数已算出 `start` / `end` / `length` / `width`,判据是纯本地计算,不需要新数据。
|
||||
- 实现要点:
|
||||
- 带长米数 `(end - start) * length`,与 `width`(该 segment 双向宽度之和)比较。
|
||||
- 复用既有 `baseDisabled` 机制与既有文案「该道路全部位于路口保留区,请进入 JunctionTools 编辑。」,
|
||||
不新增第二套提示语。
|
||||
- 保留原 `unavailable` 判断作为兜底,两者取或。
|
||||
- 被置灰的 segment 仍然产出手柄(位置照旧),只是 `editable: false` 且带 `disabledReason`。
|
||||
- 不改:`junctionReserves()` 的 0.45 上限、cutback 推导、`solveConstraints()`、junction 手柄。
|
||||
|
||||
## 2. 测试
|
||||
|
||||
- 位置:`test/direct-edit-solver.js`。
|
||||
- 覆盖:
|
||||
- 短路段(带长 < 宽度)的全部 `road-*` 手柄 `editable: false` 且 `disabledReason` 非空。
|
||||
- 正常路段 `editable: true` 且无 `disabledReason`。
|
||||
- 判据边界:带长略小于 / 略大于宽度两侧各一例。
|
||||
- junction 手柄仍 `editable: true`。
|
||||
- 置灰 segment 上的既有约束仍被求解(`constraintStates` 里 `applied: true`),
|
||||
证明置灰没有回溯否决已保存编辑。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
```bash
|
||||
npm run test # 含 test/direct-edit-solver.js
|
||||
npm run test:client # 客户端类型
|
||||
npm run test:client:unit # 客户端纯逻辑
|
||||
npm run format:check
|
||||
npm run build
|
||||
```
|
||||
|
||||
几何不变的实测(本任务的核心安全性证明):对 `test/fixtures/fengshu-er-road.osm`
|
||||
在改动前后各跑一次 `compileGeometry`,比较全部输出图层的 JSON,必须完全一致。
|
||||
`npm run test` 里的 fixture 基线测试已覆盖这条,若基线有 diff 即为回归。
|
||||
|
||||
手测(可选,`directEdit` 开关打开):选中 `test/fixtures` 里那 6 段短路之一,
|
||||
应看到灰色手柄且 header 显示原因。这是父任务 prd 那条验收标准第一次真正可演示。
|
||||
|
||||
## 4. 门禁
|
||||
|
||||
- 几何输出零变化(fixture 基线无 diff)。
|
||||
- 正常路段行为与当前 main 完全一致。
|
||||
- 既有约束不因置灰而失效。
|
||||
|
||||
## 回滚点
|
||||
|
||||
单文件单函数改动,`git revert` 即可回到当前行为。
|
||||
客户端不需要任何配合改动,回滚后灰色手柄自然消失,回到「空真」状态。
|
||||
@@ -0,0 +1,87 @@
|
||||
# 路口主导的短路段整条置灰
|
||||
|
||||
父任务:`.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 步交付并有单测覆盖,本任务只补上服务端的输入。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "junction-dominated-roads",
|
||||
"name": "junction-dominated-roads",
|
||||
"title": "路口主导的短路段整条置灰",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-27",
|
||||
"completedAt": "2026-08-28",
|
||||
"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": {}
|
||||
}
|
||||
42
.trellis/workspace/dingkang/index.md
Normal file
42
.trellis/workspace/dingkang/index.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Workspace Index - dingkang
|
||||
|
||||
> Journal tracking for AI development sessions.
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
|
||||
<!-- @@@auto:current-status -->
|
||||
- **Active File**: `journal-1.md`
|
||||
- **Total Sessions**: 2
|
||||
- **Last Active**: 2026-08-27
|
||||
<!-- @@@/auto:current-status -->
|
||||
|
||||
---
|
||||
|
||||
## Active Documents
|
||||
|
||||
<!-- @@@auto:active-documents -->
|
||||
| File | Lines | Status |
|
||||
|------|-------|--------|
|
||||
| `journal-1.md` | ~163 | Active |
|
||||
<!-- @@@/auto:active-documents -->
|
||||
|
||||
---
|
||||
|
||||
## Session History
|
||||
|
||||
<!-- @@@auto:session-history -->
|
||||
| # | Date | Title | Commits | Branch |
|
||||
|---|------|-------|---------|--------|
|
||||
| 2 | 2026-08-27 | Archive direct-edit solver API | `a26d4be` | `feat/direct-edit-documents` |
|
||||
| 1 | 2026-08-27 | Complete direct edit document and revision foundation | `93f09e3`, `3500237`, `c0c8a16`, `ba8eaba` | `feat/direct-edit-documents` |
|
||||
<!-- @@@/auto:session-history -->
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Sessions are appended to journal files
|
||||
- New journal file created when current exceeds 2000 lines
|
||||
- Use `add_session.py` to record sessions
|
||||
163
.trellis/workspace/dingkang/journal-1.md
Normal file
163
.trellis/workspace/dingkang/journal-1.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Journal - dingkang (Part 1)
|
||||
|
||||
> AI development session journal
|
||||
> Started: 2026-08-26
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Session 1: Complete direct edit document and revision foundation
|
||||
|
||||
**Date**: 2026-08-27
|
||||
**Task**: Complete direct edit document and revision foundation
|
||||
**Branch**: `feat/direct-edit-documents`
|
||||
|
||||
### Summary
|
||||
|
||||
Completed the v2 direct-edit document, immutable revision storage, and area-config snapshot foundation.
|
||||
|
||||
### Main Changes
|
||||
|
||||
- Added validated native-road-edits/v2 documents, revisions, content-addressed OSM, and snapshot-only compilation.
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `93f09e3` | (see git log) |
|
||||
| `3500237` | (see git log) |
|
||||
| `c0c8a16` | (see git log) |
|
||||
| `ba8eaba` | (see git log) |
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] npm run format:check; npm run test; npm run test:client; npm run test:client:unit; npm run build
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Start the direct-edit solver and API child task.
|
||||
|
||||
## Session 2: Direct-edit handle manifest
|
||||
|
||||
**Date**: 2026-08-27
|
||||
**Task**: Continue `direct-edit-solver-api`, step 2
|
||||
**Git Commit**: `ab63d23`
|
||||
|
||||
### Summary
|
||||
|
||||
Added junction reserve calculation, six-kind handle manifest generation, and
|
||||
shared native feature ID builders. The solver remains pure and empty documents
|
||||
remain an identity transform for compiled geometry.
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] npm test; npm run format:check; npm run test:client; npm run test:client:unit; npm run build
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Implement the six constraint kinds and invariant diagnostics (step 3).
|
||||
|
||||
## Session 3: Direct-edit constraint solving
|
||||
|
||||
**Date**: 2026-08-27
|
||||
**Task**: Continue `direct-edit-solver-api`, step 3
|
||||
**Git Commit**: `0eb404d`
|
||||
|
||||
### Summary
|
||||
|
||||
Activated all six constraint kinds, added transactional validation and blocking
|
||||
diagnostics, and wired road profiles and junction plans into geometry compilation.
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] npm test; npm run format:check; npm run test:client; npm run test:client:unit; npm run build
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Implement exact/recheck/pending/conflicted/stale replay matching (step 4).
|
||||
|
||||
## Session 4: Direct-edit replay matching
|
||||
|
||||
**Date**: 2026-08-27
|
||||
**Task**: Continue `direct-edit-solver-api`, step 4
|
||||
**Git Commit**: `735ce69`
|
||||
|
||||
### Summary
|
||||
|
||||
Added exact-anchor replay classification, snapshot candidate fallback, stale
|
||||
lane-boundary handling, and compiler-geometry `recheck` propagation.
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] npm test; npm run format:check; npm run test:client; npm run test:client:unit; npm run build
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Add the read-only edit-state and edit-preview endpoints (step 5).
|
||||
|
||||
## Session 5: Read-only edit APIs
|
||||
|
||||
**Date**: 2026-08-27
|
||||
**Task**: Continue `direct-edit-solver-api`, step 5
|
||||
**Git Commit**: `41c314f`
|
||||
|
||||
### Summary
|
||||
|
||||
Added `GET /api/edit-state` and `POST /api/edit-preview`. Preview compiles from
|
||||
the draft document without writing active files and returns sequence, degraded
|
||||
status, diagnostics, handles, and derived layers.
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] npm test; npm run format:check; npm run test:client; npm run test:client:unit; npm run build
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Implement edit/revision/rebase writes with expected-version protection (step 6).
|
||||
|
||||
## Session 6: Edit persistence and rebase APIs
|
||||
|
||||
**Date**: 2026-08-27
|
||||
**Task**: Complete `direct-edit-solver-api`, step 6
|
||||
**Git Commit**: `f3f763f`
|
||||
|
||||
### Summary
|
||||
|
||||
Added atomic active-document saves with expected-version 409 protection,
|
||||
revision checkpoint creation, and read-only revision rebase status summaries.
|
||||
Existing state/session responses now expose active revision and document version.
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] npm test; npm run format:check; npm run test:client; npm run test:client:unit; npm run build
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Run the solver-api quality review, then archive the child task before starting
|
||||
the map-editor child task.
|
||||
|
||||
|
||||
## Session 2: Archive direct-edit solver API
|
||||
|
||||
**Date**: 2026-08-27
|
||||
**Task**: Archive direct-edit solver API
|
||||
**Branch**: `feat/direct-edit-documents`
|
||||
|
||||
### Summary
|
||||
|
||||
Completed the direct-edit solver/API quality gate, documented the pure resolver and preview/save/revision/rebase contracts, and archived 08-26-direct-edit-solver-api. Next: activate the map-editor child task.
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `a26d4be` | (see git log) |
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
@@ -8,4 +8,4 @@ npx road-compiler --input /absolute/path/road-compiler.input.json
|
||||
|
||||
The compiler does not read host area configuration. The caller owns configuration normalization and creates the input JSON. Successful compilation writes exactly one `NATIVE_ROAD_COMPILE_DONE` JSON marker to stdout.
|
||||
|
||||
Run `npm test` and `npm run test:road-parity` after `npm ci` to validate the package without the host repository.
|
||||
Run `npm test` after `npm ci` to validate the package and its two self-contained area fixtures without the host repository. The migrated baselines remain upgrade corpus; the host repository owns the strict path-normalized parity gate.
|
||||
|
||||
@@ -6,19 +6,81 @@
|
||||
"license_file": "upstream/LICENSE.md"
|
||||
},
|
||||
"assets": [
|
||||
{"id":"through","source":"upstream/through.svg","anchor_x":12.5,"supported":true,"tested":true,"template":"through"},
|
||||
{"id":"left","source":"upstream/left.svg","anchor_x":17,"supported":true,"tested":true,"template":"left"},
|
||||
{"id":"right","source":"upstream/left.svg","derived_from":"left","mirror_x":true,"anchor_x":17,"supported":true,"tested":true,"template":"right"},
|
||||
{"id":"through;left","source":"upstream/left-through.svg","anchor_x":17,"supported":true,"tested":true,"template":"through_left"},
|
||||
{"id":"through;right","source":"upstream/left-through.svg","derived_from":"through;left","mirror_x":true,"anchor_x":17,"supported":true,"tested":true,"template":"through_right"},
|
||||
{"id":"through;left;right","source":"upstream/left-slight_left-through.svg","derived_from":"through;left + through;right","anchor_x":17,"supported":true,"tested":true,"template":"through_left_right"},
|
||||
{"id":"slight_left","source":"upstream/slight_left.svg","supported":false,"tested":false},
|
||||
{"id":"slight_left;through","source":"upstream/slight_left-through.svg","supported":false,"tested":false},
|
||||
{"id":"left;slight_left;through","source":"upstream/left-slight_left-through.svg","supported":false,"tested":false},
|
||||
{"id":"sharp_left","source":"upstream/sharp_left.svg","supported":false,"tested":false},
|
||||
{"id":"sharp_left;through","source":"upstream/sharp_left-through.svg","supported":false,"tested":false},
|
||||
{"id":"reverse_left","source":"upstream/reverse_left.svg","supported":false,"tested":false},
|
||||
{"id":"reverse_left;through","source":"upstream/reverse_left-through.svg","supported":false,"tested":false},
|
||||
{"id":"reverse_left;left;slight_left;through","source":"upstream/reverse_left-left-slight_left-through.svg","supported":false,"tested":false}
|
||||
{
|
||||
"id": "through",
|
||||
"source": "upstream/through.svg",
|
||||
"anchor_x": 12.5,
|
||||
"supported": true,
|
||||
"tested": true,
|
||||
"template": "through"
|
||||
},
|
||||
{
|
||||
"id": "left",
|
||||
"source": "upstream/left.svg",
|
||||
"anchor_x": 17,
|
||||
"supported": true,
|
||||
"tested": true,
|
||||
"template": "left"
|
||||
},
|
||||
{
|
||||
"id": "right",
|
||||
"source": "upstream/left.svg",
|
||||
"derived_from": "left",
|
||||
"mirror_x": true,
|
||||
"anchor_x": 17,
|
||||
"supported": true,
|
||||
"tested": true,
|
||||
"template": "right"
|
||||
},
|
||||
{
|
||||
"id": "through;left",
|
||||
"source": "upstream/left-through.svg",
|
||||
"anchor_x": 17,
|
||||
"supported": true,
|
||||
"tested": true,
|
||||
"template": "through_left"
|
||||
},
|
||||
{
|
||||
"id": "through;right",
|
||||
"source": "upstream/left-through.svg",
|
||||
"derived_from": "through;left",
|
||||
"mirror_x": true,
|
||||
"anchor_x": 17,
|
||||
"supported": true,
|
||||
"tested": true,
|
||||
"template": "through_right"
|
||||
},
|
||||
{
|
||||
"id": "through;left;right",
|
||||
"source": "upstream/left-slight_left-through.svg",
|
||||
"derived_from": "through;left + through;right",
|
||||
"anchor_x": 17,
|
||||
"supported": true,
|
||||
"tested": true,
|
||||
"template": "through_left_right"
|
||||
},
|
||||
{ "id": "slight_left", "source": "upstream/slight_left.svg", "supported": false, "tested": false },
|
||||
{ "id": "slight_left;through", "source": "upstream/slight_left-through.svg", "supported": false, "tested": false },
|
||||
{
|
||||
"id": "left;slight_left;through",
|
||||
"source": "upstream/left-slight_left-through.svg",
|
||||
"supported": false,
|
||||
"tested": false
|
||||
},
|
||||
{ "id": "sharp_left", "source": "upstream/sharp_left.svg", "supported": false, "tested": false },
|
||||
{ "id": "sharp_left;through", "source": "upstream/sharp_left-through.svg", "supported": false, "tested": false },
|
||||
{ "id": "reverse_left", "source": "upstream/reverse_left.svg", "supported": false, "tested": false },
|
||||
{
|
||||
"id": "reverse_left;through",
|
||||
"source": "upstream/reverse_left-through.svg",
|
||||
"supported": false,
|
||||
"tested": false
|
||||
},
|
||||
{
|
||||
"id": "reverse_left;left;slight_left;through",
|
||||
"source": "upstream/reverse_left-left-slight_left-through.svg",
|
||||
"supported": false,
|
||||
"tested": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ and as such it is not copyrighted.
|
||||
|
||||
Each icon should be
|
||||
|
||||
* 25px x 25px
|
||||
* SVG
|
||||
* Single color fill
|
||||
* Path outline only, no stroke
|
||||
* Pixel grid aligned (where possible)
|
||||
- 25px x 25px
|
||||
- SVG
|
||||
- Single color fill
|
||||
- Path outline only, no stroke
|
||||
- Pixel grid aligned (where possible)
|
||||
|
||||
### Arrows
|
||||
|
||||
@@ -25,7 +25,6 @@ lane arrows.
|
||||
|
||||
<img alt='Standard Arrow' width='300px' src='docs/standard_arrow.png'/>
|
||||
|
||||
|
||||
Arrowhead rotation angles are chosen based on the turn lane indication:
|
||||
|
||||
<table>
|
||||
@@ -84,13 +83,12 @@ As the number of arrows increases, the arrows scale down:
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
Other important rules for arrows:
|
||||
* Bottom aligned with each other, 2px from icon bottom edge
|
||||
* Left-right centered
|
||||
* Smooth curves, no kinks
|
||||
* Arrow shaft path connects to the arrowhead anchor point
|
||||
|
||||
- Bottom aligned with each other, 2px from icon bottom edge
|
||||
- Left-right centered
|
||||
- Smooth curves, no kinks
|
||||
- Arrow shaft path connects to the arrowhead anchor point
|
||||
|
||||
### Other Icons
|
||||
|
||||
|
||||
@@ -33,27 +33,27 @@ protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not limited
|
||||
to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display, communicate,
|
||||
and translate a Work;
|
||||
i. the right to reproduce, adapt, distribute, perform, display, communicate,
|
||||
and translate a Work;
|
||||
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
|
||||
iii. publicity and privacy rights pertaining to a person's image or likeness
|
||||
depicted in a Work;
|
||||
iii. publicity and privacy rights pertaining to a person's image or likeness
|
||||
depicted in a Work;
|
||||
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data in
|
||||
a Work;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data in
|
||||
a Work;
|
||||
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
|
||||
vii. other similar, equivalent or corresponding rights throughout the world
|
||||
based on applicable law or treaty, and any national implementations thereof.
|
||||
vii. other similar, equivalent or corresponding rights throughout the world
|
||||
based on applicable law or treaty, and any national implementations thereof.
|
||||
|
||||
**2. Waiver.** To the greatest extent permitted by, but not in contravention of,
|
||||
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
||||
@@ -94,25 +94,25 @@ Affirmer's express Statement of Purpose.
|
||||
|
||||
**4. Limitations and Disclaimers.**
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
|
||||
b. Affirmer offers the Work as-is and makes no representations or warranties
|
||||
of any kind concerning the Work, express, implied, statutory or otherwise,
|
||||
including without limitation warranties of title, merchantability, fitness
|
||||
for a particular purpose, non infringement, or the absence of latent or
|
||||
other defects, accuracy, or the present or absence of errors, whether or not
|
||||
discoverable, all to the greatest extent permissible under applicable law.
|
||||
b. Affirmer offers the Work as-is and makes no representations or warranties
|
||||
of any kind concerning the Work, express, implied, statutory or otherwise,
|
||||
including without limitation warranties of title, merchantability, fitness
|
||||
for a particular purpose, non infringement, or the absence of latent or
|
||||
other defects, accuracy, or the present or absence of errors, whether or not
|
||||
discoverable, all to the greatest extent permissible under applicable law.
|
||||
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without limitation
|
||||
any person's Copyright and Related Rights in the Work. Further, Affirmer
|
||||
disclaims responsibility for obtaining any necessary consents, permissions
|
||||
or other rights required for any use of the Work.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without limitation
|
||||
any person's Copyright and Related Rights in the Work. Further, Affirmer
|
||||
disclaims responsibility for obtaining any necessary consents, permissions
|
||||
or other rights required for any use of the Work.
|
||||
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to this
|
||||
CC0 or use of the Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to this
|
||||
CC0 or use of the Work.
|
||||
|
||||
For more information, please see
|
||||
<http://creativecommons.org/publicdomain/zero/1.0/>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { compiler } = require("../src");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { compiler } = require('../src');
|
||||
const { exportNativeRoadPackage } = require('../src/export/native-road-package');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const index = args.indexOf("--input");
|
||||
if (index < 0 || !args[index + 1] || index + 2 !== args.length) {
|
||||
throw new Error("Usage: road-compiler --input <RoadCompilerInput.json>");
|
||||
const index = args.indexOf('--input');
|
||||
const exportIndex = args.indexOf('--export-zip');
|
||||
const expectedLength = exportIndex >= 0 ? 4 : 2;
|
||||
if (index < 0 || !args[index + 1] || (exportIndex >= 0 && !args[exportIndex + 1]) || args.length !== expectedLength) {
|
||||
throw new Error('Usage: road-compiler --input <RoadCompilerInput.json> [--export-zip <output.zip>]');
|
||||
}
|
||||
const inputFile = path.resolve(args[index + 1]);
|
||||
const input = JSON.parse(fs.readFileSync(inputFile, "utf8"));
|
||||
const input = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
|
||||
const { result, comparison } = compiler.compileInput(input);
|
||||
console.log(`NATIVE_ROAD_COMPILE_DONE ${JSON.stringify({ areaId: input.areaId, roads: result.model.roads.length, endpoints: result.model.endpoints.length, diagnostics: result.diagnostics.length, output: input.outDir, comparison })}`);
|
||||
if (exportIndex >= 0) exportNativeRoadPackage(input.outDir, path.resolve(args[exportIndex + 1]));
|
||||
console.log(
|
||||
`NATIVE_ROAD_COMPILE_DONE ${JSON.stringify({ areaId: input.areaId, roads: result.model.roads.length, endpoints: result.model.endpoints.length, diagnostics: result.diagnostics.length, output: input.outDir, comparison })}`,
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
const path = require("path");
|
||||
const { startWorkbench } = require("../workbench/server");
|
||||
const path = require('path');
|
||||
const { startWorkbench } = require('../workbench/server');
|
||||
const fs = require('fs');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const index = args.indexOf("--input");
|
||||
if (index < 0 || !args[index + 1]) throw new Error("Usage: road-workbench --input <RoadCompilerInput.json> [--port <port>]");
|
||||
const inputFile = path.resolve(args[index + 1]);
|
||||
const input = require(inputFile);
|
||||
const portIndex = args.indexOf("--port");
|
||||
const index = args.indexOf('--input');
|
||||
const inputFile = index >= 0 && args[index + 1] ? path.resolve(args[index + 1]) : null;
|
||||
const input = inputFile ? JSON.parse(fs.readFileSync(inputFile, 'utf8')) : null;
|
||||
const portIndex = args.indexOf('--port');
|
||||
const port = portIndex >= 0 ? Number(args[portIndex + 1]) : 8787;
|
||||
startWorkbench({ input, inputFile, port });
|
||||
startWorkbench({ input, inputFile, repoRoot: path.resolve(__dirname, '..'), port });
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# Native Road Package v1
|
||||
# Native Road Package v1.1
|
||||
|
||||
`native-road-package/v1.1` adds a required `manifest.json` to the v1 output.
|
||||
Consumers may ignore this file when reading legacy v1 packages, but v1.1
|
||||
consumers must validate and consume it.
|
||||
|
||||
`native-road-package/v1` defines the boundary between the host area pipeline
|
||||
and the native road compiler. The compiler accepts only the input below; it
|
||||
@@ -35,6 +39,12 @@ shape and owns all path derivation.
|
||||
|
||||
`outDir` contains these JSON documents:
|
||||
|
||||
- `manifest.json`: `{ contract: "native-road-package/v1.1", areaId, layers }`.
|
||||
`layers` declares exactly the twelve GeoJSON sources. Renderable entries have
|
||||
`role` (`surface` or `marking`) and `materialLayer`; semantic entries have
|
||||
`role: "semantic"` and no material. Optional `splitBy` contains one property
|
||||
and exact-match cases plus one default case.
|
||||
|
||||
- `compiled.json`: top-level keys are `schema`, `areaId`, `source`, `model`,
|
||||
`movements`, `trafficSignals`, `diagnostics`, and `layers`. `source` has
|
||||
`osm`, `overrides`, and `trafficSignals` paths.
|
||||
@@ -63,6 +73,20 @@ shape and owns all path derivation.
|
||||
The editable signal source at `trafficSignalsFile` is a sibling of `outDir`.
|
||||
It is included in the parity baseline because regeneration must be stable.
|
||||
|
||||
## ZIP Export
|
||||
|
||||
The Web workbench's **导出道路包** action and the CLI's optional
|
||||
`--export-zip <output.zip>` emit the portable package form. The ZIP root is
|
||||
flat: it contains the six JSON documents above and `layers/*.geojson`, without
|
||||
an enclosing area directory. It contains no OSM input, overrides, or compiler
|
||||
workspace paths. `compiled.json` retains only model and movement data needed by
|
||||
the downstream preview; its internal `source` paths are removed.
|
||||
|
||||
The ZIP manifest adds informational `generator: { name, version }`. Consumers
|
||||
must select compatibility only from `contract`, `areaId`, and declared files.
|
||||
Entry order and timestamp are fixed, so unchanged compiled output exports to
|
||||
the same ZIP hash.
|
||||
|
||||
Successful CLI execution prints exactly one completion marker:
|
||||
|
||||
```text
|
||||
|
||||
1582
package-lock.json
generated
1582
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@osm-asset/road-compiler",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"private": false,
|
||||
"type": "commonjs",
|
||||
"main": "src/index.js",
|
||||
@@ -8,11 +8,30 @@
|
||||
"road-compiler": "bin/road-compiler.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node test/index.js",
|
||||
"test:road-parity": "node test/road-parity.js",
|
||||
"road:workbench": "node bin/road-workbench.js"
|
||||
"test": "node test/index.js && node test/native-road-edits.js && node test/road-revisions.js && node test/direct-edit-solver.js && node test/workbench-edit-api.js && node test/workbench-import-compile.js && node test/fixtures.js",
|
||||
"test:fixtures:update-baseline": "node test/update-fixture-baselines.js",
|
||||
"road:workbench": "node bin/road-workbench.js",
|
||||
"road:export": "node bin/road-compiler.js",
|
||||
"build": "vite build --config workbench/client/vite.config.ts",
|
||||
"dev": "vite --config workbench/client/vite.config.ts",
|
||||
"test:client": "tsc --noEmit -p workbench/client/tsconfig.json",
|
||||
"test:client:unit": "vitest run --passWithNoTests",
|
||||
"format": "prettier --write \"{src,bin,test,workbench}/**/*.{js,ts,tsx,css,html}\" \"*.{json,js,ts}\"",
|
||||
"format:check": "prettier --check \"{src,bin,test,workbench}/**/*.{js,ts,tsx,css,html}\" \"*.{json,js,ts}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"ol": "10.10.0"
|
||||
"fflate": "0.8.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"ol": "10.10.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.1.0",
|
||||
"vitest": "4.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user