@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/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@expofp/wayfinding` — framework-neutral wayfinding: routing/snapping engine,
3
+ * an imperative runtime, and a scene renderer behind a single entry point.
4
+ */
5
+ export { createWayfinding } from './createWayfinding.js';
6
+ // Protocol sentinel: the host must stamp it as the `id` of a "current position"
7
+ // route endpoint so the runtime renders that point as the live position.
8
+ export { CURRENT_POSITION_POINT_ID } from './runtime/index.js';
9
+ // Pure routing helper: orders waypoints for the shortest visiting sequence
10
+ // (nearest-neighbour + 2-opt). Stateless and instance-free, so callers can plan
11
+ // multi-stop routes before a wayfinding instance exists (e.g. the SDK
12
+ // `getOptimizedRoutes`).
13
+ export { optimizeWaypointOrder } from './core/routing/optimizeWaypointOrder.js';
@@ -0,0 +1,14 @@
1
+ import type { RendererPort, WayfindingRenderer, WayfindingRendererConfig } from './types.js';
2
+ /**
3
+ * Create a {@link WayfindingRenderer} — the composition root that wires
4
+ * together {@link LayerManager}, {@link IconManager}, {@link TrailManager},
5
+ * and {@link RouteLineManager}.
6
+ *
7
+ * Exposes `applyScale`/`applyRoll`/`handleClick`/`handleHover` for the app glue
8
+ * to drive from engine events.
9
+ * @param rendererPort
10
+ * @param config
11
+ * @throws If `rendererPort.getLayers()` throws (e.g. the scene is not started).
12
+ */
13
+ export declare function createWayfindingRenderer(rendererPort: RendererPort, config?: WayfindingRendererConfig): WayfindingRenderer;
14
+ //# sourceMappingURL=createWayfindingRenderer.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { createIconManager } from './iconManager.js';
2
+ import { createLayerManager } from './layerManager.js';
3
+ import { createRouteLineManager } from './routeLineManager.js';
4
+ import { createTrailManager } from './trailManager.js';
5
+ const DEFAULT_COLORS = {
6
+ passed: '#B5B7BC',
7
+ remaining: '#A4CCE2',
8
+ active: '#0794EA',
9
+ };
10
+ /**
11
+ * Create a {@link WayfindingRenderer} — the composition root that wires
12
+ * together {@link LayerManager}, {@link IconManager}, {@link TrailManager},
13
+ * and {@link RouteLineManager}.
14
+ *
15
+ * Exposes `applyScale`/`applyRoll`/`handleClick`/`handleHover` for the app glue
16
+ * to drive from engine events.
17
+ * @param rendererPort
18
+ * @param config
19
+ * @throws If `rendererPort.getLayers()` throws (e.g. the scene is not started).
20
+ */
21
+ export function createWayfindingRenderer(rendererPort, config = {}) {
22
+ const colors = { ...DEFAULT_COLORS, ...config.colors };
23
+ const layerManager = createLayerManager(rendererPort);
24
+ const iconManager = createIconManager(layerManager, rendererPort);
25
+ const trailManager = createTrailManager(layerManager, rendererPort);
26
+ const routeLineManager = createRouteLineManager(layerManager, colors, rendererPort);
27
+ const wfRenderer = {
28
+ setIcon: (...args) => iconManager.setIcon(...args),
29
+ clearIcons: (...args) => iconManager.clearIcons(...args),
30
+ setTrail: (...args) => trailManager.setTrail(...args),
31
+ setRouteLines: (...args) => routeLineManager.setRouteLines(...args),
32
+ applyScale: (scale) => [...iconManager.applyScale(scale), ...trailManager.applyScale(scale)],
33
+ applyRoll: (cameraAngle) => iconManager.applyRoll(cameraAngle),
34
+ handleClick: (defs) => iconManager.handleClick(defs),
35
+ handleHover: (defs) => iconManager.handleHover(defs),
36
+ clearRoute() {
37
+ routeLineManager.clearLines();
38
+ trailManager.destroy();
39
+ iconManager.hideAll();
40
+ },
41
+ flush: () => layerManager.flush(),
42
+ destroy() {
43
+ wfRenderer.clearRoute();
44
+ iconManager.destroy();
45
+ trailManager.destroy();
46
+ routeLineManager.destroy();
47
+ layerManager.destroy();
48
+ },
49
+ };
50
+ return wfRenderer;
51
+ }
@@ -0,0 +1,39 @@
1
+ import type { RenderableDef } from '@expofp/renderer';
2
+ import type { LayerManager } from './layerManager.js';
3
+ import type { IconConfig, RendererPort } from './types.js';
4
+ /**
5
+ * Manages `ImageDef` icons in the scene graph.
6
+ *
7
+ * Icons are keyed by `(name, key?)`. First `setIcon` creates the `ImageDef`;
8
+ * subsequent calls mutate in place. Pass `config: null` to hide an icon
9
+ * without destroying it.
10
+ *
11
+ * Exposes pure `applyScale`/`applyRoll`/`handleClick`/`handleHover` that the
12
+ * app glue drives from engine events.
13
+ */
14
+ export interface IconManager {
15
+ /** Place, update, or hide an icon. See {@link WayfindingRenderer.setIcon}. */
16
+ setIcon(name: string, config: IconConfig | null, key?: string | number): void;
17
+ /** Hide all icons matching the given name(s). */
18
+ clearIcons(names: string | readonly string[]): void;
19
+ /** Hide every managed icon (used by `clearRoute`). */
20
+ hideAll(): void;
21
+ /** Clear internal state. */
22
+ destroy(): void;
23
+ /** Recompute bounds of all visible icons for `scale`; returns changed defs. */
24
+ applyScale(scale: number): RenderableDef[];
25
+ /** Apply camera roll to cardinal-snap icons; returns changed defs. */
26
+ applyRoll(cameraAngle: number): RenderableDef[];
27
+ /** Fire the first hit clickable icon's `onClick` from already-picked defs. */
28
+ handleClick(defs: readonly RenderableDef[]): void;
29
+ /** Whether a clickable icon is under the already-picked defs. */
30
+ handleHover(defs: readonly RenderableDef[]): boolean;
31
+ }
32
+ /**
33
+ * Create an {@link IconManager} that places `ImageDef` icons into layers
34
+ * managed by the given {@link LayerManager}.
35
+ * @param layerManager
36
+ * @param rendererPort
37
+ */
38
+ export declare function createIconManager(layerManager: LayerManager, rendererPort: RendererPort): IconManager;
39
+ //# sourceMappingURL=iconManager.d.ts.map
@@ -0,0 +1,166 @@
1
+ import { Rect } from '@expofp/geometry';
2
+ /**
3
+ * Create an {@link IconManager} that places `ImageDef` icons into layers
4
+ * managed by the given {@link LayerManager}.
5
+ * @param layerManager
6
+ * @param rendererPort
7
+ */
8
+ export function createIconManager(layerManager, rendererPort) {
9
+ const icons = new Map();
10
+ const iconKeysByName = new Map();
11
+ const keyToName = new Map();
12
+ function composeKey(name, key) {
13
+ return key === undefined ? name : `${name}#${key}`;
14
+ }
15
+ function trackKey(name, iconKey) {
16
+ let nameKeys = iconKeysByName.get(name);
17
+ if (!nameKeys) {
18
+ nameKeys = new Set();
19
+ iconKeysByName.set(name, nameKeys);
20
+ }
21
+ nameKeys.add(iconKey);
22
+ keyToName.set(iconKey, name);
23
+ }
24
+ function untrackKey(iconKey) {
25
+ const name = keyToName.get(iconKey);
26
+ if (!name)
27
+ return;
28
+ const nameKeys = iconKeysByName.get(name);
29
+ if (nameKeys)
30
+ nameKeys.delete(iconKey);
31
+ keyToName.delete(iconKey);
32
+ }
33
+ function createIcon(iconKey, name, cfg) {
34
+ const layer = layerManager.touchLayer(cfg.layer);
35
+ const scale = rendererPort.getScale();
36
+ const def = rendererPort.createImageDef(cfg.canvas, new Rect({ x: cfg.x, y: cfg.y }, { x: cfg.canvas.width * scale, y: cfg.canvas.height * scale }, cfg.rotation), { hidden: cfg.hidden ?? false, dim: cfg.dimmed ?? false, origin: cfg.origin });
37
+ layer.children.push(def);
38
+ const record = {
39
+ imageDef: def,
40
+ layerName: cfg.layer,
41
+ cardinalSnap: cfg.cardinalSnap ?? false,
42
+ center: { x: cfg.x, y: cfg.y },
43
+ rotation: cfg.rotation,
44
+ callback: cfg.onClick ?? null,
45
+ };
46
+ icons.set(iconKey, record);
47
+ trackKey(name, iconKey);
48
+ return record;
49
+ }
50
+ function recreateIcon(iconKey, name, record, cfg) {
51
+ const oldLayer = layerManager.resolveLayer(record.layerName);
52
+ const oldChildren = oldLayer.children;
53
+ const idx = oldChildren.indexOf(record.imageDef);
54
+ if (idx >= 0)
55
+ oldChildren.splice(idx, 1);
56
+ layerManager.touchedLayers.set(record.layerName, oldLayer);
57
+ icons.delete(iconKey);
58
+ untrackKey(iconKey);
59
+ createIcon(iconKey, name, cfg);
60
+ }
61
+ function updateIcon(iconKey, name, record, cfg) {
62
+ if (record.layerName !== cfg.layer) {
63
+ throw new Error(`renderer: icon "${iconKey}" cannot move between layers (was "${record.layerName}", got "${cfg.layer}")`);
64
+ }
65
+ if (record.imageDef.source !== cfg.canvas) {
66
+ recreateIcon(iconKey, name, record, cfg);
67
+ return;
68
+ }
69
+ const scale = rendererPort.getScale();
70
+ record.imageDef.bounds = rendererPort.rectToRenderer(new Rect({ x: cfg.x, y: cfg.y }, { x: cfg.canvas.width * scale, y: cfg.canvas.height * scale }, cfg.rotation));
71
+ record.imageDef.hidden = cfg.hidden ?? false;
72
+ record.imageDef.dim = cfg.dimmed ?? false;
73
+ record.center = { x: cfg.x, y: cfg.y };
74
+ record.rotation = cfg.rotation;
75
+ record.callback = cfg.onClick ?? null;
76
+ layerManager.touchLayer(cfg.layer);
77
+ }
78
+ return {
79
+ setIcon(name, cfg, key) {
80
+ const iconKey = composeKey(name, key);
81
+ if (cfg === null) {
82
+ const record = icons.get(iconKey);
83
+ if (record) {
84
+ record.imageDef.hidden = true;
85
+ record.callback = null;
86
+ layerManager.touchLayer(record.layerName);
87
+ }
88
+ return;
89
+ }
90
+ const existing = icons.get(iconKey);
91
+ if (existing)
92
+ updateIcon(iconKey, name, existing, cfg);
93
+ else
94
+ createIcon(iconKey, name, cfg);
95
+ },
96
+ clearIcons(names) {
97
+ const list = typeof names === 'string' ? [names] : names;
98
+ for (const name of list) {
99
+ const nameKeys = iconKeysByName.get(name);
100
+ if (!nameKeys)
101
+ continue;
102
+ for (const iconKey of nameKeys) {
103
+ const record = icons.get(iconKey);
104
+ if (record) {
105
+ record.imageDef.hidden = true;
106
+ record.callback = null;
107
+ layerManager.touchLayer(record.layerName);
108
+ }
109
+ }
110
+ }
111
+ },
112
+ hideAll() {
113
+ for (const record of icons.values()) {
114
+ record.imageDef.hidden = true;
115
+ record.callback = null;
116
+ layerManager.touchedLayers.set(record.layerName, layerManager.resolveLayer(record.layerName));
117
+ }
118
+ },
119
+ destroy() {
120
+ icons.clear();
121
+ iconKeysByName.clear();
122
+ keyToName.clear();
123
+ },
124
+ applyScale(scale) {
125
+ const defs = [];
126
+ for (const record of icons.values()) {
127
+ if (record.imageDef.hidden)
128
+ continue;
129
+ record.imageDef.bounds = rendererPort.rectToRenderer(new Rect(record.center, { x: record.imageDef.source.width * scale, y: record.imageDef.source.height * scale }, record.rotation));
130
+ defs.push(record.imageDef);
131
+ }
132
+ return defs;
133
+ },
134
+ applyRoll(cameraAngle) {
135
+ const defs = [];
136
+ for (const record of icons.values()) {
137
+ if (!record.cardinalSnap)
138
+ continue;
139
+ const currentRotation = record.rotation ?? 0;
140
+ const delta = rendererPort.getRotation(cameraAngle, currentRotation);
141
+ if (delta === undefined)
142
+ continue;
143
+ record.rotation = currentRotation + delta;
144
+ record.imageDef.bounds.rotation = record.rotation;
145
+ defs.push(record.imageDef);
146
+ }
147
+ return defs;
148
+ },
149
+ handleClick(defs) {
150
+ for (const record of icons.values()) {
151
+ if (record.callback && !record.imageDef.hidden && defs.includes(record.imageDef)) {
152
+ record.callback();
153
+ return;
154
+ }
155
+ }
156
+ },
157
+ handleHover(defs) {
158
+ for (const record of icons.values()) {
159
+ if (record.callback && !record.imageDef.hidden && defs.includes(record.imageDef)) {
160
+ return true;
161
+ }
162
+ }
163
+ return false;
164
+ },
165
+ };
166
+ }
@@ -0,0 +1,3 @@
1
+ export { createWayfindingRenderer } from './createWayfindingRenderer.js';
2
+ export type { IconConfig, RendererPort, WayfindingRenderer } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export { createWayfindingRenderer } from './createWayfindingRenderer.js';
@@ -0,0 +1,27 @@
1
+ import type { LayerDef } from '@expofp/renderer';
2
+ import type { RendererPort } from './types.js';
3
+ /**
4
+ * Manages scene layer resolution, caching, dirty-tracking, and flush.
5
+ *
6
+ * Layers are resolved by name from `rendererPort.getLayers()` and cached. `touchLayer`
7
+ * marks a layer as dirty; `flush` commits all dirty layers via `rendererPort.update`
8
+ * in a single call.
9
+ */
10
+ export interface LayerManager {
11
+ /** Layers modified since the last {@link flush}. */
12
+ readonly touchedLayers: Map<string, LayerDef>;
13
+ /** Resolve a layer by name (cached). Throws if not found. */
14
+ resolveLayer(name: string): LayerDef;
15
+ /** Resolve a layer and mark it as dirty for the next {@link flush}. */
16
+ touchLayer(name: string): LayerDef;
17
+ /** Commit all dirty layers to `RendererService` and clear the dirty set. */
18
+ flush(): void;
19
+ /** Clear caches and dirty set. */
20
+ destroy(): void;
21
+ }
22
+ /**
23
+ * Create a {@link LayerManager} bound to the given {@link RendererPort}.
24
+ * @param rendererPort
25
+ */
26
+ export declare function createLayerManager(rendererPort: RendererPort): LayerManager;
27
+ //# sourceMappingURL=layerManager.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Create a {@link LayerManager} bound to the given {@link RendererPort}.
3
+ * @param rendererPort
4
+ */
5
+ export function createLayerManager(rendererPort) {
6
+ const allLayers = rendererPort.getLayers();
7
+ const layerCache = new Map();
8
+ const touchedLayers = new Map();
9
+ return {
10
+ touchedLayers,
11
+ resolveLayer(name) {
12
+ const cached = layerCache.get(name);
13
+ if (cached)
14
+ return cached;
15
+ const layer = allLayers.find((candidate) => candidate.name === name);
16
+ if (!layer)
17
+ throw new Error(`renderer: layer "${name}" not found in scene`);
18
+ layerCache.set(name, layer);
19
+ return layer;
20
+ },
21
+ touchLayer(name) {
22
+ const layer = this.resolveLayer(name);
23
+ touchedLayers.set(name, layer);
24
+ return layer;
25
+ },
26
+ flush() {
27
+ if (touchedLayers.size === 0)
28
+ return;
29
+ for (const layer of touchedLayers.values()) {
30
+ layer.children = [...layer.children];
31
+ }
32
+ rendererPort.update(...touchedLayers.values());
33
+ touchedLayers.clear();
34
+ },
35
+ destroy() {
36
+ touchedLayers.clear();
37
+ layerCache.clear();
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,11 @@
1
+ import type { Point2Like } from '@expofp/geometry';
2
+ import type { LineDef } from '@expofp/renderer';
3
+ export interface AnimationHandle {
4
+ stop: () => void;
5
+ getProgress: () => number;
6
+ }
7
+ export type AnimatedLineDef = Omit<LineDef, 'points'> & {
8
+ points: [Point2Like, Point2Like];
9
+ };
10
+ export declare function animateLineSegments(lineSegments: AnimatedLineDef[], callback: (segment: AnimatedLineDef, segmentIndex: number, reset: boolean) => void, getScale: () => number, startProgress?: number): AnimationHandle | undefined;
11
+ //# sourceMappingURL=lineAnimation.d.ts.map
@@ -0,0 +1,127 @@
1
+ export function animateLineSegments(lineSegments, callback, getScale, startProgress = 0) {
2
+ let currentSegmentIndex = 0;
3
+ let segmentProgress = 0;
4
+ let lastTimestamp = null;
5
+ let animationFrameId = null;
6
+ if (!lineSegments.length)
7
+ return;
8
+ const segmentLengths = lineSegments.map((s) => {
9
+ const dx = s.points[1].x - s.points[0].x;
10
+ const dy = s.points[1].y - s.points[0].y;
11
+ return Math.sqrt(dx * dx + dy * dy);
12
+ });
13
+ const totalLength = segmentLengths.reduce((a, b) => a + b, 0);
14
+ // A zero-length route would spin the per-frame while-loop forever.
15
+ if (!totalLength)
16
+ return;
17
+ const baseSpeed = Math.max(20, Math.min(totalLength / 2, 300));
18
+ function initFromProgress(progress) {
19
+ const targetDistance = (progress % 1) * totalLength;
20
+ let accumulated = 0;
21
+ currentSegmentIndex = 0;
22
+ segmentProgress = 0;
23
+ for (let i = 0; i < segmentLengths.length; i++) {
24
+ if (accumulated + segmentLengths[i] > targetDistance) {
25
+ currentSegmentIndex = i;
26
+ segmentProgress = targetDistance - accumulated;
27
+ break;
28
+ }
29
+ accumulated += segmentLengths[i];
30
+ if (i === segmentLengths.length - 1) {
31
+ currentSegmentIndex = 0;
32
+ segmentProgress = 0;
33
+ }
34
+ }
35
+ }
36
+ function updateSegment(segment, progress) {
37
+ const start = segment.points[0];
38
+ const end = segment.points[1];
39
+ const dx = end.x - start.x;
40
+ const dy = end.y - start.y;
41
+ const length = Math.sqrt(dx * dx + dy * dy);
42
+ const t = Math.min(progress / length, 1);
43
+ return {
44
+ points: [
45
+ { x: start.x, y: start.y },
46
+ { x: start.x + dx * t, y: start.y + dy * t },
47
+ ],
48
+ color: segment.color,
49
+ width: segment.width,
50
+ };
51
+ }
52
+ function animate(timestamp) {
53
+ lastTimestamp ??= timestamp;
54
+ const delta = (timestamp - lastTimestamp) / 1000;
55
+ lastTimestamp = timestamp;
56
+ const scale = getScale();
57
+ // A detached canvas (host element removed from the DOM on SPA navigation)
58
+ // reports a bogus ptScale (Infinity on a zero-size canvas). Anything but a
59
+ // positive finite scale poisons segmentProgress and spins the loop below,
60
+ // freezing the tab — skip the frame until a usable scale returns.
61
+ if (!(Number.isFinite(scale) && scale > 0)) {
62
+ animationFrameId = requestAnimationFrame(animate);
63
+ return;
64
+ }
65
+ const adjustedSpeed = baseSpeed * Math.sqrt(scale);
66
+ segmentProgress += adjustedSpeed * delta;
67
+ // Keep progress within a single lap. totalLength > 0 is guaranteed above, so
68
+ // the loop can advance at most lineSegments.length steps — it can never spin,
69
+ // whatever scale/delta the engine feeds in.
70
+ segmentProgress %= totalLength;
71
+ while (segmentProgress >= segmentLengths[currentSegmentIndex]) {
72
+ segmentProgress -= segmentLengths[currentSegmentIndex];
73
+ currentSegmentIndex++;
74
+ if (currentSegmentIndex >= lineSegments.length) {
75
+ currentSegmentIndex = 0;
76
+ }
77
+ }
78
+ const currentSegment = lineSegments[currentSegmentIndex];
79
+ const updatedSegment = updateSegment(currentSegment, segmentProgress);
80
+ callback(updatedSegment, currentSegmentIndex, false);
81
+ animationFrameId = requestAnimationFrame(animate);
82
+ }
83
+ function start() {
84
+ stop();
85
+ if (startProgress <= 0) {
86
+ currentSegmentIndex = 0;
87
+ segmentProgress = 0;
88
+ }
89
+ lastTimestamp = null;
90
+ animationFrameId = requestAnimationFrame(animate);
91
+ }
92
+ function stop() {
93
+ if (animationFrameId) {
94
+ cancelAnimationFrame(animationFrameId);
95
+ animationFrameId = null;
96
+ lastTimestamp = null;
97
+ }
98
+ }
99
+ function getProgress() {
100
+ if (totalLength <= 0)
101
+ return 0;
102
+ let accumulated = 0;
103
+ for (let i = 0; i < currentSegmentIndex; i++) {
104
+ accumulated += segmentLengths[i];
105
+ }
106
+ accumulated += segmentProgress;
107
+ return accumulated / totalLength;
108
+ }
109
+ function handleVisibilityChange() {
110
+ if (document.visibilityState === 'visible')
111
+ start();
112
+ else
113
+ stop();
114
+ }
115
+ if (startProgress > 0) {
116
+ initFromProgress(startProgress);
117
+ }
118
+ document.addEventListener('visibilitychange', handleVisibilityChange);
119
+ start();
120
+ return {
121
+ stop: () => {
122
+ stop();
123
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
124
+ },
125
+ getProgress,
126
+ };
127
+ }
@@ -0,0 +1,44 @@
1
+ import type { LayerManager } from './layerManager.js';
2
+ import type { RendererPort, RouteColors } from './types.js';
3
+ /**
4
+ * Manages static route lines (passed + remaining) and an animated overlay
5
+ * on the remaining segment.
6
+ */
7
+ export interface RouteLineManager {
8
+ setRouteLines(passed: readonly {
9
+ readonly p0: {
10
+ x: number;
11
+ y: number;
12
+ };
13
+ readonly p1: {
14
+ x: number;
15
+ y: number;
16
+ };
17
+ }[], remaining: readonly {
18
+ readonly p0: {
19
+ x: number;
20
+ y: number;
21
+ };
22
+ readonly p1: {
23
+ x: number;
24
+ y: number;
25
+ };
26
+ }[], options: {
27
+ readonly linesLayer: string;
28
+ readonly animatedLinesLayer: string;
29
+ readonly resetAnimation?: boolean;
30
+ }): void;
31
+ /** Stop animation, wipe both lines layers. */
32
+ clearLines(): void;
33
+ /** Alias for {@link clearLines}. */
34
+ destroy(): void;
35
+ }
36
+ /**
37
+ * Create a {@link RouteLineManager} that renders `LineDef` segments
38
+ * into layers managed by the given {@link LayerManager}.
39
+ * @param layerManager
40
+ * @param colors
41
+ * @param rendererPort
42
+ */
43
+ export declare function createRouteLineManager(layerManager: LayerManager, colors: RouteColors, rendererPort: RendererPort): RouteLineManager;
44
+ //# sourceMappingURL=routeLineManager.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { animateLineSegments, } from './lineAnimation.js';
2
+ /**
3
+ * Create a {@link RouteLineManager} that renders `LineDef` segments
4
+ * into layers managed by the given {@link LayerManager}.
5
+ * @param layerManager
6
+ * @param colors
7
+ * @param rendererPort
8
+ */
9
+ export function createRouteLineManager(layerManager, colors, rendererPort) {
10
+ let animationHandle = null;
11
+ let lastLinesLayer = null;
12
+ let lastAnimatedLayer = null;
13
+ return {
14
+ setRouteLines(passed, remaining, options) {
15
+ const linesLayer = layerManager.touchLayer(options.linesLayer);
16
+ const animatedLayer = layerManager.touchLayer(options.animatedLinesLayer);
17
+ lastLinesLayer = linesLayer;
18
+ lastAnimatedLayer = animatedLayer;
19
+ const reset = options.resetAnimation ?? true;
20
+ let resumeFrom = 0;
21
+ if (animationHandle) {
22
+ if (!reset)
23
+ resumeFrom = animationHandle.getProgress();
24
+ animationHandle.stop();
25
+ animationHandle = null;
26
+ }
27
+ const passedDefs = passed.map((seg) => rendererPort.createLineDef(seg, colors.passed));
28
+ const remainingDefs = remaining.map((seg) => rendererPort.createLineDef(seg, colors.remaining));
29
+ linesLayer.children = [...passedDefs, ...remainingDefs];
30
+ if (!remainingDefs.length) {
31
+ animatedLayer.children = [];
32
+ return;
33
+ }
34
+ const animatedDefs = remainingDefs.map((def) => ({
35
+ ...def,
36
+ color: colors.active,
37
+ }));
38
+ animationHandle =
39
+ animateLineSegments(animatedDefs, (line, idx, justReset) => {
40
+ animatedLayer.children = justReset ? [] : [...animatedDefs.slice(0, idx), line];
41
+ rendererPort.update(animatedLayer);
42
+ }, () => rendererPort.getScale(), resumeFrom) ?? null;
43
+ },
44
+ clearLines() {
45
+ if (animationHandle) {
46
+ animationHandle.stop();
47
+ animationHandle = null;
48
+ }
49
+ if (lastLinesLayer) {
50
+ lastLinesLayer.children = [];
51
+ layerManager.touchedLayers.set(lastLinesLayer.name, lastLinesLayer);
52
+ }
53
+ if (lastAnimatedLayer) {
54
+ lastAnimatedLayer.children = [];
55
+ layerManager.touchedLayers.set(lastAnimatedLayer.name, lastAnimatedLayer);
56
+ }
57
+ },
58
+ destroy() {
59
+ this.clearLines();
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,32 @@
1
+ import { type Point2Like } from '@expofp/geometry';
2
+ import type { RenderableDef } from '@expofp/renderer';
3
+ import type { LayerManager } from './layerManager.js';
4
+ import type { RendererPort } from './types.js';
5
+ /**
6
+ * Manages named trails of evenly-spaced dots between two points.
7
+ *
8
+ * On camera zoom the app glue calls `applyScale` so dots are recalculated and
9
+ * visual spacing stays constant.
10
+ */
11
+ export interface TrailManager {
12
+ /**
13
+ * Place, update, or remove a named trail.
14
+ * Pass `from/to = null` to remove the trail.
15
+ */
16
+ setTrail(name: string, from: Point2Like | null, to: Point2Like | null, options?: {
17
+ readonly canvas: HTMLCanvasElement;
18
+ readonly layer: string;
19
+ }): void;
20
+ /** Redraw all trails for `scale`; returns changed layer defs. */
21
+ applyScale(scale: number): RenderableDef[];
22
+ /** Remove all trails. */
23
+ destroy(): void;
24
+ }
25
+ /**
26
+ * Create a {@link TrailManager} that renders dot trails into layers
27
+ * managed by the given {@link LayerManager}.
28
+ * @param layerManager
29
+ * @param rendererPort
30
+ */
31
+ export declare function createTrailManager(layerManager: LayerManager, rendererPort: RendererPort): TrailManager;
32
+ //# sourceMappingURL=trailManager.d.ts.map