@expofp/wayfinding 3.13.0 → 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/createWayfindingEngine.js +13 -13
- package/dist/core/graph/constants.d.ts +7 -4
- package/dist/core/graph/constants.js +7 -4
- package/dist/core/graph/findShortestPath.d.ts +2 -0
- package/dist/core/graph/findShortestPath.js +2 -0
- package/dist/core/graph/graphHelpers.js +5 -2
- package/dist/core/graph/linkCost.d.ts +7 -1
- package/dist/core/graph/linkCost.js +18 -5
- package/dist/core/graph/pathfinder/aStarPathFinder.js +7 -4
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.js +0 -1
- package/dist/core/position/snapToRoute.d.ts +6 -1
- package/dist/core/position/snapToRoute.js +15 -79
- package/dist/core/rendering/computeTransitionPoints.js +3 -3
- package/dist/core/routing/buildMultiPointRoute.js +2 -10
- package/dist/core/routing/getRouteLength.d.ts +1 -1
- package/dist/core/types.d.ts +5 -9
- package/dist/runtime/createWayfindingRuntime.js +7 -10
- package/dist/runtime/endpointView.js +2 -2
- package/dist/runtime/positionTrailView.js +2 -6
- package/dist/runtime/routeRenderData.d.ts +6 -10
- package/dist/runtime/routeRenderData.js +11 -10
- package/dist/runtime/snapPositionToRoute.d.ts +10 -0
- package/dist/runtime/snapPositionToRoute.js +36 -0
- package/package.json +2 -2
- package/dist/core/rendering/normalizeRouteDirection.d.ts +0 -19
- package/dist/core/rendering/normalizeRouteDirection.js +0 -82
|
@@ -8,41 +8,41 @@ const POINT_EPS = 1e-6;
|
|
|
8
8
|
const pointEquals = (a, b) => a.layer === b.layer && Math.abs(a.x - b.x) < POINT_EPS && Math.abs(a.y - b.y) < POINT_EPS;
|
|
9
9
|
/**
|
|
10
10
|
* Trim/extend the route's FROM tip to land exactly on `entry.projection`.
|
|
11
|
-
*
|
|
11
|
+
* The FROM boundary is the first line; `boundary.p0` is its graph node.
|
|
12
12
|
* @param lines
|
|
13
13
|
* @param entry
|
|
14
14
|
*/
|
|
15
15
|
function attachFromEntry(lines, entry) {
|
|
16
16
|
if (!lines.length)
|
|
17
17
|
return lines;
|
|
18
|
-
const boundary = lines[
|
|
19
|
-
const chosenEndpoint = boundary.
|
|
18
|
+
const boundary = lines[0];
|
|
19
|
+
const chosenEndpoint = boundary.p0;
|
|
20
20
|
const otherEnd = pointEquals(entry.segment.p0, chosenEndpoint)
|
|
21
21
|
? entry.segment.p1
|
|
22
22
|
: entry.segment.p0;
|
|
23
|
-
if (pointEquals(boundary.
|
|
24
|
-
return [
|
|
23
|
+
if (pointEquals(boundary.p1, otherEnd)) {
|
|
24
|
+
return [{ ...boundary, p0: entry.projection }, ...lines.slice(1)];
|
|
25
25
|
}
|
|
26
|
-
return [
|
|
26
|
+
return [{ ...entry.segment, p0: entry.projection, p1: chosenEndpoint }, ...lines];
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Mirror of {@link attachFromEntry} for the TO side
|
|
30
|
-
*
|
|
29
|
+
* Mirror of {@link attachFromEntry} for the TO side: the TO boundary is the
|
|
30
|
+
* last line and `boundary.p1` is its graph node.
|
|
31
31
|
* @param lines
|
|
32
32
|
* @param entry
|
|
33
33
|
*/
|
|
34
34
|
function attachToEntry(lines, entry) {
|
|
35
35
|
if (!lines.length)
|
|
36
36
|
return lines;
|
|
37
|
-
const boundary = lines[
|
|
38
|
-
const chosenEndpoint = boundary.
|
|
37
|
+
const boundary = lines[lines.length - 1];
|
|
38
|
+
const chosenEndpoint = boundary.p1;
|
|
39
39
|
const otherEnd = pointEquals(entry.segment.p0, chosenEndpoint)
|
|
40
40
|
? entry.segment.p1
|
|
41
41
|
: entry.segment.p0;
|
|
42
|
-
if (pointEquals(boundary.
|
|
43
|
-
return [{ ...boundary,
|
|
42
|
+
if (pointEquals(boundary.p0, otherEnd)) {
|
|
43
|
+
return [...lines.slice(0, -1), { ...boundary, p1: entry.projection }];
|
|
44
44
|
}
|
|
45
|
-
return [{ ...entry.segment, p0:
|
|
45
|
+
return [...lines, { ...entry.segment, p0: chosenEndpoint, p1: entry.projection }];
|
|
46
46
|
}
|
|
47
47
|
function attachOffGraphEntries(lines, fromEntry, toEntry) {
|
|
48
48
|
let result = lines;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Default A* cost for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Default A* cost for an unset virtual link that changes layers. Being far
|
|
3
|
+
* larger than any physical segment cost, it makes the pathfinder minimize the
|
|
4
|
+
* number of floor changes, so a route only crosses a floor when needed to reach
|
|
5
|
+
* the destination. A per-line `virtualLength` overrides it (designer control).
|
|
5
6
|
*/
|
|
6
|
-
export declare const
|
|
7
|
+
export declare const FLOOR_TRANSITION_PENALTY = 10000;
|
|
8
|
+
/** Default A* cost for a virtual link that stays on the same layer (free). */
|
|
9
|
+
export declare const VIRTUAL_LINE_PENALTY = 0;
|
|
7
10
|
/**
|
|
8
11
|
* Fallback line weight when `line.weight` is 0 (meaning "not set" in floorplan data,
|
|
9
12
|
* not "free passage").
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Default A* cost for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Default A* cost for an unset virtual link that changes layers. Being far
|
|
3
|
+
* larger than any physical segment cost, it makes the pathfinder minimize the
|
|
4
|
+
* number of floor changes, so a route only crosses a floor when needed to reach
|
|
5
|
+
* the destination. A per-line `virtualLength` overrides it (designer control).
|
|
5
6
|
*/
|
|
6
|
-
export const
|
|
7
|
+
export const FLOOR_TRANSITION_PENALTY = 10000;
|
|
8
|
+
/** Default A* cost for a virtual link that stays on the same layer (free). */
|
|
9
|
+
export const VIRTUAL_LINE_PENALTY = 0;
|
|
7
10
|
/**
|
|
8
11
|
* Fallback line weight when `line.weight` is 0 (meaning "not set" in floorplan data,
|
|
9
12
|
* not "free passage").
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type GraphInstance, type GraphLine, type RoutePoint, type ShortestPathResult } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Computes the weighted path cost matching the graph link distance formula.
|
|
4
|
+
* Floor-change surcharges are already baked into `linkCost`, so this simply
|
|
5
|
+
* sums the per-edge cost.
|
|
4
6
|
* @param points
|
|
5
7
|
* @param lines
|
|
6
8
|
*/
|
|
@@ -6,6 +6,8 @@ function createRoutePoint(layer, x, y) {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Computes the weighted path cost matching the graph link distance formula.
|
|
9
|
+
* Floor-change surcharges are already baked into `linkCost`, so this simply
|
|
10
|
+
* sums the per-edge cost.
|
|
9
11
|
* @param points
|
|
10
12
|
* @param lines
|
|
11
13
|
*/
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { degToRad, lineAngle, pointDistance } from '@expofp/geometry';
|
|
2
|
-
export const toNodeId = (p) => `${p.layer}_${p.x}_${p.y}`;
|
|
2
|
+
export const toNodeId = (p) => `${p.layer ?? ''}_${p.x}_${p.y}`;
|
|
3
3
|
/** Maximum distance (SVG units) to consider two graph nodes as the same point. */
|
|
4
4
|
const POINT_PROXIMITY_THRESHOLD = 1;
|
|
5
|
-
|
|
5
|
+
// `RoutePoint.layer` is typed as a string, but external data may still supply a
|
|
6
|
+
// nullish/invalid layer. Normalize it to '' so point comparisons stay consistent
|
|
7
|
+
// with the ids that toNodeId/parseNodeId round-trip to.
|
|
8
|
+
export const arePointsClose = (p1, p2) => (p1.layer ?? '') === (p2.layer ?? '') && pointDistance(p1, p2) <= POINT_PROXIMITY_THRESHOLD;
|
|
6
9
|
export const matchesLine = (line, p0, p1) => (arePointsClose(line.p0, p0) && arePointsClose(line.p1, p1)) ||
|
|
7
10
|
(arePointsClose(line.p0, p1) && arePointsClose(line.p1, p0));
|
|
8
11
|
export function findLineByEndpoints(lines, p0, p1) {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type GraphLine } from '../types.js';
|
|
2
|
+
export declare function changesLayer(line: GraphLine): boolean;
|
|
2
3
|
/**
|
|
3
|
-
* Weighted cost of a
|
|
4
|
+
* Weighted cost of a graph edge for A*.
|
|
5
|
+
*
|
|
6
|
+
* Physical links cost their weighted length. A virtual link uses its configured
|
|
7
|
+
* `virtualLength` when set (designer control); otherwise a layer-changing link
|
|
8
|
+
* costs `FLOOR_TRANSITION_PENALTY` (routes avoid needless floor changes) and a
|
|
9
|
+
* same-layer link is free (`VIRTUAL_LINE_PENALTY`).
|
|
4
10
|
* @param line
|
|
5
11
|
*/
|
|
6
12
|
export declare function linkCost(line: GraphLine): number;
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { pointDistance } from '@expofp/geometry';
|
|
2
|
-
import { DEFAULT_LINE_WEIGHT, VIRTUAL_LINE_PENALTY } from './constants.js';
|
|
2
|
+
import { DEFAULT_LINE_WEIGHT, FLOOR_TRANSITION_PENALTY, VIRTUAL_LINE_PENALTY, } from './constants.js';
|
|
3
|
+
function physicalCost(line) {
|
|
4
|
+
return pointDistance(line.p0, line.p1) / (line.weight || DEFAULT_LINE_WEIGHT);
|
|
5
|
+
}
|
|
6
|
+
export function changesLayer(line) {
|
|
7
|
+
return (line.p0.layer ?? '') !== (line.p1.layer ?? '');
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
|
-
* Weighted cost of a
|
|
10
|
+
* Weighted cost of a graph edge for A*.
|
|
11
|
+
*
|
|
12
|
+
* Physical links cost their weighted length. A virtual link uses its configured
|
|
13
|
+
* `virtualLength` when set (designer control); otherwise a layer-changing link
|
|
14
|
+
* costs `FLOOR_TRANSITION_PENALTY` (routes avoid needless floor changes) and a
|
|
15
|
+
* same-layer link is free (`VIRTUAL_LINE_PENALTY`).
|
|
5
16
|
* @param line
|
|
6
17
|
*/
|
|
7
18
|
export function linkCost(line) {
|
|
8
|
-
if (line.virtual)
|
|
9
|
-
return line
|
|
10
|
-
|
|
19
|
+
if (!line.virtual)
|
|
20
|
+
return physicalCost(line);
|
|
21
|
+
if (line.virtualLength != null)
|
|
22
|
+
return line.virtualLength;
|
|
23
|
+
return changesLayer(line) ? FLOOR_TRANSITION_PENALTY : VIRTUAL_LINE_PENALTY;
|
|
11
24
|
}
|
|
@@ -41,8 +41,9 @@ export function createAStarPathFinder() {
|
|
|
41
41
|
return !!graph.hasNode(id);
|
|
42
42
|
},
|
|
43
43
|
/**
|
|
44
|
-
* Returns path nodes in **target
|
|
45
|
-
*
|
|
44
|
+
* Returns path nodes in **source→target** order (`fromId` first, `toId`
|
|
45
|
+
* last). A* reconstructs from the goal, so the raw path is target→source; it
|
|
46
|
+
* is reversed here once, at the source, so no downstream reversal is needed.
|
|
46
47
|
* @param fromId
|
|
47
48
|
* @param toId
|
|
48
49
|
*/
|
|
@@ -52,10 +53,12 @@ export function createAStarPathFinder() {
|
|
|
52
53
|
const nodes = finder.find(fromId, toId);
|
|
53
54
|
if (!nodes.length)
|
|
54
55
|
return [];
|
|
55
|
-
return nodes
|
|
56
|
+
return nodes
|
|
57
|
+
.map((node) => {
|
|
56
58
|
const [layer, x, y] = parseNodeId(node.id);
|
|
57
59
|
return { id: node.id, layer, x, y };
|
|
58
|
-
})
|
|
60
|
+
})
|
|
61
|
+
.reverse();
|
|
59
62
|
},
|
|
60
63
|
};
|
|
61
64
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ export { projectPointOnSegment } from './geometry/projectPointOnSegment.js';
|
|
|
6
6
|
export { computeTrailPoints } from './rendering/computeTrailPoints.js';
|
|
7
7
|
export type { TransitionPointDef } from './rendering/computeTransitionPoints.js';
|
|
8
8
|
export { computeTransitionPoints } from './rendering/computeTransitionPoints.js';
|
|
9
|
-
export { normalizeRouteDirection } from './rendering/normalizeRouteDirection.js';
|
|
10
9
|
export { computeBoundingBox } from './rendering/routeGeometry.js';
|
|
11
10
|
export { getRouteLength } from './routing/getRouteLength.js';
|
|
12
11
|
export type { GpsProjectionConfig } from './position/gpsThreshold.js';
|
package/dist/core/index.js
CHANGED
|
@@ -3,7 +3,6 @@ export { projectPointOnSegment } from './geometry/projectPointOnSegment.js';
|
|
|
3
3
|
// Rendering utilities
|
|
4
4
|
export { computeTrailPoints } from './rendering/computeTrailPoints.js';
|
|
5
5
|
export { computeTransitionPoints } from './rendering/computeTransitionPoints.js';
|
|
6
|
-
export { normalizeRouteDirection } from './rendering/normalizeRouteDirection.js';
|
|
7
6
|
export { computeBoundingBox } from './rendering/routeGeometry.js';
|
|
8
7
|
// Routing utilities
|
|
9
8
|
export { getRouteLength } from './routing/getRouteLength.js';
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type RouteLine, type RoutePoint, type RouteSnapResult, type SnapToRouteConfig } from '../types.js';
|
|
2
2
|
export { SNAP_THRESHOLD_METERS } from './gpsThreshold.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Snap a position onto a from→to route polyline. Pure geometry: nearest segment
|
|
5
|
+
* within `snapThreshold`, plus the remaining arc-length to the destination.
|
|
6
|
+
* Segments on a layer other than `position.layer` are skipped.
|
|
7
|
+
*/
|
|
8
|
+
export declare function snapToRoute(position: RoutePoint, routeLines: readonly RouteLine[], { snapThreshold }: SnapToRouteConfig): RouteSnapResult;
|
|
4
9
|
//# sourceMappingURL=snapToRoute.d.ts.map
|
|
@@ -1,38 +1,18 @@
|
|
|
1
1
|
import { pointDistance } from '@expofp/geometry';
|
|
2
|
-
import { pointInPolygon } from '../geometry/pointInPolygon.js';
|
|
3
2
|
import { projectPointOnSegment } from '../geometry/projectPointOnSegment.js';
|
|
4
|
-
function pointAtDistanceSafe(routeLines, distanceFromStart) {
|
|
5
|
-
let acc = 0;
|
|
6
|
-
for (const line of routeLines) {
|
|
7
|
-
const len = pointDistance(line.p0, line.p1);
|
|
8
|
-
if (len === 0)
|
|
9
|
-
continue;
|
|
10
|
-
if (acc + len >= distanceFromStart) {
|
|
11
|
-
const t = (distanceFromStart - acc) / len;
|
|
12
|
-
const x = line.p0.x + (line.p1.x - line.p0.x) * t;
|
|
13
|
-
const y = line.p0.y + (line.p1.y - line.p0.y) * t;
|
|
14
|
-
return {
|
|
15
|
-
point: { layer: line.p0.layer, x, y },
|
|
16
|
-
line,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
acc += len;
|
|
20
|
-
}
|
|
21
|
-
const last = routeLines[routeLines.length - 1];
|
|
22
|
-
return {
|
|
23
|
-
point: { layer: last.p0.layer, x: last.p1.x, y: last.p1.y },
|
|
24
|
-
line: last,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
3
|
export { SNAP_THRESHOLD_METERS } from './gpsThreshold.js';
|
|
28
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Snap a position onto a from→to route polyline. Pure geometry: nearest segment
|
|
6
|
+
* within `snapThreshold`, plus the remaining arc-length to the destination.
|
|
7
|
+
* Segments on a layer other than `position.layer` are skipped.
|
|
8
|
+
*/
|
|
9
|
+
export function snapToRoute(position, routeLines, { snapThreshold }) {
|
|
29
10
|
const lineCount = routeLines.length;
|
|
30
11
|
if (!lineCount)
|
|
31
12
|
return { snapped: false };
|
|
32
|
-
const { snapThreshold, to, from, minRemainingUnits = 0.5 } = config;
|
|
33
13
|
const posLayer = position.layer || null;
|
|
34
14
|
let bestDistance = Infinity;
|
|
35
|
-
let
|
|
15
|
+
let best = null;
|
|
36
16
|
let accumulatedLength = 0;
|
|
37
17
|
let hasSegmentOnPosLayer = posLayer == null;
|
|
38
18
|
for (let i = 0; i < lineCount; i++) {
|
|
@@ -49,66 +29,22 @@ export function snapToRoute(position, routeLines, config) {
|
|
|
49
29
|
const projection = projectPointOnSegment({ x: position.x, y: position.y }, p0, p1);
|
|
50
30
|
if (projection.distance < bestDistance) {
|
|
51
31
|
bestDistance = projection.distance;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
32
|
+
best = {
|
|
33
|
+
point: { layer: p0.layer, x: projection.projected.x, y: projection.projected.y },
|
|
34
|
+
line,
|
|
35
|
+
fromStart: accumulatedLength + segLen * projection.t,
|
|
56
36
|
};
|
|
57
37
|
}
|
|
58
38
|
accumulatedLength += segLen;
|
|
59
39
|
}
|
|
60
40
|
if (posLayer != null && !hasSegmentOnPosLayer)
|
|
61
41
|
return { snapped: false };
|
|
62
|
-
|
|
63
|
-
const routeStart = routeLines[0].p0;
|
|
64
|
-
const routeEnd = routeLines[lineCount - 1].p1;
|
|
65
|
-
const distDestToStart = pointDistance(to, routeStart);
|
|
66
|
-
const distDestToEnd = pointDistance(to, routeEnd);
|
|
67
|
-
const destinationAtPolylineStart = distDestToStart <= distDestToEnd;
|
|
68
|
-
const minRemain = Math.min(minRemainingUnits, totalLen);
|
|
69
|
-
if (from?.bounds &&
|
|
70
|
-
(posLayer == null || !from.layer || from.layer === posLayer) &&
|
|
71
|
-
pointInPolygon({ x: position.x, y: position.y }, from.bounds)) {
|
|
72
|
-
const remainingToDestination = totalLen;
|
|
73
|
-
const distanceFromStart = destinationAtPolylineStart
|
|
74
|
-
? remainingToDestination
|
|
75
|
-
: totalLen - remainingToDestination;
|
|
76
|
-
const final = pointAtDistanceSafe(routeLines, distanceFromStart);
|
|
77
|
-
return {
|
|
78
|
-
snapped: true,
|
|
79
|
-
snappedPoint: final.point,
|
|
80
|
-
snappedLine: final.line,
|
|
81
|
-
distance: remainingToDestination,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
if (to.bounds &&
|
|
85
|
-
(posLayer == null || !to.layer || to.layer === posLayer) &&
|
|
86
|
-
pointInPolygon({ x: position.x, y: position.y }, to.bounds)) {
|
|
87
|
-
const remainingToDestination = minRemain;
|
|
88
|
-
const passedSvgLength = totalLen - remainingToDestination;
|
|
89
|
-
const distanceFromStart = destinationAtPolylineStart ? remainingToDestination : passedSvgLength;
|
|
90
|
-
const final = pointAtDistanceSafe(routeLines, distanceFromStart);
|
|
91
|
-
return {
|
|
92
|
-
snapped: true,
|
|
93
|
-
snappedPoint: final.point,
|
|
94
|
-
snappedLine: final.line,
|
|
95
|
-
distance: remainingToDestination,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
if (!bestResult || bestDistance > snapThreshold)
|
|
42
|
+
if (!best || bestDistance > snapThreshold)
|
|
99
43
|
return { snapped: false };
|
|
100
|
-
let remainingToDestination = destinationAtPolylineStart
|
|
101
|
-
? bestResult.distanceFromPolylineStart
|
|
102
|
-
: totalLen - bestResult.distanceFromPolylineStart;
|
|
103
|
-
if (remainingToDestination < minRemain)
|
|
104
|
-
remainingToDestination = minRemain;
|
|
105
|
-
const passedSvgLength = totalLen - remainingToDestination;
|
|
106
|
-
const distanceFromStart = destinationAtPolylineStart ? remainingToDestination : passedSvgLength;
|
|
107
|
-
const final = pointAtDistanceSafe(routeLines, distanceFromStart);
|
|
108
44
|
return {
|
|
109
45
|
snapped: true,
|
|
110
|
-
snappedPoint:
|
|
111
|
-
snappedLine:
|
|
112
|
-
distance:
|
|
46
|
+
snappedPoint: best.point,
|
|
47
|
+
snappedLine: best.line,
|
|
48
|
+
distance: accumulatedLength - best.fromStart,
|
|
113
49
|
};
|
|
114
50
|
}
|
|
@@ -92,9 +92,9 @@ export function computeTransitionPoints(input) {
|
|
|
92
92
|
layer: visible.layer,
|
|
93
93
|
targetLayer: resolvedTarget,
|
|
94
94
|
direction: resolveDirection(floorOrder, visible.layer, resolvedTarget),
|
|
95
|
-
//
|
|
96
|
-
// to
|
|
97
|
-
role: currentLayerName ? (pickP0 ? '
|
|
95
|
+
// Current-floor endpoint at `p0` → route departs here → "exit" (clickable
|
|
96
|
+
// to advance); at `p1` → arrives here → "entry". Null layer → default "exit".
|
|
97
|
+
role: currentLayerName ? (pickP0 ? 'exit' : 'entry') : 'exit',
|
|
98
98
|
};
|
|
99
99
|
});
|
|
100
100
|
// 5. Deduplicate by coordinates. Drop entries that collide with an exit
|
|
@@ -28,16 +28,8 @@ export function buildMultiPointRoute(graph, candidateGroups) {
|
|
|
28
28
|
for (let i = 0; i < reachable.length - 1; i++) {
|
|
29
29
|
segmentResults.push(buildRoute(graph, reachable[i], reachable[i + 1]));
|
|
30
30
|
}
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// buildRoute(A, B) → lines [B→...→A]
|
|
34
|
-
// buildRoute(B, C) → lines [C→...→B]
|
|
35
|
-
//
|
|
36
|
-
// Without reverse: allLines = [B→...→A, C→...→B] — legs are disconnected.
|
|
37
|
-
// With reverse: allLines = [C→...→B, B→...→A] — endpoint B connects the
|
|
38
|
-
// chain, creating the continuous target→source polyline that
|
|
39
|
-
// route animation and snapping logic expect.
|
|
40
|
-
segmentResults.reverse();
|
|
31
|
+
// Legs concatenate directly: for [A, B, C], buildRoute(A, B) → [A→…→B] and
|
|
32
|
+
// buildRoute(B, C) → [B→…→C] share endpoint B, forming one continuous polyline.
|
|
41
33
|
const allLines = segmentResults.flatMap((result) => result.lines);
|
|
42
34
|
const totalDistance = segmentResults.reduce((sum, result) => sum + result.totalDistance, 0);
|
|
43
35
|
return { lines: allLines, totalDistance };
|
package/dist/core/types.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface RoutePoint {
|
|
|
15
15
|
export interface RouteLine {
|
|
16
16
|
readonly p0: RoutePoint;
|
|
17
17
|
readonly p1: RoutePoint;
|
|
18
|
-
/** Virtual
|
|
18
|
+
/** Virtual link rather than a physical path segment. May or may not change layers. */
|
|
19
19
|
readonly virtual: boolean;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
@@ -31,11 +31,10 @@ export interface GraphLine extends RouteLine {
|
|
|
31
31
|
/** Link weight for cost (`0` = unset → `DEFAULT_LINE_WEIGHT`). */
|
|
32
32
|
readonly weight: number;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* it avoided. When unset, the non-zero default keeps routes on the same floor.
|
|
34
|
+
* Per-line virtual cost from the designer ("Virtual length"). When set it
|
|
35
|
+
* replaces the default policy (`0` = free/preferred, large = avoided);
|
|
36
|
+
* otherwise a layer-changing link costs `FLOOR_TRANSITION_PENALTY`, a
|
|
37
|
+
* same-layer one is free.
|
|
39
38
|
*/
|
|
40
39
|
readonly virtualLength?: number;
|
|
41
40
|
}
|
|
@@ -99,8 +98,5 @@ export interface RouteEndpoint extends RoutePoint {
|
|
|
99
98
|
}
|
|
100
99
|
export interface SnapToRouteConfig {
|
|
101
100
|
readonly snapThreshold: number;
|
|
102
|
-
readonly minRemainingUnits?: number;
|
|
103
|
-
readonly to: RouteEndpoint;
|
|
104
|
-
readonly from?: RouteEndpoint;
|
|
105
101
|
}
|
|
106
102
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box } from '@expofp/geometry';
|
|
2
|
-
import { createRerouteController, getRouteLength,
|
|
2
|
+
import { createRerouteController, getRouteLength, } from '../core/index.js';
|
|
3
3
|
import { createEndpointView } from './endpointView.js';
|
|
4
4
|
import { getRouteLines } from './getRouteLines.js';
|
|
5
5
|
import { createPositionTrailView } from './positionTrailView.js';
|
|
@@ -7,6 +7,7 @@ import { CURRENT_POSITION_POINT_ID } from './positionView.js';
|
|
|
7
7
|
import { createRouteLinesView } from './routeLinesView.js';
|
|
8
8
|
import { computeRouteRenderData } from './routeRenderData.js';
|
|
9
9
|
import { computeRouteUpdate } from './routeUpdate.js';
|
|
10
|
+
import { snapPositionToRoute } from './snapPositionToRoute.js';
|
|
10
11
|
import { createTrailView } from './trailView.js';
|
|
11
12
|
import { createTransitionView } from './transitionView.js';
|
|
12
13
|
const linesToPoints = (lines) => lines.flatMap((line) => [line.p0, line.p1]);
|
|
@@ -84,8 +85,6 @@ export function createWayfindingRuntime({ engine, renderer, iconProvider, floorC
|
|
|
84
85
|
const data = computeRouteRenderData({
|
|
85
86
|
routeLines,
|
|
86
87
|
currentRouteLayer: getCurrentRouteLayer(),
|
|
87
|
-
from: currentFrom,
|
|
88
|
-
to: currentTo,
|
|
89
88
|
snap,
|
|
90
89
|
});
|
|
91
90
|
routeLinesView.draw(data, preserveProgress);
|
|
@@ -110,12 +109,14 @@ export function createWayfindingRuntime({ engine, renderer, iconProvider, floorC
|
|
|
110
109
|
// `hasSnappedCurrentRoute` only flips for a live-CP FROM (booth/kiosk origins never snap),
|
|
111
110
|
// so it already implies a CP origin — no need to re-check the endpoint id here.
|
|
112
111
|
const fromAnchor = hasSnappedCurrentRoute ? null : pickAnchor(route.from);
|
|
112
|
+
// The trail tip is the route's graph endpoint on the same side as its
|
|
113
|
+
// off-graph anchor: first point for FROM, last point for TO.
|
|
113
114
|
if (fromAnchor) {
|
|
114
|
-
positionTrails.setTrail(route.routePoints[
|
|
115
|
+
positionTrails.setTrail(route.routePoints[0], fromAnchor);
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
117
118
|
const toAnchor = pickAnchor(route.to);
|
|
118
|
-
positionTrails.setTrail(toAnchor ? route.routePoints[
|
|
119
|
+
positionTrails.setTrail(toAnchor ? route.routePoints[route.routePoints.length - 1] : null, toAnchor);
|
|
119
120
|
}
|
|
120
121
|
function syncRouteVisuals() {
|
|
121
122
|
if (!currentFrom || !currentTo)
|
|
@@ -157,11 +158,7 @@ export function createWayfindingRuntime({ engine, renderer, iconProvider, floorC
|
|
|
157
158
|
: // Selected floor doesn't pass through the route — view has nothing
|
|
158
159
|
// to draw, but distance must still update. Snap against the full
|
|
159
160
|
// non-virtual route directly.
|
|
160
|
-
|
|
161
|
-
snapThreshold,
|
|
162
|
-
from: currentFrom,
|
|
163
|
-
to: currentTo,
|
|
164
|
-
});
|
|
161
|
+
snapPositionToRoute(currentPosition, nonVirtual, currentFrom, currentTo, snapThreshold);
|
|
165
162
|
if (result.snapped)
|
|
166
163
|
hasSnappedCurrentRoute = true;
|
|
167
164
|
snappedToRoute = result.snapped;
|
|
@@ -10,8 +10,8 @@ export function createEndpointView({ renderer, iconProvider, floorContext, layer
|
|
|
10
10
|
return;
|
|
11
11
|
const fromOnFloor = floorContext.isLayerVisible(from.layer);
|
|
12
12
|
const toOnFloor = floorContext.isLayerVisible(to.layer);
|
|
13
|
-
const sourcePosition = visibleRoutePoints[
|
|
14
|
-
const destPosition = visibleRoutePoints[
|
|
13
|
+
const sourcePosition = visibleRoutePoints[0];
|
|
14
|
+
const destPosition = visibleRoutePoints[visibleRoutePoints.length - 1];
|
|
15
15
|
const sourceInactive = from.id === CURRENT_POSITION_POINT_ID && snapped;
|
|
16
16
|
renderer.setIcon('source', {
|
|
17
17
|
canvas: iconProvider.getIcon(sourceInactive ? 'source_inactive' : 'source').canvas,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { snapToRoute, } from '../core/index.js';
|
|
2
1
|
import { createPositionView } from './positionView.js';
|
|
2
|
+
import { snapPositionToRoute } from './snapPositionToRoute.js';
|
|
3
3
|
const TRAIL_SLOT = 'trail';
|
|
4
4
|
export function createPositionTrailView({ renderer, iconProvider, floorContext, trails, iconLayer, snapThreshold, }) {
|
|
5
5
|
const positionIcon = createPositionView({ renderer, iconProvider, layer: iconLayer });
|
|
@@ -12,11 +12,7 @@ export function createPositionTrailView({ renderer, iconProvider, floorContext,
|
|
|
12
12
|
positionIcon.hide();
|
|
13
13
|
},
|
|
14
14
|
applyToRoute(position, route, routeLines) {
|
|
15
|
-
const snap =
|
|
16
|
-
snapThreshold,
|
|
17
|
-
from: route.from,
|
|
18
|
-
to: route.to,
|
|
19
|
-
});
|
|
15
|
+
const snap = snapPositionToRoute(position, routeLines, route.from, route.to, snapThreshold);
|
|
20
16
|
const hidden = !floorContext.isLayerVisible(position.layer);
|
|
21
17
|
if (snap.snapped) {
|
|
22
18
|
positionIcon.place({ ...position, x: snap.snappedPoint.x, y: snap.snappedPoint.y }, hidden);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type RouteLine, type RouteSnapResult } from '../core/index.js';
|
|
2
2
|
interface RouteRenderData {
|
|
3
3
|
/** Route lines behind the current user position (empty when no snap). */
|
|
4
4
|
readonly passed: RouteLine[];
|
|
@@ -12,23 +12,19 @@ interface RouteRenderData {
|
|
|
12
12
|
* passed/remaining split for the visible floor plus the remaining
|
|
13
13
|
* distance to the destination.
|
|
14
14
|
*
|
|
15
|
-
* The full non-virtual route is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* The full non-virtual route is sliced down to the visible floor for rendering.
|
|
16
|
+
* The remaining distance is read from `snap.distance` (computed once by the
|
|
17
|
+
* runtime snap, `snapPositionToRoute`); with no snap it falls back to the full
|
|
18
|
+
* route length.
|
|
19
19
|
* @param root0
|
|
20
20
|
* @param root0.routeLines
|
|
21
21
|
* @param root0.currentRouteLayer
|
|
22
|
-
* @param root0.from
|
|
23
|
-
* @param root0.to
|
|
24
22
|
* @param root0.snap
|
|
25
23
|
*/
|
|
26
|
-
export declare function computeRouteRenderData({ routeLines, currentRouteLayer,
|
|
24
|
+
export declare function computeRouteRenderData({ routeLines, currentRouteLayer, snap, }: {
|
|
27
25
|
readonly routeLines: RouteLine[];
|
|
28
26
|
/** null = show-all mode; otherwise the active route floor. */
|
|
29
27
|
readonly currentRouteLayer: string | null;
|
|
30
|
-
readonly from: RouteEndpoint;
|
|
31
|
-
readonly to: RouteEndpoint;
|
|
32
28
|
readonly snap: RouteSnapResult | null;
|
|
33
29
|
}): RouteRenderData;
|
|
34
30
|
export {};
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import { getRouteLength,
|
|
1
|
+
import { getRouteLength, splitRouteByPoint, } from '../core/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Pure pipeline that produces the data needed to render the route: the
|
|
4
4
|
* passed/remaining split for the visible floor plus the remaining
|
|
5
5
|
* distance to the destination.
|
|
6
6
|
*
|
|
7
|
-
* The full non-virtual route is
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The full non-virtual route is sliced down to the visible floor for rendering.
|
|
8
|
+
* The remaining distance is read from `snap.distance` (computed once by the
|
|
9
|
+
* runtime snap, `snapPositionToRoute`); with no snap it falls back to the full
|
|
10
|
+
* route length.
|
|
11
11
|
* @param root0
|
|
12
12
|
* @param root0.routeLines
|
|
13
13
|
* @param root0.currentRouteLayer
|
|
14
|
-
* @param root0.from
|
|
15
|
-
* @param root0.to
|
|
16
14
|
* @param root0.snap
|
|
17
15
|
*/
|
|
18
|
-
export function computeRouteRenderData({ routeLines, currentRouteLayer,
|
|
19
|
-
const nonVirtualOrdered =
|
|
16
|
+
export function computeRouteRenderData({ routeLines, currentRouteLayer, snap, }) {
|
|
17
|
+
const nonVirtualOrdered = routeLines.filter((line) => !line.virtual);
|
|
20
18
|
const clipToVisibleFloor = (lines) => currentRouteLayer === null
|
|
21
19
|
? lines
|
|
22
20
|
: lines.filter((line) => line.p0.layer === currentRouteLayer);
|
|
@@ -34,7 +32,10 @@ export function computeRouteRenderData({ routeLines, currentRouteLayer, from, to
|
|
|
34
32
|
// Ref-equal result means no segment matched the snap layer (bestIndex === -1); only on a
|
|
35
33
|
// real split do we anchor the first remaining segment at the snap point.
|
|
36
34
|
const splitOccurred = remaining !== nonVirtualOrdered;
|
|
37
|
-
|
|
35
|
+
// Anchor only when `remaining[0]` is on the snap's floor — at a transition it
|
|
36
|
+
// already belongs to the next floor, and keeping its own `p0` survives the clip.
|
|
37
|
+
const anchorAtSnap = splitOccurred && remaining.length > 0 && remaining[0].p0.layer === snap.snappedPoint.layer;
|
|
38
|
+
const adjustedRemaining = anchorAtSnap
|
|
38
39
|
? [{ ...remaining[0], p0: snap.snappedPoint }, ...remaining.slice(1)]
|
|
39
40
|
: remaining;
|
|
40
41
|
return {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RouteEndpoint, type RouteLine, type RoutePoint, type RouteSnapResult } from '../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Snap a position to the from→to route, resolving from/to booth containment
|
|
4
|
+
* first (domain logic kept out of the geometric `snapToRoute`):
|
|
5
|
+
* - inside the origin booth → route start, full distance remaining;
|
|
6
|
+
* - inside the destination booth → route end, zero remaining;
|
|
7
|
+
* - otherwise → geometric snap within `snapThreshold`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function snapPositionToRoute(position: RoutePoint, routeLines: readonly RouteLine[], from: RouteEndpoint, to: RouteEndpoint, snapThreshold: number): RouteSnapResult;
|
|
10
|
+
//# sourceMappingURL=snapPositionToRoute.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { pointInPolygon } from '../core/geometry/pointInPolygon.js';
|
|
2
|
+
import { getRouteLength, snapToRoute, } from '../core/index.js';
|
|
3
|
+
/** Whether `position` lies inside `endpoint`'s booth bounds on a matching layer. */
|
|
4
|
+
function insideBounds(position, endpoint, posLayer) {
|
|
5
|
+
if (!endpoint?.bounds)
|
|
6
|
+
return false;
|
|
7
|
+
const layerMatches = posLayer == null || !endpoint.layer || endpoint.layer === posLayer;
|
|
8
|
+
const containsPosition = pointInPolygon({ x: position.x, y: position.y }, endpoint.bounds);
|
|
9
|
+
return layerMatches && containsPosition;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Snap a position to the from→to route, resolving from/to booth containment
|
|
13
|
+
* first (domain logic kept out of the geometric `snapToRoute`):
|
|
14
|
+
* - inside the origin booth → route start, full distance remaining;
|
|
15
|
+
* - inside the destination booth → route end, zero remaining;
|
|
16
|
+
* - otherwise → geometric snap within `snapThreshold`.
|
|
17
|
+
*/
|
|
18
|
+
export function snapPositionToRoute(position, routeLines, from, to, snapThreshold) {
|
|
19
|
+
if (!routeLines.length)
|
|
20
|
+
return { snapped: false };
|
|
21
|
+
const posLayer = position.layer || null;
|
|
22
|
+
if (insideBounds(position, from, posLayer)) {
|
|
23
|
+
const first = routeLines[0];
|
|
24
|
+
return {
|
|
25
|
+
snapped: true,
|
|
26
|
+
snappedPoint: first.p0,
|
|
27
|
+
snappedLine: first,
|
|
28
|
+
distance: getRouteLength(routeLines),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (insideBounds(position, to, posLayer)) {
|
|
32
|
+
const last = routeLines[routeLines.length - 1];
|
|
33
|
+
return { snapped: true, snappedPoint: last.p1, snappedLine: last, distance: 0 };
|
|
34
|
+
}
|
|
35
|
+
return snapToRoute(position, routeLines, { snapThreshold });
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/wayfinding",
|
|
3
|
-
"version": "3.13.
|
|
3
|
+
"version": "3.13.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ExpoFP SDK internal: framework-neutral wayfinding (routing, snapping, scene rendering)",
|
|
6
6
|
"homepage": "https://developer.expofp.com/",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"ngraph.graph": "^19.1.0",
|
|
36
36
|
"tslib": "^2.3.0",
|
|
37
|
-
"@expofp/geometry": "3.13.
|
|
37
|
+
"@expofp/geometry": "3.13.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@expofp/renderer": "^3.2.4"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { RouteLine, RoutePoint } from '../types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Ensures that `routeLines` run in the from→to direction.
|
|
4
|
-
*
|
|
5
|
-
* A* returns paths in target→source order. For rendering (arrow animation,
|
|
6
|
-
* route progress split) and remaining-distance computation the lines must
|
|
7
|
-
* go from the origin to the destination.
|
|
8
|
-
*
|
|
9
|
-
* Direction is decided by projecting `from` (or `to` as a fallback) onto
|
|
10
|
-
* `routeLines`: if the anchor lands closer to the polyline end than to
|
|
11
|
-
* its start, the array is reversed and each segment's endpoints are
|
|
12
|
-
* flipped to keep continuity.
|
|
13
|
-
* @param routeLines Lines to normalize.
|
|
14
|
-
* @param from Route origin (user-selected "from" point).
|
|
15
|
-
* @param to Route destination (user-selected "to" point).
|
|
16
|
-
* @returns Lines guaranteed to run from→to, or the original array unchanged.
|
|
17
|
-
*/
|
|
18
|
-
export declare function normalizeRouteDirection(routeLines: RouteLine[], from: RoutePoint, to: RoutePoint): RouteLine[];
|
|
19
|
-
//# sourceMappingURL=normalizeRouteDirection.d.ts.map
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { pointDistance } from '@expofp/geometry';
|
|
2
|
-
/**
|
|
3
|
-
* Projects `point` onto the closest segment of `segments` that shares the same layer,
|
|
4
|
-
* returning the arc-length distance from the polyline start to the projection.
|
|
5
|
-
* @param point
|
|
6
|
-
* @param segments
|
|
7
|
-
* @param segmentLengths
|
|
8
|
-
*/
|
|
9
|
-
function projectPointOnRoute(point, segments, segmentLengths) {
|
|
10
|
-
let accumulatedLength = 0;
|
|
11
|
-
let bestDistance = Infinity;
|
|
12
|
-
let bestFromStart = Infinity;
|
|
13
|
-
const total = segmentLengths.reduce((sum, len) => sum + len, 0);
|
|
14
|
-
for (let i = 0; i < segments.length; i++) {
|
|
15
|
-
const segment = segments[i];
|
|
16
|
-
const segmentLength = segmentLengths[i];
|
|
17
|
-
// Empty `point.layer` = wildcard (single-floor plans without layers).
|
|
18
|
-
if (point.layer && segment.p0.layer !== point.layer) {
|
|
19
|
-
accumulatedLength += segmentLength;
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
const dx = segment.p1.x - segment.p0.x;
|
|
23
|
-
const dy = segment.p1.y - segment.p0.y;
|
|
24
|
-
const lengthSquared = dx * dx + dy * dy;
|
|
25
|
-
// Near-zero-length segment — nothing meaningful to project onto.
|
|
26
|
-
if (lengthSquared < 1e-10) {
|
|
27
|
-
accumulatedLength += segmentLength;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
// Project point onto segment, clamped to [0, 1].
|
|
31
|
-
const t = ((point.x - segment.p0.x) * dx + (point.y - segment.p0.y) * dy) / lengthSquared;
|
|
32
|
-
const clamped = Math.max(0, Math.min(1, t));
|
|
33
|
-
const projectedX = segment.p0.x + dx * clamped;
|
|
34
|
-
const projectedY = segment.p0.y + dy * clamped;
|
|
35
|
-
const distance = Math.hypot(point.x - projectedX, point.y - projectedY);
|
|
36
|
-
if (distance < bestDistance) {
|
|
37
|
-
bestDistance = distance;
|
|
38
|
-
bestFromStart = accumulatedLength + segmentLength * clamped;
|
|
39
|
-
}
|
|
40
|
-
accumulatedLength += segmentLength;
|
|
41
|
-
}
|
|
42
|
-
return { found: bestFromStart !== Infinity, fromStart: bestFromStart, total };
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Ensures that `routeLines` run in the from→to direction.
|
|
46
|
-
*
|
|
47
|
-
* A* returns paths in target→source order. For rendering (arrow animation,
|
|
48
|
-
* route progress split) and remaining-distance computation the lines must
|
|
49
|
-
* go from the origin to the destination.
|
|
50
|
-
*
|
|
51
|
-
* Direction is decided by projecting `from` (or `to` as a fallback) onto
|
|
52
|
-
* `routeLines`: if the anchor lands closer to the polyline end than to
|
|
53
|
-
* its start, the array is reversed and each segment's endpoints are
|
|
54
|
-
* flipped to keep continuity.
|
|
55
|
-
* @param routeLines Lines to normalize.
|
|
56
|
-
* @param from Route origin (user-selected "from" point).
|
|
57
|
-
* @param to Route destination (user-selected "to" point).
|
|
58
|
-
* @returns Lines guaranteed to run from→to, or the original array unchanged.
|
|
59
|
-
*/
|
|
60
|
-
export function normalizeRouteDirection(routeLines, from, to) {
|
|
61
|
-
// Precompute segment lengths once (used for both projections).
|
|
62
|
-
const segmentLengths = routeLines.map((line) => pointDistance(line.p0, line.p1));
|
|
63
|
-
// Try to anchor using `from`. Fall back to `to` if `from` doesn't land on any same-layer segment.
|
|
64
|
-
const fromProjection = projectPointOnRoute(from, routeLines, segmentLengths);
|
|
65
|
-
const anchor = fromProjection.found
|
|
66
|
-
? fromProjection
|
|
67
|
-
: projectPointOnRoute(to, routeLines, segmentLengths);
|
|
68
|
-
if (!anchor.found)
|
|
69
|
-
return routeLines;
|
|
70
|
-
const distToStart = anchor.fromStart;
|
|
71
|
-
const distToEnd = anchor.total - anchor.fromStart;
|
|
72
|
-
// If `from` was used as anchor: `from` should be closer to the start.
|
|
73
|
-
// If `to` was used as fallback: `to` should be closer to the end.
|
|
74
|
-
const shouldReverse = fromProjection.found ? distToEnd < distToStart : distToStart < distToEnd;
|
|
75
|
-
if (!shouldReverse)
|
|
76
|
-
return routeLines;
|
|
77
|
-
// Reverse segment order AND flip each segment's endpoints to maintain continuity.
|
|
78
|
-
return routeLines
|
|
79
|
-
.slice()
|
|
80
|
-
.reverse()
|
|
81
|
-
.map((line) => ({ ...line, p0: line.p1, p1: line.p0 }));
|
|
82
|
-
}
|