@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.
- package/README.md +84 -0
- package/dist/core/createWayfindingEngine.d.ts +16 -0
- package/dist/core/createWayfindingEngine.js +87 -0
- package/dist/core/geometry/pointInPolygon.d.ts +17 -0
- package/dist/core/geometry/pointInPolygon.js +50 -0
- package/dist/core/geometry/projectPointOnSegment.d.ts +27 -0
- package/dist/core/geometry/projectPointOnSegment.js +21 -0
- package/dist/core/graph/buildGraph.d.ts +3 -0
- package/dist/core/graph/buildGraph.js +7 -0
- package/dist/core/graph/buildNGraph.d.ts +13 -0
- package/dist/core/graph/buildNGraph.js +24 -0
- package/dist/core/graph/constants.d.ts +20 -0
- package/dist/core/graph/constants.js +19 -0
- package/dist/core/graph/findShortestPath.d.ts +23 -0
- package/dist/core/graph/findShortestPath.js +65 -0
- package/dist/core/graph/graphCache.d.ts +7 -0
- package/dist/core/graph/graphCache.js +20 -0
- package/dist/core/graph/graphHelpers.d.ts +8 -0
- package/dist/core/graph/graphHelpers.js +52 -0
- package/dist/core/graph/linkCost.d.ts +13 -0
- package/dist/core/graph/linkCost.js +24 -0
- package/dist/core/graph/pathfinder/aStarPathFinder.d.ts +3 -0
- package/dist/core/graph/pathfinder/aStarPathFinder.js +64 -0
- package/dist/core/graph/pathfinder/parseNodeId.d.ts +10 -0
- package/dist/core/graph/pathfinder/parseNodeId.js +20 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +12 -0
- package/dist/core/position/distanceToRoute.d.ts +3 -0
- package/dist/core/position/distanceToRoute.js +29 -0
- package/dist/core/position/gpsThreshold.d.ts +32 -0
- package/dist/core/position/gpsThreshold.js +48 -0
- package/dist/core/position/rerouteController.d.ts +13 -0
- package/dist/core/position/rerouteController.js +22 -0
- package/dist/core/position/snapToRoute.d.ts +9 -0
- package/dist/core/position/snapToRoute.js +50 -0
- package/dist/core/position/splitRouteByPoint.d.ts +6 -0
- package/dist/core/position/splitRouteByPoint.js +56 -0
- package/dist/core/rendering/computeTrailPoints.d.ts +12 -0
- package/dist/core/rendering/computeTrailPoints.js +35 -0
- package/dist/core/rendering/computeTransitionPoints.d.ts +32 -0
- package/dist/core/rendering/computeTransitionPoints.js +114 -0
- package/dist/core/rendering/getVisibleRouteLines.d.ts +10 -0
- package/dist/core/rendering/getVisibleRouteLines.js +18 -0
- package/dist/core/rendering/normalizeRouteDirection.d.ts +19 -0
- package/dist/core/rendering/normalizeRouteDirection.js +82 -0
- package/dist/core/rendering/routeGeometry.d.ts +9 -0
- package/dist/core/rendering/routeGeometry.js +10 -0
- package/dist/core/routing/buildMultiPointRoute.d.ts +13 -0
- package/dist/core/routing/buildMultiPointRoute.js +36 -0
- package/dist/core/routing/buildRoute.d.ts +12 -0
- package/dist/core/routing/buildRoute.js +19 -0
- package/dist/core/routing/getRouteLength.d.ts +3 -0
- package/dist/core/routing/getRouteLength.js +4 -0
- package/dist/core/routing/graphPointResolvers.d.ts +9 -0
- package/dist/core/routing/graphPointResolvers.js +13 -0
- package/dist/core/routing/optimizeWaypointOrder.d.ts +14 -0
- package/dist/core/routing/optimizeWaypointOrder.js +88 -0
- package/dist/core/routing/resolveWaypointCandidates.d.ts +23 -0
- package/dist/core/routing/resolveWaypointCandidates.js +49 -0
- package/dist/core/routing/routeResult.d.ts +4 -0
- package/dist/core/routing/routeResult.js +2 -0
- package/dist/core/types.d.ts +102 -0
- package/dist/core/types.js +1 -0
- package/dist/createWayfinding.d.ts +68 -0
- package/dist/createWayfinding.js +44 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/renderer/createWayfindingRenderer.d.ts +14 -0
- package/dist/renderer/createWayfindingRenderer.js +51 -0
- package/dist/renderer/iconManager.d.ts +39 -0
- package/dist/renderer/iconManager.js +166 -0
- package/dist/renderer/index.d.ts +3 -0
- package/dist/renderer/index.js +1 -0
- package/dist/renderer/layerManager.d.ts +27 -0
- package/dist/renderer/layerManager.js +40 -0
- package/dist/renderer/lineAnimation.d.ts +11 -0
- package/dist/renderer/lineAnimation.js +127 -0
- package/dist/renderer/routeLineManager.d.ts +44 -0
- package/dist/renderer/routeLineManager.js +62 -0
- package/dist/renderer/trailManager.d.ts +32 -0
- package/dist/renderer/trailManager.js +79 -0
- package/dist/renderer/types.d.ts +129 -0
- package/dist/renderer/types.js +1 -0
- package/dist/runtime/createWayfindingRuntime.d.ts +3 -0
- package/dist/runtime/createWayfindingRuntime.js +248 -0
- package/dist/runtime/endpointView.d.ts +20 -0
- package/dist/runtime/endpointView.js +40 -0
- package/dist/runtime/getRouteLines.d.ts +19 -0
- package/dist/runtime/getRouteLines.js +16 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/positionTrailView.d.ts +52 -0
- package/dist/runtime/positionTrailView.js +35 -0
- package/dist/runtime/positionView.d.ts +23 -0
- package/dist/runtime/positionView.js +25 -0
- package/dist/runtime/routeLinesView.d.ts +18 -0
- package/dist/runtime/routeLinesView.js +18 -0
- package/dist/runtime/routeRenderData.d.ts +31 -0
- package/dist/runtime/routeRenderData.js +46 -0
- package/dist/runtime/routeUpdate.d.ts +17 -0
- package/dist/runtime/routeUpdate.js +17 -0
- package/dist/runtime/snapPositionToRoute.d.ts +10 -0
- package/dist/runtime/snapPositionToRoute.js +36 -0
- package/dist/runtime/trailView.d.ts +20 -0
- package/dist/runtime/trailView.js +43 -0
- package/dist/runtime/transitionView.d.ts +17 -0
- package/dist/runtime/transitionView.js +50 -0
- package/dist/runtime/types.d.ts +86 -0
- package/dist/runtime/types.js +1 -0
- 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,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
|
+
}
|