Files
road-compiler/workbench/client/probe/ol-ext-transform/fixture.ts
que01 bc4b9a9717 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>
2026-08-27 14:59:11 +08:00

62 lines
2.2 KiB
TypeScript

// Synthetic road for the probe, so the page runs without importing OSM or
// touching the workbench API. Geometry helpers come from the real edit modules
// on purpose: the probe has to exercise the production conversion, not a copy.
import { offsetCoordinate, polylineLengthMeters, type Coordinate } from '../../src/edit/meters';
import type { EditHandle } from '../../src/edit/types';
/** The road heads due north, so its normal — the drag axis — points due east. */
export const TANGENT_AZIMUTH = 0;
export const AXIS_AZIMUTH = 90;
export const WIDTH_METERS = 12;
export const LANE_COUNT = 4;
export const CENTERLINE: Coordinate[] = [
[116.397, 39.905],
[116.397, 39.915],
];
export const ROAD_LENGTH_METERS = polylineLengthMeters(CENTERLINE);
const MIDPOINT: Coordinate = [116.397, 39.91];
/** Outer ring of the baseline road surface, EPSG:4326. */
export function roadSurfaceRing(): Coordinate[] {
const half = WIDTH_METERS / 2;
const left = CENTERLINE.map((point) => offsetCoordinate(point, TANGENT_AZIMUTH + 90, half));
const right = CENTERLINE.map((point) => offsetCoordinate(point, TANGENT_AZIMUTH - 90, half));
return [...left, ...right.reverse(), left[0]];
}
/** Lane divider lines, to make it obvious if the baseline ever gets rewritten. */
export function laneDividerLines(): Coordinate[][] {
const laneWidth = WIDTH_METERS / LANE_COUNT;
const lines: Coordinate[][] = [];
for (let index = 1; index < LANE_COUNT; index += 1) {
const lateral = -WIDTH_METERS / 2 + laneWidth * index;
lines.push(CENTERLINE.map((point) => offsetCoordinate(point, TANGENT_AZIMUTH + 90, lateral)));
}
return lines;
}
/**
* A left edge-offset handle shaped exactly like the one `makeRoadHandles()`
* emits, so `projectHandleValue()` sees production input.
*/
export const PROBE_HANDLE: EditHandle = {
handleId: 'probe:road-edge-offset:left',
kind: 'road-edge-offset',
anchor: {
type: 'road-interval',
roadId: 'probe:road/1:forward',
startStation: 0.15,
endStation: 0.85,
side: 'left',
},
position: offsetCoordinate(MIDPOINT, AXIS_AZIMUTH, WIDTH_METERS / 2),
axisAzimuth: AXIS_AZIMUTH,
value: { current: 0, min: -WIDTH_METERS * 0.45, max: WIDTH_METERS * 0.45, unit: 'meter' },
affects: [],
editable: true,
};