fix: expose and apply control marking handles
This commit is contained in:
@@ -653,12 +653,23 @@ function makeJunctionHandles(model, junctionData, constraints) {
|
|||||||
const normal = (tangent + 90) % 360;
|
const normal = (tangent + 90) % 360;
|
||||||
const anchor = { type: 'junction-approach', nodeId: String(nodeId), segmentId: approach.segmentId };
|
const anchor = { type: 'junction-approach', nodeId: String(nodeId), segmentId: approach.segmentId };
|
||||||
const affects = junctionAffects(model, nodeId, segmentIds);
|
const affects = junctionAffects(model, nodeId, segmentIds);
|
||||||
const add = (kind, value, min, max, axis, constraint) => {
|
// Control markings are positioned by their distance back from the junction,
|
||||||
|
// rather than at the approach cutback point shared by shape handles. Keeping
|
||||||
|
// these positions distinct is important: otherwise the later stop-line
|
||||||
|
// feature covers the crosswalk handle and neither target is discoverable.
|
||||||
|
const controlPoint = (offsetMeters, lateralMeters = 0) => {
|
||||||
|
const coordinate = coordinateAt(
|
||||||
|
line,
|
||||||
|
Math.min(0.98, Math.max(0.02, offsetMeters / Math.max(approach.length, 1))),
|
||||||
|
);
|
||||||
|
return lateralMeters ? offsetCoordinate(coordinate, normal, lateralMeters) : coordinate;
|
||||||
|
};
|
||||||
|
const add = (kind, value, min, max, axis, constraint, position = point) => {
|
||||||
handles.push({
|
handles.push({
|
||||||
handleId: `handle:${kind}:node/${nodeId}:${approach.segmentId}`,
|
handleId: `handle:${kind}:node/${nodeId}:${approach.segmentId}`,
|
||||||
kind,
|
kind,
|
||||||
anchor,
|
anchor,
|
||||||
position: point,
|
position,
|
||||||
axisAzimuth: ((axis % 360) + 360) % 360,
|
axisAzimuth: ((axis % 360) + 360) % 360,
|
||||||
value: { current: value, min, max, unit: 'meter' },
|
value: { current: value, min, max, unit: 'meter' },
|
||||||
...(constraint ? { constraintId: constraint.id } : {}),
|
...(constraint ? { constraintId: constraint.id } : {}),
|
||||||
@@ -696,8 +707,12 @@ function makeJunctionHandles(model, junctionData, constraints) {
|
|||||||
numericValue(crosswalkConstraint, 'insetMeters', CROSSWALK_JUNCTION_INSET_METERS),
|
numericValue(crosswalkConstraint, 'insetMeters', CROSSWALK_JUNCTION_INSET_METERS),
|
||||||
0,
|
0,
|
||||||
MAX_CROSSWALK_INSET_METERS,
|
MAX_CROSSWALK_INSET_METERS,
|
||||||
tangent,
|
// The approach tangent points away from the junction. Increasing an
|
||||||
|
// inset moves the crosswalk toward the junction, so expose the inverse
|
||||||
|
// axis to keep positive drag values aligned with the handle's meaning.
|
||||||
|
(tangent + 180) % 360,
|
||||||
crosswalkConstraint,
|
crosswalkConstraint,
|
||||||
|
controlPoint(numericValue(crosswalkConstraint, 'insetMeters', CROSSWALK_JUNCTION_INSET_METERS), -1),
|
||||||
);
|
);
|
||||||
const stopLineConstraint = constraintFor(constraints, 'junction-stop-line-offset', anchor);
|
const stopLineConstraint = constraintFor(constraints, 'junction-stop-line-offset', anchor);
|
||||||
add(
|
add(
|
||||||
@@ -707,6 +722,7 @@ function makeJunctionHandles(model, junctionData, constraints) {
|
|||||||
Math.max(MIN_STOP_LINE_OFFSET_METERS, approach.length * 0.45),
|
Math.max(MIN_STOP_LINE_OFFSET_METERS, approach.length * 0.45),
|
||||||
tangent,
|
tangent,
|
||||||
stopLineConstraint,
|
stopLineConstraint,
|
||||||
|
controlPoint(numericValue(stopLineConstraint, 'offsetMeters', STOP_LINE_OFFSET_METERS), 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const ordered = [...approaches].sort((a, b) => a.segmentId.localeCompare(b.segmentId));
|
const ordered = [...approaches].sort((a, b) => a.segmentId.localeCompare(b.segmentId));
|
||||||
|
|||||||
@@ -106,6 +106,20 @@ assert.ok(
|
|||||||
northSidewalks.find((handle) => handle.anchor.side === 'left').position[0] < 113,
|
northSidewalks.find((handle) => handle.anchor.side === 'left').position[0] < 113,
|
||||||
'the sidewalk handle must follow the same side convention as the edge handle',
|
'the sidewalk handle must follow the same side convention as the edge handle',
|
||||||
);
|
);
|
||||||
|
const controlHandles = junctionResolution.handles.handles.filter((handle) => handle.anchor.segmentId === 'segment:a');
|
||||||
|
const crosswalkHandle = controlHandles.find((handle) => handle.kind === 'junction-crosswalk-inset');
|
||||||
|
const stopLineHandle = controlHandles.find((handle) => handle.kind === 'junction-stop-line-offset');
|
||||||
|
assert.ok(crosswalkHandle && stopLineHandle, 'both control-marking handles must be published');
|
||||||
|
assert.notDeepEqual(
|
||||||
|
crosswalkHandle.position,
|
||||||
|
stopLineHandle.position,
|
||||||
|
'crosswalk and stop-line handles must not overlap at the shared cutback point',
|
||||||
|
);
|
||||||
|
assert.notEqual(
|
||||||
|
crosswalkHandle.axisAzimuth,
|
||||||
|
stopLineHandle.axisAzimuth,
|
||||||
|
'crosswalk inset and stop-line offset use opposite positive directions along the approach',
|
||||||
|
);
|
||||||
|
|
||||||
// A selection scopes the manifest to the object being edited. The whole-area
|
// A selection scopes the manifest to the object being edited. The whole-area
|
||||||
// manifest is 844 KB on a 41-road workspace, 89% of it `affects` id lists, while
|
// manifest is 844 KB on a 41-road workspace, 89% of it `affects` id lists, while
|
||||||
|
|||||||
@@ -204,8 +204,15 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
/** A drag becomes one drafted constraint layered over the session's document. */
|
/** A drag becomes one drafted constraint layered over the session's document. */
|
||||||
const draftFor = (handle: EditHandle, value: number): PreviewDraft | null => {
|
const draftFor = (handle: EditHandle, value: number): PreviewDraft | null => {
|
||||||
|
// Road-anchored edits carry a directional road id. Junction-approach
|
||||||
|
// controls are keyed by segment instead, so resolve them through the
|
||||||
|
// compiled model before building the anchor snapshot. Without this branch
|
||||||
|
// control handles silently returned null and never scheduled a preview.
|
||||||
const roadId = 'roadId' in handle.anchor ? handle.anchor.roadId : null;
|
const roadId = 'roadId' in handle.anchor ? handle.anchor.roadId : null;
|
||||||
const road = roadId ? state?.compiled.model.roads.find((item) => item.id === roadId) : undefined;
|
const segmentId = 'segmentId' in handle.anchor ? handle.anchor.segmentId : null;
|
||||||
|
const road = state?.compiled.model.roads.find(
|
||||||
|
(item) => (roadId ? item.id === roadId : false) || (segmentId ? item.segmentId === segmentId : false),
|
||||||
|
);
|
||||||
// Without a centerline there is no snapshot to record, so drop the drag rather
|
// Without a centerline there is no snapshot to record, so drop the drag rather
|
||||||
// than send a constraint the server would have to reject.
|
// than send a constraint the server would have to reject.
|
||||||
if (!session || !road || road.centerline.length < 2) return null;
|
if (!session || !road || road.centerline.length < 2) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user