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:
43
workbench/client/src/edit/selection.ts
Normal file
43
workbench/client/src/edit/selection.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// 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 编辑。';
|
||||
}
|
||||
Reference in New Issue
Block a user