@expofp/wayfinding 3.11.11 → 3.11.12
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 +4 -7
- package/dist/core/graph/constants.js +4 -7
- package/dist/core/graph/findShortestPath.d.ts +0 -2
- package/dist/core/graph/findShortestPath.js +0 -2
- package/dist/core/graph/graphHelpers.js +2 -5
- package/dist/core/graph/linkCost.d.ts +1 -7
- package/dist/core/graph/linkCost.js +5 -18
- package/dist/core/graph/pathfinder/aStarPathFinder.js +4 -7
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/position/snapToRoute.d.ts +1 -6
- package/dist/core/position/snapToRoute.js +79 -15
- package/dist/core/rendering/computeTransitionPoints.js +3 -3
- package/dist/core/routing/buildMultiPointRoute.js +10 -2
- package/dist/core/routing/getRouteLength.d.ts +1 -1
- package/dist/core/types.d.ts +9 -5
- package/dist/runtime/createWayfindingRuntime.js +10 -7
- package/dist/runtime/endpointView.js +2 -2
- package/dist/runtime/positionTrailView.js +6 -2
- package/dist/runtime/routeRenderData.d.ts +10 -6
- package/dist/runtime/routeRenderData.js +10 -11
- package/package.json +2 -2
- package/dist/runtime/snapPositionToRoute.d.ts +0 -10
- package/dist/runtime/snapPositionToRoute.js +0 -36
|
@@ -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
|
+
* A* returns TO→FROM order, so the FROM boundary is the last line.
|
|
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[lines.length - 1];
|
|
19
|
+
const chosenEndpoint = boundary.p1;
|
|
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 [{ ...boundary,
|
|
23
|
+
if (pointEquals(boundary.p0, otherEnd)) {
|
|
24
|
+
return [...lines.slice(0, -1), { ...boundary, p1: entry.projection }];
|
|
25
25
|
}
|
|
26
|
-
return [{ ...entry.segment, p0:
|
|
26
|
+
return [...lines, { ...entry.segment, p0: chosenEndpoint, p1: entry.projection }];
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Mirror of {@link attachFromEntry} for the TO side
|
|
30
|
-
*
|
|
29
|
+
* Mirror of {@link attachFromEntry} for the TO side. TO boundary is the
|
|
30
|
+
* FIRST line; `boundary.p0` is the chosen graph node on the TO end.
|
|
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[0];
|
|
38
|
+
const chosenEndpoint = boundary.p0;
|
|
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 [
|
|
42
|
+
if (pointEquals(boundary.p1, otherEnd)) {
|
|
43
|
+
return [{ ...boundary, p0: entry.projection }, ...lines.slice(1)];
|
|
44
44
|
}
|
|
45
|
-
return [
|
|
45
|
+
return [{ ...entry.segment, p0: entry.projection, p1: chosenEndpoint }, ...lines];
|
|
46
46
|
}
|
|
47
47
|
function attachOffGraphEntries(lines, fromEntry, toEntry) {
|
|
48
48
|
let result = lines;
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Default A* cost for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the destination. A per-line `virtualLength` overrides it (designer control).
|
|
2
|
+
* Default A* cost for a virtual (floor-transition) link. Non-zero so a floor
|
|
3
|
+
* change costs more than any same-floor detour; otherwise floor changes are free
|
|
4
|
+
* and a route between two points on one floor can detour through another.
|
|
6
5
|
*/
|
|
7
|
-
export declare const
|
|
8
|
-
/** Default A* cost for a virtual link that stays on the same layer (free). */
|
|
9
|
-
export declare const VIRTUAL_LINE_PENALTY = 0;
|
|
6
|
+
export declare const VIRTUAL_LINE_PENALTY = 10000;
|
|
10
7
|
/**
|
|
11
8
|
* Fallback line weight when `line.weight` is 0 (meaning "not set" in floorplan data,
|
|
12
9
|
* not "free passage").
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Default A* cost for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the destination. A per-line `virtualLength` overrides it (designer control).
|
|
2
|
+
* Default A* cost for a virtual (floor-transition) link. Non-zero so a floor
|
|
3
|
+
* change costs more than any same-floor detour; otherwise floor changes are free
|
|
4
|
+
* and a route between two points on one floor can detour through another.
|
|
6
5
|
*/
|
|
7
|
-
export const
|
|
8
|
-
/** Default A* cost for a virtual link that stays on the same layer (free). */
|
|
9
|
-
export const VIRTUAL_LINE_PENALTY = 0;
|
|
6
|
+
export const VIRTUAL_LINE_PENALTY = 10000;
|
|
10
7
|
/**
|
|
11
8
|
* Fallback line weight when `line.weight` is 0 (meaning "not set" in floorplan data,
|
|
12
9
|
* not "free passage").
|
|
@@ -1,8 +1,6 @@
|
|
|
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.
|
|
6
4
|
* @param points
|
|
7
5
|
* @param lines
|
|
8
6
|
*/
|
|
@@ -6,8 +6,6 @@ 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.
|
|
11
9
|
* @param points
|
|
12
10
|
* @param lines
|
|
13
11
|
*/
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { degToRad, lineAngle, pointDistance } from '@expofp/geometry';
|
|
2
|
-
export const toNodeId = (p) => `${p.layer
|
|
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
|
-
|
|
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;
|
|
5
|
+
export const arePointsClose = (p1, p2) => p1.layer === p2.layer && pointDistance(p1, p2) <= POINT_PROXIMITY_THRESHOLD;
|
|
9
6
|
export const matchesLine = (line, p0, p1) => (arePointsClose(line.p0, p0) && arePointsClose(line.p1, p1)) ||
|
|
10
7
|
(arePointsClose(line.p0, p1) && arePointsClose(line.p1, p0));
|
|
11
8
|
export function findLineByEndpoints(lines, p0, p1) {
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { type GraphLine } from '../types.js';
|
|
2
|
-
export declare function changesLayer(line: GraphLine): boolean;
|
|
3
2
|
/**
|
|
4
|
-
* Weighted cost of a graph edge
|
|
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`).
|
|
3
|
+
* Weighted cost of a route line for A* graph edge (legacy formula).
|
|
10
4
|
* @param line
|
|
11
5
|
*/
|
|
12
6
|
export declare function linkCost(line: GraphLine): number;
|
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
import { pointDistance } from '@expofp/geometry';
|
|
2
|
-
import { DEFAULT_LINE_WEIGHT,
|
|
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
|
-
}
|
|
2
|
+
import { DEFAULT_LINE_WEIGHT, VIRTUAL_LINE_PENALTY } from './constants.js';
|
|
9
3
|
/**
|
|
10
|
-
* Weighted cost of a graph edge
|
|
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`).
|
|
4
|
+
* Weighted cost of a route line for A* graph edge (legacy formula).
|
|
16
5
|
* @param line
|
|
17
6
|
*/
|
|
18
7
|
export function linkCost(line) {
|
|
19
|
-
if (
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
return line.virtualLength;
|
|
23
|
-
return changesLayer(line) ? FLOOR_TRANSITION_PENALTY : VIRTUAL_LINE_PENALTY;
|
|
8
|
+
if (line.virtual)
|
|
9
|
+
return line.virtualLength ?? VIRTUAL_LINE_PENALTY;
|
|
10
|
+
return pointDistance(line.p0, line.p1) / (line.weight || DEFAULT_LINE_WEIGHT);
|
|
24
11
|
}
|
|
@@ -41,9 +41,8 @@ export function createAStarPathFinder() {
|
|
|
41
41
|
return !!graph.hasNode(id);
|
|
42
42
|
},
|
|
43
43
|
/**
|
|
44
|
-
* Returns path nodes in **source
|
|
45
|
-
*
|
|
46
|
-
* is reversed here once, at the source, so no downstream reversal is needed.
|
|
44
|
+
* Returns path nodes in **target→source** order (A* reconstructs from the goal);
|
|
45
|
+
* downstream consumers depend on this ordering.
|
|
47
46
|
* @param fromId
|
|
48
47
|
* @param toId
|
|
49
48
|
*/
|
|
@@ -53,12 +52,10 @@ export function createAStarPathFinder() {
|
|
|
53
52
|
const nodes = finder.find(fromId, toId);
|
|
54
53
|
if (!nodes.length)
|
|
55
54
|
return [];
|
|
56
|
-
return nodes
|
|
57
|
-
.map((node) => {
|
|
55
|
+
return nodes.map((node) => {
|
|
58
56
|
const [layer, x, y] = parseNodeId(node.id);
|
|
59
57
|
return { id: node.id, layer, x, y };
|
|
60
|
-
})
|
|
61
|
-
.reverse();
|
|
58
|
+
});
|
|
62
59
|
},
|
|
63
60
|
};
|
|
64
61
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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';
|
|
9
10
|
export { computeBoundingBox } from './rendering/routeGeometry.js';
|
|
10
11
|
export { getRouteLength } from './routing/getRouteLength.js';
|
|
11
12
|
export type { GpsProjectionConfig } from './position/gpsThreshold.js';
|
package/dist/core/index.js
CHANGED
|
@@ -3,6 +3,7 @@ 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';
|
|
6
7
|
export { computeBoundingBox } from './rendering/routeGeometry.js';
|
|
7
8
|
// Routing utilities
|
|
8
9
|
export { getRouteLength } from './routing/getRouteLength.js';
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import { type RouteLine, type RoutePoint, type RouteSnapResult, type SnapToRouteConfig } from '../types.js';
|
|
2
2
|
export { SNAP_THRESHOLD_METERS } from './gpsThreshold.js';
|
|
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;
|
|
3
|
+
export declare function snapToRoute(position: RoutePoint, routeLines: RouteLine[], config: SnapToRouteConfig): RouteSnapResult;
|
|
9
4
|
//# sourceMappingURL=snapToRoute.d.ts.map
|
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import { pointDistance } from '@expofp/geometry';
|
|
2
|
+
import { pointInPolygon } from '../geometry/pointInPolygon.js';
|
|
2
3
|
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
|
+
}
|
|
3
27
|
export { SNAP_THRESHOLD_METERS } from './gpsThreshold.js';
|
|
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 }) {
|
|
28
|
+
export function snapToRoute(position, routeLines, config) {
|
|
10
29
|
const lineCount = routeLines.length;
|
|
11
30
|
if (!lineCount)
|
|
12
31
|
return { snapped: false };
|
|
32
|
+
const { snapThreshold, to, from, minRemainingUnits = 0.5 } = config;
|
|
13
33
|
const posLayer = position.layer || null;
|
|
14
34
|
let bestDistance = Infinity;
|
|
15
|
-
let
|
|
35
|
+
let bestResult = null;
|
|
16
36
|
let accumulatedLength = 0;
|
|
17
37
|
let hasSegmentOnPosLayer = posLayer == null;
|
|
18
38
|
for (let i = 0; i < lineCount; i++) {
|
|
@@ -29,22 +49,66 @@ export function snapToRoute(position, routeLines, { snapThreshold }) {
|
|
|
29
49
|
const projection = projectPointOnSegment({ x: position.x, y: position.y }, p0, p1);
|
|
30
50
|
if (projection.distance < bestDistance) {
|
|
31
51
|
bestDistance = projection.distance;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
bestResult = {
|
|
53
|
+
segmentIndex: i,
|
|
54
|
+
t: projection.t,
|
|
55
|
+
distanceFromPolylineStart: accumulatedLength + segLen * projection.t,
|
|
36
56
|
};
|
|
37
57
|
}
|
|
38
58
|
accumulatedLength += segLen;
|
|
39
59
|
}
|
|
40
60
|
if (posLayer != null && !hasSegmentOnPosLayer)
|
|
41
61
|
return { snapped: false };
|
|
42
|
-
|
|
62
|
+
const totalLen = accumulatedLength;
|
|
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)
|
|
43
99
|
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);
|
|
44
108
|
return {
|
|
45
109
|
snapped: true,
|
|
46
|
-
snappedPoint:
|
|
47
|
-
snappedLine:
|
|
48
|
-
distance:
|
|
110
|
+
snappedPoint: final.point,
|
|
111
|
+
snappedLine: final.line,
|
|
112
|
+
distance: remainingToDestination,
|
|
49
113
|
};
|
|
50
114
|
}
|
|
@@ -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
|
+
// When currentLayerName is null, pickP0 is always true — default to "exit"
|
|
96
|
+
// to preserve existing behavior (all points show stairs icon).
|
|
97
|
+
role: currentLayerName ? (pickP0 ? 'entry' : 'exit') : 'exit',
|
|
98
98
|
};
|
|
99
99
|
});
|
|
100
100
|
// 5. Deduplicate by coordinates. Drop entries that collide with an exit
|
|
@@ -28,8 +28,16 @@ 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
|
-
//
|
|
31
|
+
// A* returns each leg in target→source order (see aStarPathFinder.find()).
|
|
32
|
+
// For candidates [A, B, C], legs are computed as:
|
|
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();
|
|
33
41
|
const allLines = segmentResults.flatMap((result) => result.lines);
|
|
34
42
|
const totalDistance = segmentResults.reduce((sum, result) => sum + result.totalDistance, 0);
|
|
35
43
|
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 link rather than a physical path segment.
|
|
18
|
+
/** Virtual (floor-transition) link rather than a physical path segment. */
|
|
19
19
|
readonly virtual: boolean;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
@@ -31,10 +31,11 @@ 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
|
-
*
|
|
34
|
+
* Optional custom cost for virtual (floor-transition) links, configured
|
|
35
|
+
* per-line in the designer as "Virtual length". When set, the pathfinder
|
|
36
|
+
* uses this value instead of the default VIRTUAL_LINE_PENALTY, overriding it
|
|
37
|
+
* per line — `0` makes the transition free (preferred), a large value makes
|
|
38
|
+
* it avoided. When unset, the non-zero default keeps routes on the same floor.
|
|
38
39
|
*/
|
|
39
40
|
readonly virtualLength?: number;
|
|
40
41
|
}
|
|
@@ -98,5 +99,8 @@ export interface RouteEndpoint extends RoutePoint {
|
|
|
98
99
|
}
|
|
99
100
|
export interface SnapToRouteConfig {
|
|
100
101
|
readonly snapThreshold: number;
|
|
102
|
+
readonly minRemainingUnits?: number;
|
|
103
|
+
readonly to: RouteEndpoint;
|
|
104
|
+
readonly from?: RouteEndpoint;
|
|
101
105
|
}
|
|
102
106
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box } from '@expofp/geometry';
|
|
2
|
-
import { createRerouteController, getRouteLength, } from '../core/index.js';
|
|
2
|
+
import { createRerouteController, getRouteLength, snapToRoute, } 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,7 +7,6 @@ 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';
|
|
11
10
|
import { createTrailView } from './trailView.js';
|
|
12
11
|
import { createTransitionView } from './transitionView.js';
|
|
13
12
|
const linesToPoints = (lines) => lines.flatMap((line) => [line.p0, line.p1]);
|
|
@@ -85,6 +84,8 @@ export function createWayfindingRuntime({ engine, renderer, iconProvider, floorC
|
|
|
85
84
|
const data = computeRouteRenderData({
|
|
86
85
|
routeLines,
|
|
87
86
|
currentRouteLayer: getCurrentRouteLayer(),
|
|
87
|
+
from: currentFrom,
|
|
88
|
+
to: currentTo,
|
|
88
89
|
snap,
|
|
89
90
|
});
|
|
90
91
|
routeLinesView.draw(data, preserveProgress);
|
|
@@ -109,14 +110,12 @@ export function createWayfindingRuntime({ engine, renderer, iconProvider, floorC
|
|
|
109
110
|
// `hasSnappedCurrentRoute` only flips for a live-CP FROM (booth/kiosk origins never snap),
|
|
110
111
|
// so it already implies a CP origin — no need to re-check the endpoint id here.
|
|
111
112
|
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.
|
|
114
113
|
if (fromAnchor) {
|
|
115
|
-
positionTrails.setTrail(route.routePoints[
|
|
114
|
+
positionTrails.setTrail(route.routePoints[route.routePoints.length - 1], fromAnchor);
|
|
116
115
|
return;
|
|
117
116
|
}
|
|
118
117
|
const toAnchor = pickAnchor(route.to);
|
|
119
|
-
positionTrails.setTrail(toAnchor ? route.routePoints[
|
|
118
|
+
positionTrails.setTrail(toAnchor ? route.routePoints[0] : null, toAnchor);
|
|
120
119
|
}
|
|
121
120
|
function syncRouteVisuals() {
|
|
122
121
|
if (!currentFrom || !currentTo)
|
|
@@ -158,7 +157,11 @@ export function createWayfindingRuntime({ engine, renderer, iconProvider, floorC
|
|
|
158
157
|
: // Selected floor doesn't pass through the route — view has nothing
|
|
159
158
|
// to draw, but distance must still update. Snap against the full
|
|
160
159
|
// non-virtual route directly.
|
|
161
|
-
|
|
160
|
+
snapToRoute(currentPosition, nonVirtual, {
|
|
161
|
+
snapThreshold,
|
|
162
|
+
from: currentFrom,
|
|
163
|
+
to: currentTo,
|
|
164
|
+
});
|
|
162
165
|
if (result.snapped)
|
|
163
166
|
hasSnappedCurrentRoute = true;
|
|
164
167
|
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[visibleRoutePoints.length - 1];
|
|
14
|
+
const destPosition = visibleRoutePoints[0];
|
|
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';
|
|
1
2
|
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,7 +12,11 @@ export function createPositionTrailView({ renderer, iconProvider, floorContext,
|
|
|
12
12
|
positionIcon.hide();
|
|
13
13
|
},
|
|
14
14
|
applyToRoute(position, route, routeLines) {
|
|
15
|
-
const snap =
|
|
15
|
+
const snap = snapToRoute(position, [...routeLines], {
|
|
16
|
+
snapThreshold,
|
|
17
|
+
from: route.from,
|
|
18
|
+
to: route.to,
|
|
19
|
+
});
|
|
16
20
|
const hidden = !floorContext.isLayerVisible(position.layer);
|
|
17
21
|
if (snap.snapped) {
|
|
18
22
|
positionIcon.place({ ...position, x: snap.snappedPoint.x, y: snap.snappedPoint.y }, hidden);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RouteLine, type RouteSnapResult } from '../core/index.js';
|
|
1
|
+
import { type RouteEndpoint, 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,19 +12,23 @@ 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
|
-
* route length.
|
|
15
|
+
* The full non-virtual route is normalized to from→to direction once and
|
|
16
|
+
* sliced down to the visible floor for rendering. The remaining distance
|
|
17
|
+
* is read from `snap.distance` (computed once inside `snapToRoute`); when
|
|
18
|
+
* no snap is active we fall back to the full route length.
|
|
19
19
|
* @param root0
|
|
20
20
|
* @param root0.routeLines
|
|
21
21
|
* @param root0.currentRouteLayer
|
|
22
|
+
* @param root0.from
|
|
23
|
+
* @param root0.to
|
|
22
24
|
* @param root0.snap
|
|
23
25
|
*/
|
|
24
|
-
export declare function computeRouteRenderData({ routeLines, currentRouteLayer, snap, }: {
|
|
26
|
+
export declare function computeRouteRenderData({ routeLines, currentRouteLayer, from, to, snap, }: {
|
|
25
27
|
readonly routeLines: RouteLine[];
|
|
26
28
|
/** null = show-all mode; otherwise the active route floor. */
|
|
27
29
|
readonly currentRouteLayer: string | null;
|
|
30
|
+
readonly from: RouteEndpoint;
|
|
31
|
+
readonly to: RouteEndpoint;
|
|
28
32
|
readonly snap: RouteSnapResult | null;
|
|
29
33
|
}): RouteRenderData;
|
|
30
34
|
export {};
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { getRouteLength, splitRouteByPoint, } from '../core/index.js';
|
|
1
|
+
import { getRouteLength, normalizeRouteDirection, 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
|
-
* route length.
|
|
7
|
+
* The full non-virtual route is normalized to from→to direction once and
|
|
8
|
+
* sliced down to the visible floor for rendering. The remaining distance
|
|
9
|
+
* is read from `snap.distance` (computed once inside `snapToRoute`); when
|
|
10
|
+
* no snap is active we fall back to the full route length.
|
|
11
11
|
* @param root0
|
|
12
12
|
* @param root0.routeLines
|
|
13
13
|
* @param root0.currentRouteLayer
|
|
14
|
+
* @param root0.from
|
|
15
|
+
* @param root0.to
|
|
14
16
|
* @param root0.snap
|
|
15
17
|
*/
|
|
16
|
-
export function computeRouteRenderData({ routeLines, currentRouteLayer, snap, }) {
|
|
17
|
-
const nonVirtualOrdered = routeLines.filter((line) => !line.virtual);
|
|
18
|
+
export function computeRouteRenderData({ routeLines, currentRouteLayer, from, to, snap, }) {
|
|
19
|
+
const nonVirtualOrdered = normalizeRouteDirection(routeLines.filter((line) => !line.virtual), from, to);
|
|
18
20
|
const clipToVisibleFloor = (lines) => currentRouteLayer === null
|
|
19
21
|
? lines
|
|
20
22
|
: lines.filter((line) => line.p0.layer === currentRouteLayer);
|
|
@@ -32,10 +34,7 @@ export function computeRouteRenderData({ routeLines, currentRouteLayer, snap, })
|
|
|
32
34
|
// Ref-equal result means no segment matched the snap layer (bestIndex === -1); only on a
|
|
33
35
|
// real split do we anchor the first remaining segment at the snap point.
|
|
34
36
|
const splitOccurred = remaining !== nonVirtualOrdered;
|
|
35
|
-
|
|
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
|
|
37
|
+
const adjustedRemaining = splitOccurred && remaining.length > 0
|
|
39
38
|
? [{ ...remaining[0], p0: snap.snappedPoint }, ...remaining.slice(1)]
|
|
40
39
|
: remaining;
|
|
41
40
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/wayfinding",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.12",
|
|
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.11.
|
|
37
|
+
"@expofp/geometry": "3.11.12"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@expofp/renderer": "^3.2.4"
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
}
|