@expofp/wayfinding 3.11.11

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.
Files changed (110) hide show
  1. package/README.md +84 -0
  2. package/dist/core/createWayfindingEngine.d.ts +16 -0
  3. package/dist/core/createWayfindingEngine.js +87 -0
  4. package/dist/core/geometry/pointInPolygon.d.ts +17 -0
  5. package/dist/core/geometry/pointInPolygon.js +50 -0
  6. package/dist/core/geometry/projectPointOnSegment.d.ts +27 -0
  7. package/dist/core/geometry/projectPointOnSegment.js +21 -0
  8. package/dist/core/graph/buildGraph.d.ts +3 -0
  9. package/dist/core/graph/buildGraph.js +7 -0
  10. package/dist/core/graph/buildNGraph.d.ts +13 -0
  11. package/dist/core/graph/buildNGraph.js +24 -0
  12. package/dist/core/graph/constants.d.ts +20 -0
  13. package/dist/core/graph/constants.js +19 -0
  14. package/dist/core/graph/findShortestPath.d.ts +23 -0
  15. package/dist/core/graph/findShortestPath.js +65 -0
  16. package/dist/core/graph/graphCache.d.ts +7 -0
  17. package/dist/core/graph/graphCache.js +20 -0
  18. package/dist/core/graph/graphHelpers.d.ts +8 -0
  19. package/dist/core/graph/graphHelpers.js +52 -0
  20. package/dist/core/graph/linkCost.d.ts +13 -0
  21. package/dist/core/graph/linkCost.js +24 -0
  22. package/dist/core/graph/pathfinder/aStarPathFinder.d.ts +3 -0
  23. package/dist/core/graph/pathfinder/aStarPathFinder.js +64 -0
  24. package/dist/core/graph/pathfinder/parseNodeId.d.ts +10 -0
  25. package/dist/core/graph/pathfinder/parseNodeId.js +20 -0
  26. package/dist/core/index.d.ts +17 -0
  27. package/dist/core/index.js +12 -0
  28. package/dist/core/position/distanceToRoute.d.ts +3 -0
  29. package/dist/core/position/distanceToRoute.js +29 -0
  30. package/dist/core/position/gpsThreshold.d.ts +32 -0
  31. package/dist/core/position/gpsThreshold.js +48 -0
  32. package/dist/core/position/rerouteController.d.ts +13 -0
  33. package/dist/core/position/rerouteController.js +22 -0
  34. package/dist/core/position/snapToRoute.d.ts +9 -0
  35. package/dist/core/position/snapToRoute.js +50 -0
  36. package/dist/core/position/splitRouteByPoint.d.ts +6 -0
  37. package/dist/core/position/splitRouteByPoint.js +56 -0
  38. package/dist/core/rendering/computeTrailPoints.d.ts +12 -0
  39. package/dist/core/rendering/computeTrailPoints.js +35 -0
  40. package/dist/core/rendering/computeTransitionPoints.d.ts +32 -0
  41. package/dist/core/rendering/computeTransitionPoints.js +114 -0
  42. package/dist/core/rendering/getVisibleRouteLines.d.ts +10 -0
  43. package/dist/core/rendering/getVisibleRouteLines.js +18 -0
  44. package/dist/core/rendering/normalizeRouteDirection.d.ts +19 -0
  45. package/dist/core/rendering/normalizeRouteDirection.js +82 -0
  46. package/dist/core/rendering/routeGeometry.d.ts +9 -0
  47. package/dist/core/rendering/routeGeometry.js +10 -0
  48. package/dist/core/routing/buildMultiPointRoute.d.ts +13 -0
  49. package/dist/core/routing/buildMultiPointRoute.js +36 -0
  50. package/dist/core/routing/buildRoute.d.ts +12 -0
  51. package/dist/core/routing/buildRoute.js +19 -0
  52. package/dist/core/routing/getRouteLength.d.ts +3 -0
  53. package/dist/core/routing/getRouteLength.js +4 -0
  54. package/dist/core/routing/graphPointResolvers.d.ts +9 -0
  55. package/dist/core/routing/graphPointResolvers.js +13 -0
  56. package/dist/core/routing/optimizeWaypointOrder.d.ts +14 -0
  57. package/dist/core/routing/optimizeWaypointOrder.js +88 -0
  58. package/dist/core/routing/resolveWaypointCandidates.d.ts +23 -0
  59. package/dist/core/routing/resolveWaypointCandidates.js +49 -0
  60. package/dist/core/routing/routeResult.d.ts +4 -0
  61. package/dist/core/routing/routeResult.js +2 -0
  62. package/dist/core/types.d.ts +102 -0
  63. package/dist/core/types.js +1 -0
  64. package/dist/createWayfinding.d.ts +68 -0
  65. package/dist/createWayfinding.js +44 -0
  66. package/dist/index.d.ts +14 -0
  67. package/dist/index.js +13 -0
  68. package/dist/renderer/createWayfindingRenderer.d.ts +14 -0
  69. package/dist/renderer/createWayfindingRenderer.js +51 -0
  70. package/dist/renderer/iconManager.d.ts +39 -0
  71. package/dist/renderer/iconManager.js +166 -0
  72. package/dist/renderer/index.d.ts +3 -0
  73. package/dist/renderer/index.js +1 -0
  74. package/dist/renderer/layerManager.d.ts +27 -0
  75. package/dist/renderer/layerManager.js +40 -0
  76. package/dist/renderer/lineAnimation.d.ts +11 -0
  77. package/dist/renderer/lineAnimation.js +127 -0
  78. package/dist/renderer/routeLineManager.d.ts +44 -0
  79. package/dist/renderer/routeLineManager.js +62 -0
  80. package/dist/renderer/trailManager.d.ts +32 -0
  81. package/dist/renderer/trailManager.js +79 -0
  82. package/dist/renderer/types.d.ts +129 -0
  83. package/dist/renderer/types.js +1 -0
  84. package/dist/runtime/createWayfindingRuntime.d.ts +3 -0
  85. package/dist/runtime/createWayfindingRuntime.js +248 -0
  86. package/dist/runtime/endpointView.d.ts +20 -0
  87. package/dist/runtime/endpointView.js +40 -0
  88. package/dist/runtime/getRouteLines.d.ts +19 -0
  89. package/dist/runtime/getRouteLines.js +16 -0
  90. package/dist/runtime/index.d.ts +4 -0
  91. package/dist/runtime/index.js +2 -0
  92. package/dist/runtime/positionTrailView.d.ts +52 -0
  93. package/dist/runtime/positionTrailView.js +35 -0
  94. package/dist/runtime/positionView.d.ts +23 -0
  95. package/dist/runtime/positionView.js +25 -0
  96. package/dist/runtime/routeLinesView.d.ts +18 -0
  97. package/dist/runtime/routeLinesView.js +18 -0
  98. package/dist/runtime/routeRenderData.d.ts +31 -0
  99. package/dist/runtime/routeRenderData.js +46 -0
  100. package/dist/runtime/routeUpdate.d.ts +17 -0
  101. package/dist/runtime/routeUpdate.js +17 -0
  102. package/dist/runtime/snapPositionToRoute.d.ts +10 -0
  103. package/dist/runtime/snapPositionToRoute.js +36 -0
  104. package/dist/runtime/trailView.d.ts +20 -0
  105. package/dist/runtime/trailView.js +43 -0
  106. package/dist/runtime/transitionView.d.ts +17 -0
  107. package/dist/runtime/transitionView.js +50 -0
  108. package/dist/runtime/types.d.ts +86 -0
  109. package/dist/runtime/types.js +1 -0
  110. package/package.json +45 -0
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @expofp/wayfinding
2
+
3
+ Framework-neutral wayfinding for the ExpoFP SDK: a routing/snapping engine, an
4
+ imperative runtime orchestrator, and a scene renderer — with **no** coupling to
5
+ efp stores or MobX. `core` and `runtime` are environment-agnostic; the scene
6
+ renderer assumes a browser-like environment (`requestAnimationFrame`, `document`),
7
+ so the package is not suitable for Node/SSR as-is. All three layers sit behind a
8
+ single entry point, `createWayfinding`.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pnpm add @expofp/wayfinding @expofp/renderer
14
+ ```
15
+
16
+ `@expofp/renderer` is a **peer dependency** and must be installed by the consumer:
17
+ the package's public types (e.g. `RenderableDef` on `Wayfinding`/`RendererPort`)
18
+ reference it, so its types must resolve for `@expofp/wayfinding`'s `.d.ts` to compile.
19
+
20
+ ## Layers
21
+
22
+ | Layer | What it is |
23
+ | ---------- | -------------------------------------------------------------------------------------------- |
24
+ | `core` | Graph build + A\* pathfinding, route geometry, snapping, transition points. Pure functions. |
25
+ | `runtime` | Imperative orchestrator (`setRoute` / `setPosition` / `notifyFloorChanged`) over the engine. |
26
+ | `renderer` | Scene mutator (icons, trails, route lines) against an injected `RendererPort`. |
27
+
28
+ These layers are internal. The package exports only the `createWayfinding`
29
+ facade, the port interfaces the host implements, the boundary data types, the
30
+ `CURRENT_POSITION_POINT_ID` protocol sentinel, and the `optimizeWaypointOrder`
31
+ helper.
32
+
33
+ ## Ports (host implements)
34
+
35
+ The package knows nothing concrete about the host renderer, graph data, or app
36
+ state. The host supplies four ports via `WayfindingConfig`:
37
+
38
+ - **`dataSource`** (`GraphDataSource`) — graph lines + line ends for pathfinding.
39
+ - **`renderer`** (`RendererPort`) — def factories, current scale, scene layers, commit.
40
+ - **`iconProvider`** (`IconProvider`) — canvases for icon names.
41
+ - **`floorContext`** (`FloorContext`) — active floor / visibility predicates.
42
+
43
+ `@expofp/renderer` is a **type-only peer** (its types appear in the public API; the
44
+ concrete renderer is injected through `RendererPort`). `@expofp/geometry` is a regular
45
+ runtime dependency (`Rect`, `Box`, geometry helpers).
46
+
47
+ ## Usage
48
+
49
+ ```ts
50
+ import { createWayfinding } from '@expofp/wayfinding';
51
+
52
+ const wayfinding = createWayfinding({
53
+ dataSource, // GraphDataSource
54
+ renderer, // RendererPort
55
+ iconProvider, // IconProvider
56
+ floorContext, // FloorContext
57
+ layers, // LayerNames: { points, trail, lines, linesAnimated, currentPosition }
58
+ gpsConfig, // optional GPS calibration + snap/reroute thresholds
59
+ onTransitionClick: (point) => {
60
+ /* switch active floor */
61
+ },
62
+ onRouteUpdate: (lines, bounds) => {
63
+ /* fit camera, update UI */
64
+ },
65
+ onRouteDistance: (distance) => {
66
+ /* show remaining distance */
67
+ },
68
+ });
69
+
70
+ // Drive it with route/position/floor updates:
71
+ wayfinding.setRoute({ from, to, accessible: false });
72
+ wayfinding.setPosition(position); // or null to hide
73
+ wayfinding.notifyFloorChanged();
74
+
75
+ // Forward camera/pointer events:
76
+ wayfinding.applyScale(scale);
77
+ wayfinding.applyRoll(angle);
78
+ wayfinding.handleClick(pickedDefs);
79
+
80
+ wayfinding.destroy();
81
+ ```
82
+
83
+ `optimizeWaypointOrder` is exported separately to order booth waypoints before a
84
+ `createWayfinding` instance exists (it backs the SDK's `getOptimizedRoutes`).
@@ -0,0 +1,16 @@
1
+ import type { GraphDataSource, RouteEndpoint, RouteLine } from './types.js';
2
+ export interface Route {
3
+ readonly lines: RouteLine[];
4
+ readonly distance: number;
5
+ }
6
+ export interface WayfindingEngine {
7
+ buildRoute(from: RouteEndpoint, to: RouteEndpoint, options?: {
8
+ accessible?: boolean;
9
+ }): Route;
10
+ buildWaypointsRoute(waypoints: RouteEndpoint[], options?: {
11
+ accessible?: boolean;
12
+ fastest?: boolean;
13
+ }): Route;
14
+ }
15
+ export declare function createWayfindingEngine(dataSource: GraphDataSource): WayfindingEngine;
16
+ //# sourceMappingURL=createWayfindingEngine.d.ts.map
@@ -0,0 +1,87 @@
1
+ import { createGraphCache } from './graph/graphCache.js';
2
+ import { buildMultiPointRoute } from './routing/buildMultiPointRoute.js';
3
+ import { buildRoute as buildRouteInternal } from './routing/buildRoute.js';
4
+ import { getRouteLength } from './routing/getRouteLength.js';
5
+ import { reorderWaypoints } from './routing/optimizeWaypointOrder.js';
6
+ import { resolveWaypointCandidates, } from './routing/resolveWaypointCandidates.js';
7
+ const POINT_EPS = 1e-6;
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
+ /**
10
+ * Trim/extend the route's FROM tip to land exactly on `entry.projection`.
11
+ * The FROM boundary is the first line; `boundary.p0` is its graph node.
12
+ * @param lines
13
+ * @param entry
14
+ */
15
+ function attachFromEntry(lines, entry) {
16
+ if (!lines.length)
17
+ return lines;
18
+ const boundary = lines[0];
19
+ const chosenEndpoint = boundary.p0;
20
+ const otherEnd = pointEquals(entry.segment.p0, chosenEndpoint)
21
+ ? entry.segment.p1
22
+ : entry.segment.p0;
23
+ if (pointEquals(boundary.p1, otherEnd)) {
24
+ return [{ ...boundary, p0: entry.projection }, ...lines.slice(1)];
25
+ }
26
+ return [{ ...entry.segment, p0: entry.projection, p1: chosenEndpoint }, ...lines];
27
+ }
28
+ /**
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
+ * @param lines
32
+ * @param entry
33
+ */
34
+ function attachToEntry(lines, entry) {
35
+ if (!lines.length)
36
+ return lines;
37
+ const boundary = lines[lines.length - 1];
38
+ const chosenEndpoint = boundary.p1;
39
+ const otherEnd = pointEquals(entry.segment.p0, chosenEndpoint)
40
+ ? entry.segment.p1
41
+ : entry.segment.p0;
42
+ if (pointEquals(boundary.p0, otherEnd)) {
43
+ return [...lines.slice(0, -1), { ...boundary, p1: entry.projection }];
44
+ }
45
+ return [...lines, { ...entry.segment, p0: chosenEndpoint, p1: entry.projection }];
46
+ }
47
+ function attachOffGraphEntries(lines, fromEntry, toEntry) {
48
+ let result = lines;
49
+ if (fromEntry)
50
+ result = attachFromEntry(result, fromEntry);
51
+ if (toEntry)
52
+ result = attachToEntry(result, toEntry);
53
+ return result;
54
+ }
55
+ export function createWayfindingEngine(dataSource) {
56
+ const cache = createGraphCache();
57
+ return {
58
+ buildRoute(from, to, options) {
59
+ const graph = cache.getOrBuild(dataSource, options?.accessible ?? false);
60
+ const fromResolved = resolveWaypointCandidates(graph, from);
61
+ const toResolved = resolveWaypointCandidates(graph, to);
62
+ const result = buildRouteInternal(graph, fromResolved.candidates, toResolved.candidates);
63
+ const hasOffGraph = !!fromResolved.offGraphEntry || !!toResolved.offGraphEntry;
64
+ const lines = attachOffGraphEntries(result.lines, fromResolved.offGraphEntry, toResolved.offGraphEntry);
65
+ return {
66
+ lines,
67
+ distance: hasOffGraph ? getRouteLength(lines) : result.totalDistance,
68
+ };
69
+ },
70
+ buildWaypointsRoute(waypoints, options) {
71
+ if (waypoints.length < 2)
72
+ return { lines: [], distance: 0 };
73
+ const graph = cache.getOrBuild(dataSource, options?.accessible ?? false);
74
+ const ordered = options?.fastest ? reorderWaypoints(waypoints) : waypoints;
75
+ const resolved = ordered.map((wp) => resolveWaypointCandidates(graph, wp));
76
+ const result = buildMultiPointRoute(graph, resolved.map((r) => r.candidates));
77
+ const fromEntry = resolved[0].offGraphEntry;
78
+ const toEntry = resolved[resolved.length - 1].offGraphEntry;
79
+ const hasOffGraph = !!fromEntry || !!toEntry;
80
+ const lines = attachOffGraphEntries(result.lines, fromEntry, toEntry);
81
+ return {
82
+ lines,
83
+ distance: hasOffGraph ? getRouteLength(lines) : result.totalDistance,
84
+ };
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,17 @@
1
+ import { type PolygonVertex } from '../types.js';
2
+ /**
3
+ * Convex polygon area via fan triangulation from vertex[0].
4
+ * @param polygon
5
+ */
6
+ export declare function computePolygonArea(polygon: readonly PolygonVertex[]): number;
7
+ /**
8
+ * Area-based point-in-polygon test with 1% relative tolerance. Convex polygons only.
9
+ * Inside ⇒ sum of (point, vertex_i, vertex_{i+1}) triangle areas equals polygon area; outside ⇒ larger.
10
+ * Tolerance includes points sitting fractionally outside the polygon (editor/float imprecision).
11
+ * Pass `polygonArea` from {@link computePolygonArea} to skip recomputation in hot loops.
12
+ * @param point
13
+ * @param polygon
14
+ * @param polygonArea
15
+ */
16
+ export declare function pointInPolygon(point: PolygonVertex, polygon: readonly PolygonVertex[], polygonArea?: number): boolean;
17
+ //# sourceMappingURL=pointInPolygon.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Relative area tolerance (1%) for the sum-of-triangles containment test.
3
+ *
4
+ * NOTE: this is intentionally NOT migrated to `@expofp/geometry`'s
5
+ * `polygonContainsPoint`. That function uses a per-triangle barycentric test
6
+ * whose tolerance is 1% of each triangle's area, which gives different
7
+ * boundary decisions than this whole-polygon-area test (verified: hundreds of
8
+ * near-boundary points flip between the two). Booth-bounds containment feeds
9
+ * graph-candidate selection (`graphPointResolvers`) and snap behavior
10
+ * (`snapToRoute`), so swapping the algorithm could silently change routing.
11
+ * Kept as the original sg-free implementation to preserve route selection.
12
+ */
13
+ const POLYGON_AREA_TOLERANCE = 0.01;
14
+ function triangleArea(vertexA, vertexB, vertexC) {
15
+ return Math.abs(0.5 *
16
+ (vertexA.x * (vertexB.y - vertexC.y) +
17
+ vertexB.x * (vertexC.y - vertexA.y) +
18
+ vertexC.x * (vertexA.y - vertexB.y)));
19
+ }
20
+ /**
21
+ * Convex polygon area via fan triangulation from vertex[0].
22
+ * @param polygon
23
+ */
24
+ export function computePolygonArea(polygon) {
25
+ if (polygon.length < 3)
26
+ return 0;
27
+ let area = 0;
28
+ for (let i = 1; i < polygon.length - 1; i++) {
29
+ area += triangleArea(polygon[0], polygon[i], polygon[i + 1]);
30
+ }
31
+ return area;
32
+ }
33
+ /**
34
+ * Area-based point-in-polygon test with 1% relative tolerance. Convex polygons only.
35
+ * Inside ⇒ sum of (point, vertex_i, vertex_{i+1}) triangle areas equals polygon area; outside ⇒ larger.
36
+ * Tolerance includes points sitting fractionally outside the polygon (editor/float imprecision).
37
+ * Pass `polygonArea` from {@link computePolygonArea} to skip recomputation in hot loops.
38
+ * @param point
39
+ * @param polygon
40
+ * @param polygonArea
41
+ */
42
+ export function pointInPolygon(point, polygon, polygonArea = computePolygonArea(polygon)) {
43
+ if (polygonArea === 0)
44
+ return false;
45
+ let sumFromPoint = 0;
46
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
47
+ sumFromPoint += triangleArea(point, polygon[j], polygon[i]);
48
+ }
49
+ return Math.abs(polygonArea - sumFromPoint) < polygonArea * POLYGON_AREA_TOLERANCE;
50
+ }
@@ -0,0 +1,27 @@
1
+ import { type Point2Like } from '@expofp/geometry';
2
+ export interface SegmentProjection {
3
+ readonly projected: {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ };
7
+ readonly distance: number;
8
+ readonly t: number;
9
+ }
10
+ /**
11
+ * Projects `point` onto the segment `a`–`b`, clamping the parameter `t` to
12
+ * `[0, 1]` so the result always lies on the segment (not its infinite line).
13
+ *
14
+ * `t` is the normalized position along the segment: `0` = `a`, `1` = `b`.
15
+ * For a zero-length segment (`a` equals `b`) the projection collapses to `a`
16
+ * with `t = 0`.
17
+ *
18
+ * Thin wrapper over `@expofp/geometry`'s `lineProjectPoint`; the projected
19
+ * position is re-shaped to a plain `{ x, y }` to preserve the legacy
20
+ * {@link SegmentProjection} contract (callers depend on a bare point, not a
21
+ * `Point` instance).
22
+ * @param point
23
+ * @param a
24
+ * @param b
25
+ */
26
+ export declare function projectPointOnSegment(point: Point2Like, a: Point2Like, b: Point2Like): SegmentProjection;
27
+ //# sourceMappingURL=projectPointOnSegment.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { lineProjectPoint } from '@expofp/geometry';
2
+ /**
3
+ * Projects `point` onto the segment `a`–`b`, clamping the parameter `t` to
4
+ * `[0, 1]` so the result always lies on the segment (not its infinite line).
5
+ *
6
+ * `t` is the normalized position along the segment: `0` = `a`, `1` = `b`.
7
+ * For a zero-length segment (`a` equals `b`) the projection collapses to `a`
8
+ * with `t = 0`.
9
+ *
10
+ * Thin wrapper over `@expofp/geometry`'s `lineProjectPoint`; the projected
11
+ * position is re-shaped to a plain `{ x, y }` to preserve the legacy
12
+ * {@link SegmentProjection} contract (callers depend on a bare point, not a
13
+ * `Point` instance).
14
+ * @param point
15
+ * @param a
16
+ * @param b
17
+ */
18
+ export function projectPointOnSegment(point, a, b) {
19
+ const { projected, distance, t } = lineProjectPoint({ p0: a, p1: b }, point);
20
+ return { projected: { x: projected.x, y: projected.y }, distance, t };
21
+ }
@@ -0,0 +1,3 @@
1
+ import { type GraphBuildOptions, type GraphDataSource, type GraphInstance, type PathFinder } from '../types.js';
2
+ export declare function buildGraph(dataSource: GraphDataSource, pathFinder: PathFinder, options: GraphBuildOptions): GraphInstance;
3
+ //# sourceMappingURL=buildGraph.d.ts.map
@@ -0,0 +1,7 @@
1
+ export function buildGraph(dataSource, pathFinder, options) {
2
+ pathFinder.build(dataSource.getLines(), {
3
+ oriented: options.oriented ?? true,
4
+ onlyAccessible: options.onlyAccessible ?? false,
5
+ });
6
+ return { finder: pathFinder, dataSource, options };
7
+ }
@@ -0,0 +1,13 @@
1
+ import { type Graph } from 'ngraph.graph';
2
+ import { type GraphLine, type PathFinderOptions } from '../types.js';
3
+ interface LinkData {
4
+ distance: number;
5
+ }
6
+ /**
7
+ * Builds an ngraph instance from route lines, filtering and linking per options.
8
+ * @param lines
9
+ * @param options
10
+ */
11
+ export declare function buildNGraph(lines: GraphLine[], options: PathFinderOptions): Graph<unknown, LinkData>;
12
+ export {};
13
+ //# sourceMappingURL=buildNGraph.d.ts.map
@@ -0,0 +1,24 @@
1
+ import createGraph from 'ngraph.graph';
2
+ import { toNodeId } from './graphHelpers.js';
3
+ import { linkCost } from './linkCost.js';
4
+ /**
5
+ * Builds an ngraph instance from route lines, filtering and linking per options.
6
+ * @param lines
7
+ * @param options
8
+ */
9
+ export function buildNGraph(lines, options) {
10
+ const { oriented, onlyAccessible } = options;
11
+ const graph = createGraph();
12
+ for (const line of lines) {
13
+ if (onlyAccessible && line.unaccessible)
14
+ continue;
15
+ const fromId = toNodeId(line.p0);
16
+ const toId = toNodeId(line.p1);
17
+ const distance = linkCost(line);
18
+ graph.addLink(fromId, toId, { distance });
19
+ if (oriented && !line.unidirection) {
20
+ graph.addLink(toId, fromId, { distance });
21
+ }
22
+ }
23
+ return graph;
24
+ }
@@ -0,0 +1,20 @@
1
+ /**
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).
6
+ */
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;
10
+ /**
11
+ * Fallback line weight when `line.weight` is 0 (meaning "not set" in floorplan data,
12
+ * not "free passage").
13
+ */
14
+ export declare const DEFAULT_LINE_WEIGHT = 4;
15
+ /**
16
+ * Multiplier applied to a graph edge cost whenever the route changes direction at a node
17
+ * (A* turn penalty).
18
+ */
19
+ export declare const TURN_PENALTY = 1.01;
20
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
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).
6
+ */
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;
10
+ /**
11
+ * Fallback line weight when `line.weight` is 0 (meaning "not set" in floorplan data,
12
+ * not "free passage").
13
+ */
14
+ export const DEFAULT_LINE_WEIGHT = 4;
15
+ /**
16
+ * Multiplier applied to a graph edge cost whenever the route changes direction at a node
17
+ * (A* turn penalty).
18
+ */
19
+ export const TURN_PENALTY = 1.01;
@@ -0,0 +1,23 @@
1
+ import { type GraphInstance, type GraphLine, type RoutePoint, type ShortestPathResult } from '../types.js';
2
+ /**
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
+ * @param points
7
+ * @param lines
8
+ */
9
+ export declare function computeWeightedPathCost(points: RoutePoint[], lines: GraphLine[]): number;
10
+ /**
11
+ * Finds the shortest path between candidate point sets by trying all from×to
12
+ * pairs and picking the one with the lowest weighted cost.
13
+ *
14
+ * Complexity is O(N×M × A*) where N = |from|, M = |to|. In practice N, M ≤ 5
15
+ * (rect-based booth candidates) or = 1 (nearest-point fallback), so the brute
16
+ * force approach is acceptable. Multi-source A* would reduce this to a single
17
+ * traversal but adds significant implementation complexity.
18
+ * @param graph
19
+ * @param from
20
+ * @param to
21
+ */
22
+ export declare function findShortestPath(graph: GraphInstance, from: RoutePoint[], to: RoutePoint[]): ShortestPathResult | null;
23
+ //# sourceMappingURL=findShortestPath.d.ts.map
@@ -0,0 +1,65 @@
1
+ import { getRouteLength } from '../routing/getRouteLength.js';
2
+ import { buildRouteSegments, findLineByEndpoints, toNodeId } from './graphHelpers.js';
3
+ import { linkCost } from './linkCost.js';
4
+ function createRoutePoint(layer, x, y) {
5
+ return { layer, x, y };
6
+ }
7
+ /**
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
+ * @param points
12
+ * @param lines
13
+ */
14
+ export function computeWeightedPathCost(points, lines) {
15
+ let cost = 0;
16
+ for (let index = 1; index < points.length; index++) {
17
+ const line = findLineByEndpoints(lines, points[index - 1], points[index]);
18
+ if (!line)
19
+ continue;
20
+ cost += linkCost(line);
21
+ }
22
+ return cost;
23
+ }
24
+ /**
25
+ * Finds the shortest path between candidate point sets by trying all from×to
26
+ * pairs and picking the one with the lowest weighted cost.
27
+ *
28
+ * Complexity is O(N×M × A*) where N = |from|, M = |to|. In practice N, M ≤ 5
29
+ * (rect-based booth candidates) or = 1 (nearest-point fallback), so the brute
30
+ * force approach is acceptable. Multi-source A* would reduce this to a single
31
+ * traversal but adds significant implementation complexity.
32
+ * @param graph
33
+ * @param from
34
+ * @param to
35
+ */
36
+ export function findShortestPath(graph, from, to) {
37
+ const lines = graph.dataSource.getLines();
38
+ let bestResult = null;
39
+ let bestWeightedCost = Infinity;
40
+ for (const fromPt of from) {
41
+ for (const toPt of to) {
42
+ const fromId = toNodeId(fromPt);
43
+ const toId = toNodeId(toPt);
44
+ if (!graph.finder.hasNode(fromId) || !graph.finder.hasNode(toId)) {
45
+ console.debug(`WF. findShortestPath: node not in graph, skipping pair ${fromId} → ${toId}`);
46
+ continue;
47
+ }
48
+ const nodes = graph.finder.find(fromId, toId);
49
+ if (!nodes.length)
50
+ continue;
51
+ const points = nodes.map((node) => createRoutePoint(node.layer, node.x, node.y));
52
+ const routeLines = buildRouteSegments(points, lines);
53
+ if (!routeLines.length)
54
+ continue;
55
+ // Compare by weighted cost (consistent with A* graph link distance formula),
56
+ // but store physical distance in the result
57
+ const weightedCost = computeWeightedPathCost(points, lines);
58
+ if (weightedCost < bestWeightedCost) {
59
+ bestWeightedCost = weightedCost;
60
+ bestResult = { lines: routeLines, distance: getRouteLength(routeLines) };
61
+ }
62
+ }
63
+ }
64
+ return bestResult;
65
+ }
@@ -0,0 +1,7 @@
1
+ import { type GraphDataSource, type GraphInstance } from '../types.js';
2
+ interface GraphCache {
3
+ getOrBuild(dataSource: GraphDataSource, onlyAccessible: boolean): GraphInstance;
4
+ }
5
+ export declare function createGraphCache(): GraphCache;
6
+ export {};
7
+ //# sourceMappingURL=graphCache.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { buildGraph } from './buildGraph.js';
2
+ import { createAStarPathFinder } from './pathfinder/aStarPathFinder.js';
3
+ export function createGraphCache() {
4
+ let cached = null;
5
+ let cachedOnlyAccessible = false;
6
+ return {
7
+ getOrBuild(dataSource, onlyAccessible) {
8
+ // Cache is keyed only on `onlyAccessible`, not on dataSource identity, so it
9
+ // never invalidates if the floorplan data changes. Safe because each floorplan
10
+ // loads a fresh engine (hence a fresh cache) — a cached graph never outlives
11
+ // its dataSource. Add an identity check + invalidation if that ever stops holding.
12
+ if (cached && cachedOnlyAccessible === onlyAccessible)
13
+ return cached;
14
+ const pathFinder = createAStarPathFinder();
15
+ cached = buildGraph(dataSource, pathFinder, { oriented: true, onlyAccessible });
16
+ cachedOnlyAccessible = onlyAccessible;
17
+ return cached;
18
+ },
19
+ };
20
+ }
@@ -0,0 +1,8 @@
1
+ import { type GraphLine, type RouteLine, type RoutePoint } from '../types.js';
2
+ export declare const toNodeId: (p: RoutePoint) => string;
3
+ export declare const arePointsClose: (p1: RoutePoint, p2: RoutePoint) => boolean;
4
+ export declare const matchesLine: (line: GraphLine, p0: RoutePoint, p1: RoutePoint) => boolean;
5
+ export declare function findLineByEndpoints(lines: GraphLine[], p0: RoutePoint, p1: RoutePoint): GraphLine | undefined;
6
+ export declare function canMergeSegments(lastSegment: RouteLine, segment: RouteLine): boolean;
7
+ export declare function buildRouteSegments(points: RoutePoint[], lines: GraphLine[]): RouteLine[];
8
+ //# sourceMappingURL=graphHelpers.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { degToRad, lineAngle, pointDistance } from '@expofp/geometry';
2
+ export const toNodeId = (p) => `${p.layer ?? ''}_${p.x}_${p.y}`;
3
+ /** Maximum distance (SVG units) to consider two graph nodes as the same point. */
4
+ const POINT_PROXIMITY_THRESHOLD = 1;
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;
9
+ export const matchesLine = (line, p0, p1) => (arePointsClose(line.p0, p0) && arePointsClose(line.p1, p1)) ||
10
+ (arePointsClose(line.p0, p1) && arePointsClose(line.p1, p0));
11
+ export function findLineByEndpoints(lines, p0, p1) {
12
+ return lines.find((line) => matchesLine(line, p0, p1));
13
+ }
14
+ // Threshold expressed in radians (was 5° under simple-geometry's degree-based
15
+ // lineAngle; @expofp/geometry's lineAngle returns radians via atan2).
16
+ const MERGE_ANGLE_THRESHOLD = degToRad(5);
17
+ export function canMergeSegments(lastSegment, segment) {
18
+ if (lastSegment.virtual !== segment.virtual)
19
+ return false;
20
+ const lastAngle = lineAngle(lastSegment.p0, lastSegment.p1);
21
+ const angle = lineAngle(segment.p0, segment.p1);
22
+ let diff = Math.abs(angle - lastAngle);
23
+ // Wrap across the ±π discontinuity (was ±180° / 360° under degrees).
24
+ if (diff > Math.PI)
25
+ diff = 2 * Math.PI - diff;
26
+ return diff <= MERGE_ANGLE_THRESHOLD;
27
+ }
28
+ export function buildRouteSegments(points, lines) {
29
+ const result = [];
30
+ for (let i = 1; i < points.length; i++) {
31
+ const previousPoint = points[i - 1];
32
+ const currentPoint = points[i];
33
+ const line = findLineByEndpoints(lines, previousPoint, currentPoint);
34
+ if (!line) {
35
+ console.warn(`WF: buildRouteSegments — no line found between ${toNodeId(previousPoint)} and ${toNodeId(currentPoint)}`);
36
+ continue;
37
+ }
38
+ const segment = {
39
+ p0: previousPoint,
40
+ p1: currentPoint,
41
+ virtual: line.virtual,
42
+ };
43
+ const lastSegment = result[result.length - 1];
44
+ if (lastSegment && canMergeSegments(lastSegment, segment)) {
45
+ result[result.length - 1] = { ...lastSegment, p1: segment.p1 };
46
+ }
47
+ else {
48
+ result.push(segment);
49
+ }
50
+ }
51
+ return result;
52
+ }
@@ -0,0 +1,13 @@
1
+ import { type GraphLine } from '../types.js';
2
+ export declare function changesLayer(line: GraphLine): boolean;
3
+ /**
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`).
10
+ * @param line
11
+ */
12
+ export declare function linkCost(line: GraphLine): number;
13
+ //# sourceMappingURL=linkCost.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { pointDistance } from '@expofp/geometry';
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
+ }
9
+ /**
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`).
16
+ * @param line
17
+ */
18
+ export function linkCost(line) {
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;
24
+ }
@@ -0,0 +1,3 @@
1
+ import { type PathFinder } from '../../types.js';
2
+ export declare function createAStarPathFinder(): PathFinder;
3
+ //# sourceMappingURL=aStarPathFinder.d.ts.map