Files
road-compiler/workbench/client/src/edit/selection.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

44 lines
1.9 KiB
TypeScript

// selection -> which handles the main map may show.
//
// Pure manifest filtering, deliberately free of any OpenLayers import so it can
// be unit-tested in node. `handle-layer.ts` is the OL adapter that renders the
// result.
import { isRoadKind, type EditHandle, type HandleManifest, type JunctionReserve } from './types';
/**
* Road kinds only, and only for the selected road's segment.
*
* Junction kinds stay out even though the manifest carries them: reserve interiors
* belong to JunctionTools, and the main map must not offer a second way to edit
* them. Reserve-disabled road handles are *kept* rather than dropped, because the
* user has to be able to see why that stretch refuses to move.
*
* Handle anchors carry a directional `roadId`, while `reserves` are keyed by
* segment id, so callers pass a resolver instead of parsing ids apart.
*/
export function handlesForSegment(
manifest: HandleManifest | null,
segmentId: string | null,
segmentOf: (roadId: string) => string | undefined,
): EditHandle[] {
if (!manifest || !segmentId) return [];
return manifest.handles.filter((handle) => {
if (!isRoadKind(handle.kind)) return false;
const roadId = 'roadId' in handle.anchor ? handle.anchor.roadId : undefined;
return typeof roadId === 'string' && segmentOf(roadId) === segmentId;
});
}
/** Reserves that apply to one segment, for the range window and the tooltip copy. */
export function reservesForSegment(manifest: HandleManifest | null, segmentId: string | null): JunctionReserve[] {
if (!manifest || !segmentId) return [];
return manifest.reserves.filter((reserve) => reserve.roadId === segmentId);
}
/** Why a handle refuses to move. Falls back to a generic reason so the UI never says nothing. */
export function disabledReasonOf(handle: EditHandle): string | undefined {
if (handle.editable) return undefined;
return handle.disabledReason || '该手柄位于路口保留区,请进入 JunctionTools 编辑。';
}