feat: add direct edit handles behind directEdit flag

Steps 1-3 of the main map road interval editor.

EditSession keeps the command stack, undo/redo and previewSeq arbitration as
pure logic with no OpenLayers reference, so all of it is unit-tested in node.
Pointer displacement converts to meters through EPSG:4326 and spherical
distance: treating a 3857 delta as meters desyncs the geometry from the cursor
by 1/cos(latitude). Handle drags project onto the axis the manifest declares
and clamp to its range, so the client never writes a coordinate into a road
polygon.

All of it sits behind a directEdit flag that defaults to off. With the flag off
the workbench requests no manifest, creates no extra source and registers no
interaction, so behaviour matches main.

The ol-ext probe passed its three gates but is not adopted for road handles.
Transform translates by the raw pointer delta, so a handle detaches from its
clamped constraint value: a drag reading -24.1 m produced a draft of -5.4 m.
Production needs the handle position derived from the constraint instead, which
means owning the position update, so native OL PointerInteraction will carry
the drag. ol-ext stays out of package.json; the probe is kept as a manual
harness. Reserve handles are unreachable with the current solver, recorded in
research/ rather than worked around.

Also names the dead backend when an API response is empty, instead of
surfacing "Unexpected end of JSON input" from response.json().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 14:59:11 +08:00
parent e25c564bb9
commit bc4b9a9717
25 changed files with 2463 additions and 22 deletions

View File

@@ -0,0 +1,148 @@
/* Probe-only styles. Deliberately not sharing the workbench stylesheet, so the
whole probe can be deleted in one directory. */
* {
box-sizing: border-box;
}
body {
margin: 0;
font:
14px/1.5 system-ui,
-apple-system,
'Segoe UI',
sans-serif;
color: #1b2426;
background: #f2f4f3;
}
.probe-nav {
display: flex;
gap: 16px;
align-items: center;
padding: 10px 16px;
background: #1b2426;
color: #f2f4f3;
}
.probe-nav span {
color: #9db0b3;
font-size: 13px;
}
.probe-nav button {
margin-left: auto;
}
button {
padding: 6px 12px;
border: 1px solid #4a5a5d;
border-radius: 6px;
background: #f2f4f3;
cursor: pointer;
font: inherit;
}
.probe {
display: grid;
grid-template-columns: 1fr 340px;
height: calc(100vh - 45px);
}
.probe-map {
height: 100%;
background: #dfe4e2;
}
.probe-panel {
padding: 16px;
overflow-y: auto;
background: #fff;
border-left: 1px solid #d3dad8;
}
.probe-panel h1 {
margin: 0 0 4px;
font-size: 16px;
}
.probe-panel p {
margin: 0 0 12px;
color: #59696c;
}
.probe-panel dt {
margin-top: 12px;
font-weight: 600;
}
.probe-panel dd {
margin: 4px 0 0;
color: #35474a;
}
.probe-panel ul {
margin: 16px 0 0;
padding-left: 18px;
color: #59696c;
font-variant-numeric: tabular-nums;
}
.probe-panel h2 {
margin: 0 0 6px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #59696c;
}
.probe-panel section {
margin-bottom: 20px;
}
.probe-panel ol {
margin: 0 0 8px;
padding-left: 20px;
}
.probe-panel ol li {
margin-bottom: 4px;
}
.probe-howto {
padding: 12px;
background: #eef4f2;
border-left: 3px solid #296956;
border-radius: 4px;
}
.probe-howto p,
.probe-ask p {
margin: 8px 0 0;
font-size: 13px;
}
.probe-live {
padding: 12px;
background: #1b2426;
border-radius: 6px;
}
.probe-live h2,
.probe-live p {
color: #9db0b3;
}
.probe-big {
display: block;
margin: 2px 0 6px;
font-size: 30px;
font-variant-numeric: tabular-nums;
color: #4ad2f0;
}
.probe-ask {
padding: 12px;
background: #fdf6e8;
border-left: 3px solid #d49318;
border-radius: 4px;
}
.probe-empty {
padding: 24px;
}
.probe-error {
padding: 24px;
max-width: 900px;
}
.probe-error h1 {
margin: 0 0 4px;
font-size: 18px;
color: #bf3b2e;
}
.probe-error pre {
padding: 12px;
overflow-x: auto;
background: #1b2426;
color: #f2f4f3;
border-radius: 6px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-word;
}