@expofp/wayfinding 3.26.0 → 3.27.0

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.
@@ -50,8 +50,8 @@ export interface Wayfinding {
50
50
  applyScale(scale: number): void;
51
51
  /** Forward a camera-roll change; re-orients cardinal-snap icons and commits. */
52
52
  applyRoll(angle: number): void;
53
- /** Forward a pointer click (already picked scene defs); fires icon `onClick`. */
54
- handleClick(defs: readonly RenderableDef[]): void;
53
+ /** Forward a pointer click (already picked scene defs); fires icon `onClick`, and reports whether one claimed it. */
54
+ handleClick(defs: readonly RenderableDef[]): boolean;
55
55
  /** Forward a pointer move (already picked scene defs); `true` over a clickable icon. */
56
56
  handleHover(defs: readonly RenderableDef[]): boolean;
57
57
  /**
@@ -35,8 +35,17 @@ export function createWayfinding(config) {
35
35
  shouldReroute: (position) => runtime.shouldReroute(position),
36
36
  applyScale: (scale) => renderDefs(renderer.applyScale(scale)),
37
37
  applyRoll: (angle) => renderDefs(renderer.applyRoll(angle)),
38
- handleClick: (defs) => renderer.handleClick(defs),
39
- handleHover: (defs) => renderer.handleHover(defs),
38
+ // The renderer answers per def (it holds a def → icon map, so a pointer event
39
+ // costs a lookup rather than a scan); the picked list is walked here, stopping
40
+ // at the first icon that claims the click.
41
+ handleClick: (defs) => {
42
+ for (const def of defs) {
43
+ if (renderer.handleClick(def))
44
+ return true;
45
+ }
46
+ return false;
47
+ },
48
+ handleHover: (defs) => defs.some((def) => renderer.handleHover(def)),
40
49
  setIcon: (name, config, key) => renderer.setIcon(name, config, key),
41
50
  flush: () => renderer.flush(),
42
51
  destroy: () => runtime.destroy(),
@@ -4,11 +4,12 @@ import type { RendererPort, WayfindingRenderer, WayfindingRendererConfig } from
4
4
  * together {@link LayerManager}, {@link IconManager}, {@link TrailManager},
5
5
  * and {@link RouteLineManager}.
6
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).
7
+ * Exposes `handleClick`/`handleHover` for the app glue to drive from pointer events, and
8
+ * `applyScale`/`applyRoll` for the deprecated imperative path, which has no handler dispatch of its
9
+ * own (see {@link WayfindingRenderer.applyScale}).
10
+ * @param port - The scene layers, def factories and commit function.
11
+ * @param config - Optional color overrides.
12
+ * @returns The renderer.
12
13
  */
13
- export declare function createWayfindingRenderer(rendererPort: RendererPort, config?: WayfindingRendererConfig): WayfindingRenderer;
14
+ export declare function createWayfindingRenderer(port: RendererPort, config?: WayfindingRendererConfig): WayfindingRenderer;
14
15
  //# sourceMappingURL=createWayfindingRenderer.d.ts.map
@@ -12,27 +12,31 @@ const DEFAULT_COLORS = {
12
12
  * together {@link LayerManager}, {@link IconManager}, {@link TrailManager},
13
13
  * and {@link RouteLineManager}.
14
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).
15
+ * Exposes `handleClick`/`handleHover` for the app glue to drive from pointer events, and
16
+ * `applyScale`/`applyRoll` for the deprecated imperative path, which has no handler dispatch of its
17
+ * own (see {@link WayfindingRenderer.applyScale}).
18
+ * @param port - The scene layers, def factories and commit function.
19
+ * @param config - Optional color overrides.
20
+ * @returns The renderer.
20
21
  */
21
- export function createWayfindingRenderer(rendererPort, config = {}) {
22
+ export function createWayfindingRenderer(port, config = {}) {
22
23
  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);
24
+ const layerManager = createLayerManager(port);
25
+ const iconManager = createIconManager(layerManager, port);
26
+ const trailManager = createTrailManager(layerManager, port);
27
+ const routeLineManager = createRouteLineManager(layerManager, colors, port);
27
28
  const wfRenderer = {
28
29
  setIcon: (...args) => iconManager.setIcon(...args),
29
30
  clearIcons: (...args) => iconManager.clearIcons(...args),
30
31
  setTrail: (...args) => trailManager.setTrail(...args),
31
32
  setRouteLines: (...args) => routeLineManager.setRouteLines(...args),
32
- applyScale: (scale) => [...iconManager.applyScale(scale), ...trailManager.applyScale(scale)],
33
+ applyScale: (pixelSize) => [
34
+ ...iconManager.applyScale(pixelSize),
35
+ ...trailManager.applyScale(pixelSize),
36
+ ],
33
37
  applyRoll: (cameraAngle) => iconManager.applyRoll(cameraAngle),
34
- handleClick: (defs) => iconManager.handleClick(defs),
35
- handleHover: (defs) => iconManager.handleHover(defs),
38
+ handleClick: (def) => iconManager.handleClick(def),
39
+ handleHover: (def) => iconManager.handleHover(def),
36
40
  clearRoute() {
37
41
  routeLineManager.clearLines();
38
42
  trailManager.destroy();
@@ -1,6 +1,20 @@
1
- import type { RenderableDef } from '@expofp/renderer';
1
+ import type { ImageDef, RenderableDef } from '@expofp/renderer';
2
2
  import type { LayerManager } from './layerManager.js';
3
3
  import type { IconConfig, RendererPort } from './types.js';
4
+ /**
5
+ * Lays one wayfinding icon out for a new pixel size: it holds its on-screen size, so its bounds are
6
+ * the source's pixel dimensions times the pixel size, about its own centre — rotation and elevation
7
+ * untouched.
8
+ *
9
+ * The **only** writer of an icon's bounds size, which is what lets everything else here write logical
10
+ * fields alone and never track a scale. One function for every icon in the plan, reached through
11
+ * each icon's own `onScale` closure and called directly by `applyScale`.
12
+ * @param def - The icon to lay out.
13
+ * @param pixelSize - Plan units per device pixel.
14
+ * @returns The def when it resized, nothing when the size was already right — a settled camera costs
15
+ * no slot writes.
16
+ */
17
+ export declare function rescaleIcon(def: ImageDef, pixelSize: number): readonly RenderableDef[] | undefined;
4
18
  /**
5
19
  * Manages `ImageDef` icons in the scene graph.
6
20
  *
@@ -20,20 +34,21 @@ export interface IconManager {
20
34
  hideAll(): void;
21
35
  /** Clear internal state. */
22
36
  destroy(): void;
23
- /** Recompute bounds of all visible icons for `scale`; returns changed defs. */
24
- applyScale(scale: number): RenderableDef[];
37
+ /** Lay every visible icon out for `pixelSize`; returns changed defs. See {@link rescaleIcon}. */
38
+ applyScale(pixelSize: number): RenderableDef[];
25
39
  /** Apply camera roll to cardinal-snap icons; returns changed defs. */
26
40
  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;
41
+ /** Fire a clicked icon's `onClick`; returns whether the def was one. */
42
+ handleClick(def: RenderableDef): boolean;
43
+ /** Whether a def is a clickable icon. */
44
+ handleHover(def: RenderableDef): boolean;
31
45
  }
32
46
  /**
33
47
  * Create an {@link IconManager} that places `ImageDef` icons into layers
34
48
  * managed by the given {@link LayerManager}.
35
- * @param layerManager
36
- * @param rendererPort
49
+ * @param layerManager - Where icons are placed and dirt is tracked.
50
+ * @param port - Supplies the def factories; see {@link RendererPort}.
51
+ * @returns The manager.
37
52
  */
38
- export declare function createIconManager(layerManager: LayerManager, rendererPort: RendererPort): IconManager;
53
+ export declare function createIconManager(layerManager: LayerManager, port: RendererPort): IconManager;
39
54
  //# sourceMappingURL=iconManager.d.ts.map
@@ -1,14 +1,43 @@
1
1
  import { Rect } from '@expofp/geometry';
2
+ /**
3
+ * Lays one wayfinding icon out for a new pixel size: it holds its on-screen size, so its bounds are
4
+ * the source's pixel dimensions times the pixel size, about its own centre — rotation and elevation
5
+ * untouched.
6
+ *
7
+ * The **only** writer of an icon's bounds size, which is what lets everything else here write logical
8
+ * fields alone and never track a scale. One function for every icon in the plan, reached through
9
+ * each icon's own `onScale` closure and called directly by `applyScale`.
10
+ * @param def - The icon to lay out.
11
+ * @param pixelSize - Plan units per device pixel.
12
+ * @returns The def when it resized, nothing when the size was already right — a settled camera costs
13
+ * no slot writes.
14
+ */
15
+ export function rescaleIcon(def, pixelSize) {
16
+ const bounds = def.bounds;
17
+ const source = def.source;
18
+ const width = source.width * pixelSize;
19
+ const height = source.height * pixelSize;
20
+ if (bounds.size.x === width && bounds.size.y === height)
21
+ return undefined;
22
+ bounds.set(bounds.center.x, bounds.center.y, width, height, bounds.rotation, bounds.elevation);
23
+ return [def];
24
+ }
2
25
  /**
3
26
  * Create an {@link IconManager} that places `ImageDef` icons into layers
4
27
  * managed by the given {@link LayerManager}.
5
- * @param layerManager
6
- * @param rendererPort
28
+ * @param layerManager - Where icons are placed and dirt is tracked.
29
+ * @param port - Supplies the def factories; see {@link RendererPort}.
30
+ * @returns The manager.
7
31
  */
8
- export function createIconManager(layerManager, rendererPort) {
32
+ export function createIconManager(layerManager, port) {
9
33
  const icons = new Map();
10
34
  const iconKeysByName = new Map();
11
35
  const keyToName = new Map();
36
+ /**
37
+ * The reverse of {@link icons}: a pointer event names the def that was hit, and this is what turns
38
+ * it back into the icon that owns it — rather than scanning every record per event.
39
+ */
40
+ const iconsByDef = new Map();
12
41
  function composeKey(name, key) {
13
42
  return key === undefined ? name : `${name}#${key}`;
14
43
  }
@@ -32,18 +61,37 @@ export function createIconManager(layerManager, rendererPort) {
32
61
  }
33
62
  function createIcon(iconKey, name, cfg) {
34
63
  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 });
64
+ // Size zero on purpose: an icon's size is only expressible in terms of the current pixel size, and
65
+ // `rescaleIcon` is its one writer. The renderer lays a def out as it registers, hidden or not, so
66
+ // it is never drawn at this placeholder.
67
+ const def = port.createImageDef(cfg.canvas, new Rect({ x: cfg.x, y: cfg.y }, ZERO, cfg.rotation), {
68
+ hidden: cfg.hidden ?? false,
69
+ dim: cfg.dimmed ? 1 : 0,
70
+ origin: cfg.origin,
71
+ });
72
+ def.onScale = ({ pixelSize }) => rescaleIcon(def, pixelSize);
73
+ // Cardinal-snap icons also declare the roll rule, so a renderer that dispatches def handlers
74
+ // keeps them square to the screen; `applyRoll` drives the same arithmetic for the path that
75
+ // dispatches nothing.
76
+ if (cfg.cardinalSnap) {
77
+ def.onRoll = ({ roll }) => {
78
+ const bounds = def.bounds;
79
+ const delta = port.getRotation(roll, bounds.rotation);
80
+ if (delta === undefined)
81
+ return undefined;
82
+ bounds.rotate(delta, bounds);
83
+ return [def];
84
+ };
85
+ }
37
86
  layer.children.push(def);
38
87
  const record = {
39
88
  imageDef: def,
40
89
  layerName: cfg.layer,
41
90
  cardinalSnap: cfg.cardinalSnap ?? false,
42
- center: { x: cfg.x, y: cfg.y },
43
- rotation: cfg.rotation,
44
91
  callback: cfg.onClick ?? null,
45
92
  };
46
93
  icons.set(iconKey, record);
94
+ iconsByDef.set(def, record);
47
95
  trackKey(name, iconKey);
48
96
  return record;
49
97
  }
@@ -55,6 +103,7 @@ export function createIconManager(layerManager, rendererPort) {
55
103
  oldChildren.splice(idx, 1);
56
104
  layerManager.touchedLayers.set(record.layerName, oldLayer);
57
105
  icons.delete(iconKey);
106
+ iconsByDef.delete(record.imageDef);
58
107
  untrackKey(iconKey);
59
108
  createIcon(iconKey, name, cfg);
60
109
  }
@@ -66,25 +115,35 @@ export function createIconManager(layerManager, rendererPort) {
66
115
  recreateIcon(iconKey, name, record, cfg);
67
116
  return;
68
117
  }
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));
118
+ // Logical fields only — where the icon is, which way it faces. The size it already carries is
119
+ // correct for the camera it was laid out at, and moving an icon does not change that.
120
+ const bounds = record.imageDef.bounds;
121
+ bounds.set(cfg.x, cfg.y, bounds.size.x, bounds.size.y, cfg.rotation ?? 0, bounds.elevation);
71
122
  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;
123
+ record.imageDef.dim = cfg.dimmed ? 1 : 0;
75
124
  record.callback = cfg.onClick ?? null;
76
- layerManager.touchLayer(cfg.layer);
125
+ layerManager.touchDef(record.imageDef);
126
+ }
127
+ /**
128
+ * Hides one icon and records that its def changed.
129
+ *
130
+ * The def, not the layer that holds it: the layer's children are unchanged, so a renderer
131
+ * reconciling the layer would find nothing to do and never reach this instance. That is exactly
132
+ * how a cleared route left its endpoint pins on screen.
133
+ * @param record - The icon to hide.
134
+ */
135
+ function hideIcon(record) {
136
+ record.imageDef.hidden = true;
137
+ record.callback = null;
138
+ layerManager.touchDef(record.imageDef);
77
139
  }
78
140
  return {
79
141
  setIcon(name, cfg, key) {
80
142
  const iconKey = composeKey(name, key);
81
143
  if (cfg === null) {
82
144
  const record = icons.get(iconKey);
83
- if (record) {
84
- record.imageDef.hidden = true;
85
- record.callback = null;
86
- layerManager.touchLayer(record.layerName);
87
- }
145
+ if (record)
146
+ hideIcon(record);
88
147
  return;
89
148
  }
90
149
  const existing = icons.get(iconKey);
@@ -101,33 +160,29 @@ export function createIconManager(layerManager, rendererPort) {
101
160
  continue;
102
161
  for (const iconKey of nameKeys) {
103
162
  const record = icons.get(iconKey);
104
- if (record) {
105
- record.imageDef.hidden = true;
106
- record.callback = null;
107
- layerManager.touchLayer(record.layerName);
108
- }
163
+ if (record)
164
+ hideIcon(record);
109
165
  }
110
166
  }
111
167
  },
112
168
  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
- }
169
+ for (const record of icons.values())
170
+ hideIcon(record);
118
171
  },
119
172
  destroy() {
120
173
  icons.clear();
174
+ iconsByDef.clear();
121
175
  iconKeysByName.clear();
122
176
  keyToName.clear();
123
177
  },
124
- applyScale(scale) {
178
+ applyScale(pixelSize) {
125
179
  const defs = [];
126
180
  for (const record of icons.values()) {
127
181
  if (record.imageDef.hidden)
128
182
  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);
183
+ const changed = rescaleIcon(record.imageDef, pixelSize);
184
+ if (changed)
185
+ defs.push(...changed);
131
186
  }
132
187
  return defs;
133
188
  },
@@ -136,31 +191,27 @@ export function createIconManager(layerManager, rendererPort) {
136
191
  for (const record of icons.values()) {
137
192
  if (!record.cardinalSnap)
138
193
  continue;
139
- const currentRotation = record.rotation ?? 0;
140
- const delta = rendererPort.getRotation(cameraAngle, currentRotation);
194
+ const bounds = record.imageDef.bounds;
195
+ const delta = port.getRotation(cameraAngle, bounds.rotation);
141
196
  if (delta === undefined)
142
197
  continue;
143
- record.rotation = currentRotation + delta;
144
- record.imageDef.bounds.rotation = record.rotation;
198
+ bounds.rotate(delta, bounds);
145
199
  defs.push(record.imageDef);
146
200
  }
147
201
  return defs;
148
202
  },
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
- }
203
+ handleClick(def) {
204
+ const record = iconsByDef.get(def);
205
+ if (!record?.callback || record.imageDef.hidden)
206
+ return false;
207
+ record.callback();
208
+ return true;
156
209
  },
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;
210
+ handleHover(def) {
211
+ const record = iconsByDef.get(def);
212
+ return Boolean(record?.callback) && !record?.imageDef.hidden;
164
213
  },
165
214
  };
166
215
  }
216
+ /** The size a new icon is created at, before its handler gives it a real one. */
217
+ const ZERO = { x: 0, y: 0 };
@@ -1,27 +1,38 @@
1
- import type { LayerDef } from '@expofp/renderer';
1
+ import type { LayerDef, RenderableDef } from '@expofp/renderer';
2
2
  import type { RendererPort } from './types.js';
3
3
  /**
4
4
  * Manages scene layer resolution, caching, dirty-tracking, and flush.
5
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.
6
+ * Layers are resolved by name from the scene layers handed in and cached. `touchLayer` marks a
7
+ * layer's **membership** as dirty and `touchDef` marks one def's **fields**; `flush` commits both
8
+ * through the injected `update` in a single call.
9
+ *
10
+ * The distinction is not cosmetic. A renderer reconciles a layer's children but writes a def's slot,
11
+ * so a def whose `hidden` or `bounds` moved is only drawn correctly if the def itself is committed —
12
+ * updating the layer that holds it reconciles a membership that did not change and reaches the def's
13
+ * instance not at all.
9
14
  */
10
15
  export interface LayerManager {
11
- /** Layers modified since the last {@link flush}. */
16
+ /** Layers whose membership changed since the last {@link flush}. */
12
17
  readonly touchedLayers: Map<string, LayerDef>;
13
18
  /** Resolve a layer by name (cached). Throws if not found. */
14
19
  resolveLayer(name: string): LayerDef;
15
- /** Resolve a layer and mark it as dirty for the next {@link flush}. */
20
+ /** Resolve a layer and mark its membership as dirty for the next {@link flush}. */
16
21
  touchLayer(name: string): LayerDef;
17
- /** Commit all dirty layers to `RendererService` and clear the dirty set. */
22
+ /**
23
+ * Mark one def's fields as dirty for the next {@link flush}.
24
+ * @param def - The def whose fields were mutated.
25
+ */
26
+ touchDef(def: RenderableDef): void;
27
+ /** Commit everything dirty and clear the dirty sets. */
18
28
  flush(): void;
19
- /** Clear caches and dirty set. */
29
+ /** Clear caches and dirty sets. */
20
30
  destroy(): void;
21
31
  }
22
32
  /**
23
- * Create a {@link LayerManager} bound to the given {@link RendererPort}.
24
- * @param rendererPort
33
+ * Create a {@link LayerManager} over the scene layers the caller resolved.
34
+ * @param port - The scene layers and the commit function; see {@link RendererPort}.
35
+ * @returns The manager.
25
36
  */
26
- export declare function createLayerManager(rendererPort: RendererPort): LayerManager;
37
+ export declare function createLayerManager(port: RendererPort): LayerManager;
27
38
  //# sourceMappingURL=layerManager.d.ts.map
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Create a {@link LayerManager} bound to the given {@link RendererPort}.
3
- * @param rendererPort
2
+ * Create a {@link LayerManager} over the scene layers the caller resolved.
3
+ * @param port - The scene layers and the commit function; see {@link RendererPort}.
4
+ * @returns The manager.
4
5
  */
5
- export function createLayerManager(rendererPort) {
6
- const allLayers = rendererPort.getLayers();
6
+ export function createLayerManager(port) {
7
+ const allLayers = port.layers;
7
8
  const layerCache = new Map();
8
9
  const touchedLayers = new Map();
10
+ const touchedDefs = new Set();
9
11
  return {
10
12
  touchedLayers,
11
13
  resolveLayer(name) {
@@ -23,17 +25,22 @@ export function createLayerManager(rendererPort) {
23
25
  touchedLayers.set(name, layer);
24
26
  return layer;
25
27
  },
28
+ touchDef(def) {
29
+ touchedDefs.add(def);
30
+ },
26
31
  flush() {
27
- if (touchedLayers.size === 0)
32
+ if (touchedLayers.size === 0 && touchedDefs.size === 0)
28
33
  return;
29
34
  for (const layer of touchedLayers.values()) {
30
35
  layer.children = [...layer.children];
31
36
  }
32
- rendererPort.update(...touchedLayers.values());
37
+ port.update(...touchedLayers.values(), ...touchedDefs);
33
38
  touchedLayers.clear();
39
+ touchedDefs.clear();
34
40
  },
35
41
  destroy() {
36
42
  touchedLayers.clear();
43
+ touchedDefs.clear();
37
44
  layerCache.clear();
38
45
  },
39
46
  };
@@ -1,11 +1,25 @@
1
- import type { Point2Like } from '@expofp/geometry';
2
1
  import type { LineDef } from '@expofp/renderer';
3
2
  export interface AnimationHandle {
4
3
  stop: () => void;
5
4
  getProgress: () => number;
6
5
  }
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;
6
+ /**
7
+ * Runs the chase animation over route segments that are already in the scene, **moving them in
8
+ * place**: the segments behind the head are drawn whole, the one under it is drawn part-way, and the
9
+ * ones ahead are hidden.
10
+ *
11
+ * The membership never changes, which is the point. Rebuilding the layer's children every frame —
12
+ * with a fresh def for the partial segment — remounted an instance and churned a batch slot sixty
13
+ * times a second, and the overlay lost everything but the newest chunk. `LineDef.points` is re-read
14
+ * on every update precisely so an animation can move a line instead of replacing it.
15
+ *
16
+ * Only what actually changed is reported: normally the single segment under the head, and every
17
+ * segment once per lap, when the chase wraps.
18
+ * @param segments - The defs to animate, in route order. Their `points` are moved in place.
19
+ * @param onChange - Called with the defs that changed, for the caller to commit.
20
+ * @param getPixelSize - Plan units per device pixel, which the speed is modulated by.
21
+ * @param startProgress - Where in the lap to resume, in `[0, 1)`.
22
+ * @returns A handle, or nothing when there is no route to animate.
23
+ */
24
+ export declare function animateLineSegments(segments: LineDef[], onChange: (changed: readonly LineDef[]) => void, getPixelSize: () => number, startProgress?: number): AnimationHandle | undefined;
11
25
  //# sourceMappingURL=lineAnimation.d.ts.map
@@ -1,15 +1,48 @@
1
- export function animateLineSegments(lineSegments, callback, getScale, startProgress = 0) {
1
+ /** How much of a segment is drawn: the whole of it, or none. */
2
+ const WHOLE = 1;
3
+ const NOTHING = 0;
4
+ /**
5
+ * Runs the chase animation over route segments that are already in the scene, **moving them in
6
+ * place**: the segments behind the head are drawn whole, the one under it is drawn part-way, and the
7
+ * ones ahead are hidden.
8
+ *
9
+ * The membership never changes, which is the point. Rebuilding the layer's children every frame —
10
+ * with a fresh def for the partial segment — remounted an instance and churned a batch slot sixty
11
+ * times a second, and the overlay lost everything but the newest chunk. `LineDef.points` is re-read
12
+ * on every update precisely so an animation can move a line instead of replacing it.
13
+ *
14
+ * Only what actually changed is reported: normally the single segment under the head, and every
15
+ * segment once per lap, when the chase wraps.
16
+ * @param segments - The defs to animate, in route order. Their `points` are moved in place.
17
+ * @param onChange - Called with the defs that changed, for the caller to commit.
18
+ * @param getPixelSize - Plan units per device pixel, which the speed is modulated by.
19
+ * @param startProgress - Where in the lap to resume, in `[0, 1)`.
20
+ * @returns A handle, or nothing when there is no route to animate.
21
+ */
22
+ export function animateLineSegments(segments, onChange, getPixelSize, startProgress = 0) {
2
23
  let currentSegmentIndex = 0;
3
24
  let segmentProgress = 0;
4
25
  let lastTimestamp = null;
5
26
  let animationFrameId = null;
6
- if (!lineSegments.length)
27
+ if (!segments.length)
7
28
  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;
29
+ // Captured before anything is moved: from here on a def's own `p1` is wherever the chase left it.
30
+ // Copied rather than aliased, since the def's `points` is what gets replaced; `elevation` rides
31
+ // along so a segment on a raised plane does not drop to the floor the first time it is redrawn.
32
+ const ends = segments.map((segment) => ({
33
+ p0: { x: segment.points.p0.x, y: segment.points.p0.y },
34
+ p1: { x: segment.points.p1.x, y: segment.points.p1.y },
35
+ elevation: segment.points.elevation,
36
+ }));
37
+ const segmentLengths = ends.map((end) => {
38
+ const dx = end.p1.x - end.p0.x;
39
+ const dy = end.p1.y - end.p0.y;
11
40
  return Math.sqrt(dx * dx + dy * dy);
12
41
  });
42
+ /** The fraction each segment is currently drawn at; `-1` until it has been written even once. */
43
+ const drawn = segments.map(() => -1);
44
+ /** What the frame being rendered moved, refilled per frame so a frame allocates nothing. */
45
+ const changed = [];
13
46
  const totalLength = segmentLengths.reduce((a, b) => a + b, 0);
14
47
  // A zero-length route would spin the per-frame while-loop forever.
15
48
  if (!totalLength)
@@ -33,51 +66,79 @@ export function animateLineSegments(lineSegments, callback, getScale, startProgr
33
66
  }
34
67
  }
35
68
  }
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
- };
69
+ /**
70
+ * Brings one segment to a fraction of its length — hidden at `0`, whole at `1`, cut short between
71
+ * — and records it as changed if that moved it. A segment already drawn at this fraction costs
72
+ * nothing, which is what keeps a frame to the one segment under the head.
73
+ *
74
+ * `points` is replaced rather than mutated, because a `LineLike`'s coordinates are readonly. The
75
+ * def stays the same object throughout, which is all the renderer keys anything on.
76
+ * @param index - The segment to draw.
77
+ * @param fraction - How much of it to draw, in `[0, 1]`.
78
+ */
79
+ function draw(index, fraction) {
80
+ if (drawn[index] === fraction)
81
+ return;
82
+ drawn[index] = fraction;
83
+ const segment = segments[index];
84
+ const end = ends[index];
85
+ segment.hidden = fraction <= NOTHING;
86
+ if (!segment.hidden) {
87
+ const { p0, p1 } = end;
88
+ segment.points =
89
+ fraction >= WHOLE
90
+ ? end
91
+ : {
92
+ p0,
93
+ p1: { x: p0.x + (p1.x - p0.x) * fraction, y: p0.y + (p1.y - p0.y) * fraction },
94
+ elevation: end.elevation,
95
+ };
96
+ }
97
+ changed.push(segment);
98
+ }
99
+ /**
100
+ * Brings every segment to the state the head's position implies: everything behind it is whole,
101
+ * the one under it is part-drawn, everything ahead of it is empty.
102
+ */
103
+ function render() {
104
+ changed.length = 0;
105
+ const head = currentSegmentIndex;
106
+ for (let i = 0; i < head; i++)
107
+ draw(i, WHOLE);
108
+ // A zero-length segment divides to `Infinity` and so is whole the moment the head reaches it.
109
+ draw(head, Math.min(segmentProgress / segmentLengths[head], WHOLE));
110
+ for (let i = head + 1; i < segments.length; i++)
111
+ draw(i, NOTHING);
112
+ if (changed.length)
113
+ onChange(changed);
51
114
  }
52
115
  function animate(timestamp) {
53
116
  lastTimestamp ??= timestamp;
54
117
  const delta = (timestamp - lastTimestamp) / 1000;
55
118
  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)) {
119
+ const pixelSize = getPixelSize();
120
+ // A detached canvas (host element removed from the DOM on SPA navigation) reports a bogus pixel
121
+ // size (Infinity on a zero-size canvas). Anything but a positive finite value poisons
122
+ // segmentProgress and spins the loop below, freezing the tab — skip the frame until a usable one
123
+ // returns.
124
+ if (!(Number.isFinite(pixelSize) && pixelSize > 0)) {
62
125
  animationFrameId = requestAnimationFrame(animate);
63
126
  return;
64
127
  }
65
- const adjustedSpeed = baseSpeed * Math.sqrt(scale);
128
+ const adjustedSpeed = baseSpeed * Math.sqrt(pixelSize);
66
129
  segmentProgress += adjustedSpeed * delta;
67
130
  // 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.
131
+ // the loop can advance at most segments.length steps — it can never spin,
132
+ // whatever pixel size/delta the camera feeds in.
70
133
  segmentProgress %= totalLength;
71
134
  while (segmentProgress >= segmentLengths[currentSegmentIndex]) {
72
135
  segmentProgress -= segmentLengths[currentSegmentIndex];
73
136
  currentSegmentIndex++;
74
- if (currentSegmentIndex >= lineSegments.length) {
137
+ if (currentSegmentIndex >= segments.length) {
75
138
  currentSegmentIndex = 0;
76
139
  }
77
140
  }
78
- const currentSegment = lineSegments[currentSegmentIndex];
79
- const updatedSegment = updateSegment(currentSegment, segmentProgress);
80
- callback(updatedSegment, currentSegmentIndex, false);
141
+ render();
81
142
  animationFrameId = requestAnimationFrame(animate);
82
143
  }
83
144
  function start() {
@@ -36,9 +36,10 @@ export interface RouteLineManager {
36
36
  /**
37
37
  * Create a {@link RouteLineManager} that renders `LineDef` segments
38
38
  * into layers managed by the given {@link LayerManager}.
39
- * @param layerManager
40
- * @param colors
41
- * @param rendererPort
39
+ * @param layerManager - Where the lines are placed and dirt is tracked.
40
+ * @param colors - The route palette.
41
+ * @param port - The commit function the animation ticks through.
42
+ * @returns The manager.
42
43
  */
43
- export declare function createRouteLineManager(layerManager: LayerManager, colors: RouteColors, rendererPort: RendererPort): RouteLineManager;
44
+ export declare function createRouteLineManager(layerManager: LayerManager, colors: RouteColors, port: RendererPort): RouteLineManager;
44
45
  //# sourceMappingURL=routeLineManager.d.ts.map
@@ -1,19 +1,64 @@
1
- import { animateLineSegments, } from './lineAnimation.js';
1
+ import { animateLineSegments } from './lineAnimation.js';
2
2
  /**
3
3
  * Create a {@link RouteLineManager} that renders `LineDef` segments
4
4
  * into layers managed by the given {@link LayerManager}.
5
- * @param layerManager
6
- * @param colors
7
- * @param rendererPort
5
+ * @param layerManager - Where the lines are placed and dirt is tracked.
6
+ * @param colors - The route palette.
7
+ * @param port - The commit function the animation ticks through.
8
+ * @returns The manager.
8
9
  */
9
- export function createRouteLineManager(layerManager, colors, rendererPort) {
10
+ export function createRouteLineManager(layerManager, colors, port) {
10
11
  let animationHandle = null;
11
12
  let lastLinesLayer = null;
12
13
  let lastAnimatedLayer = null;
14
+ /**
15
+ * The pixel size the chase animation modulates its speed by. Route lines need no layout of their
16
+ * own — `LineDef.width` is already in screen pixels — so this is the one thing the animated layer's
17
+ * handler exists to deliver.
18
+ */
19
+ let currentPixelSize = 1;
20
+ /**
21
+ * Reports the pixel size to the chase animation — the animated-lines layer's `onScale`.
22
+ *
23
+ * Lays nothing out and returns nothing, so it never costs a write. It is here because the speed
24
+ * term reads a scale and there is no other channel that carries one.
25
+ * @param pixelSize - Plan units per device pixel.
26
+ */
27
+ function readPixelSize(pixelSize) {
28
+ currentPixelSize = pixelSize;
29
+ }
30
+ /**
31
+ * Empties one lines layer, hiding each def in place first.
32
+ *
33
+ * The hide is not redundant with the removal. The renderer commits a def write synchronously but
34
+ * defers a membership sweep — renderer's `useStagedChildren` (`objects/layer/
35
+ * mount-scheduler.ts`) pipes a layer's children through `useDeferredValue`, so the unmount that
36
+ * releases the removed instances lands a transition commit later. Lines that were only removed
37
+ * therefore outlived the route's icons — which `hideIcon` hides in place — by a frame or more,
38
+ * and the cleared route's line visibly lingered behind its pins. Hiding first puts the whole
39
+ * teardown in the same synchronous commit; the deferred sweep then releases slots that are
40
+ * already invisible. Drop the hides if the renderer ever hides departed defs urgently itself
41
+ * (the place to do it is `useStagedChildren`, where the fresh snapshot and the deferred children
42
+ * are both at hand).
43
+ * @param layer - The layer to empty, when it is known yet.
44
+ */
45
+ function wipe(layer) {
46
+ if (!layer)
47
+ return;
48
+ for (const def of layer.children) {
49
+ def.hidden = true;
50
+ layerManager.touchDef(def);
51
+ }
52
+ layer.children = [];
53
+ layerManager.touchedLayers.set(layer.name, layer);
54
+ }
13
55
  return {
14
56
  setRouteLines(passed, remaining, options) {
15
57
  const linesLayer = layerManager.touchLayer(options.linesLayer);
16
58
  const animatedLayer = layerManager.touchLayer(options.animatedLinesLayer);
59
+ // Declared here rather than at construction, because this is where the layer is first known.
60
+ // The `update(layer)` a flush performs is what makes it register under this.
61
+ animatedLayer.onScale = ({ pixelSize }) => readPixelSize(pixelSize);
17
62
  lastLinesLayer = linesLayer;
18
63
  lastAnimatedLayer = animatedLayer;
19
64
  const reset = options.resetAnimation ?? true;
@@ -24,36 +69,33 @@ export function createRouteLineManager(layerManager, colors, rendererPort) {
24
69
  animationHandle.stop();
25
70
  animationHandle = null;
26
71
  }
27
- const passedDefs = passed.map((seg) => rendererPort.createLineDef(seg, colors.passed));
28
- const remainingDefs = remaining.map((seg) => rendererPort.createLineDef(seg, colors.remaining));
72
+ const passedDefs = passed.map((seg) => port.createLineDef(seg, colors.passed));
73
+ const remainingDefs = remaining.map((seg) => port.createLineDef(seg, colors.remaining));
29
74
  linesLayer.children = [...passedDefs, ...remainingDefs];
30
75
  if (!remainingDefs.length) {
31
76
  animatedLayer.children = [];
32
77
  return;
33
78
  }
34
- const animatedDefs = remainingDefs.map((def) => ({
35
- ...def,
36
- color: colors.active,
79
+ // Built from the segments rather than copied off `remainingDefs`, so the two never share a
80
+ // `points` object: the chase moves these in place, and the static line beneath must not move
81
+ // with them. They start hidden, and the animation shows each as the head reaches it.
82
+ const animatedDefs = remaining.map((seg) => ({
83
+ ...port.createLineDef(seg, colors.active),
84
+ hidden: true,
37
85
  }));
86
+ // Mounted once. The chase moves these defs rather than rewriting this array, so no instance is
87
+ // remounted and no batch slot churns while it runs.
88
+ animatedLayer.children = animatedDefs;
38
89
  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;
90
+ animateLineSegments(animatedDefs, (changed) => port.update(...changed), () => currentPixelSize, resumeFrom) ?? null;
43
91
  },
44
92
  clearLines() {
45
93
  if (animationHandle) {
46
94
  animationHandle.stop();
47
95
  animationHandle = null;
48
96
  }
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
- }
97
+ wipe(lastLinesLayer);
98
+ wipe(lastAnimatedLayer);
57
99
  },
58
100
  destroy() {
59
101
  this.clearLines();
@@ -5,8 +5,10 @@ import type { RendererPort } from './types.js';
5
5
  /**
6
6
  * Manages named trails of evenly-spaced dots between two points.
7
7
  *
8
- * On camera zoom the app glue calls `applyScale` so dots are recalculated and
9
- * visual spacing stays constant.
8
+ * A trail's spacing is fixed in screen pixels, so zooming out does not shrink its dots — it holds
9
+ * fewer of them. That makes a camera move a **membership** change, which is why the layout rule is
10
+ * declared on the trail layer rather than on the dots: the defs that have to disappear cannot be the
11
+ * ones asked to lay themselves out.
10
12
  */
11
13
  export interface TrailManager {
12
14
  /**
@@ -17,16 +19,16 @@ export interface TrailManager {
17
19
  readonly canvas: HTMLCanvasElement;
18
20
  readonly layer: string;
19
21
  }): void;
20
- /** Redraw all trails for `scale`; returns changed layer defs. */
21
- applyScale(scale: number): RenderableDef[];
22
+ /** Rebuild all trails for `pixelSize`; returns changed layer defs. */
23
+ applyScale(pixelSize: number): RenderableDef[];
22
24
  /** Remove all trails. */
23
25
  destroy(): void;
24
26
  }
25
27
  /**
26
28
  * Create a {@link TrailManager} that renders dot trails into layers
27
29
  * managed by the given {@link LayerManager}.
28
- * @param layerManager
29
- * @param rendererPort
30
+ * @param layerManager - Where the trails are placed and dirt is tracked.
31
+ * @returns The manager.
30
32
  */
31
- export declare function createTrailManager(layerManager: LayerManager, rendererPort: RendererPort): TrailManager;
33
+ export declare function createTrailManager(layerManager: LayerManager, port: RendererPort): TrailManager;
32
34
  //# sourceMappingURL=trailManager.d.ts.map
@@ -1,15 +1,47 @@
1
1
  import { Rect } from '@expofp/geometry';
2
2
  import { computeTrailPoints } from '../core/index.js';
3
- /** Distance between trail dots at scale=1, in the same units as route points. */
4
- const TRAIL_INTERVAL_AT_BASE_SCALE = 40;
3
+ /** Distance between trail dots at a pixel size of 1, in the same units as route points. */
4
+ const TRAIL_INTERVAL_AT_BASE_PIXEL_SIZE = 40;
5
5
  /**
6
6
  * Create a {@link TrailManager} that renders dot trails into layers
7
7
  * managed by the given {@link LayerManager}.
8
- * @param layerManager
9
- * @param rendererPort
8
+ * @param layerManager - Where the trails are placed and dirt is tracked.
9
+ * @returns The manager.
10
10
  */
11
- export function createTrailManager(layerManager, rendererPort) {
11
+ export function createTrailManager(layerManager, port) {
12
12
  const trails = new Map();
13
+ /**
14
+ * The pixel size the trails currently hold their spacing for.
15
+ *
16
+ * The one place the wayfinding side remembers a pixel size, and it has to: a trail's membership
17
+ * changes for two reasons and only one of them is a camera move — `setTrail` rebuilds when the
18
+ * route changes, at a moment no `onScale` is delivering a size. Seeded at `1` and corrected by the
19
+ * layer handler's first run, which registration performs before anything is drawn.
20
+ */
21
+ let currentPixelSize = 1;
22
+ /**
23
+ * Rebuilds every trail on one layer for a new pixel size — what each trail layer's `onScale`
24
+ * closure delegates to.
25
+ *
26
+ * One function per manager, not per def: what it reads is this manager's trails, and the closure
27
+ * hands it the one layer it serves.
28
+ * @param layer - The trail layer.
29
+ * @param pixelSize - Plan units per device pixel.
30
+ * @returns The layer when its membership changed, so the renderer reconciles it.
31
+ */
32
+ function layOutTrails(layer, pixelSize) {
33
+ currentPixelSize = pixelSize;
34
+ let rebuilt = false;
35
+ for (const record of trails.values()) {
36
+ if (record.layerName !== layer.name)
37
+ continue;
38
+ redraw(record);
39
+ rebuilt = true;
40
+ }
41
+ if (rebuilt)
42
+ return [layer];
43
+ return undefined;
44
+ }
13
45
  function removeDotsFromLayer(record) {
14
46
  if (!record.dotDefs.length)
15
47
  return;
@@ -18,12 +50,16 @@ export function createTrailManager(layerManager, rendererPort) {
18
50
  layer.children = layer.children.filter((child) => !oldDots.has(child));
19
51
  record.dotDefs = [];
20
52
  }
21
- function redraw(record, scale) {
53
+ function redraw(record) {
22
54
  const layer = layerManager.touchLayer(record.layerName);
23
- const interval = TRAIL_INTERVAL_AT_BASE_SCALE * scale;
55
+ // Declared here rather than at construction, because this is where the layer is first known. The
56
+ // `update(layer)` that a flush or a re-layout performs is what makes the layer register under it.
57
+ layer.onScale = ({ pixelSize }) => layOutTrails(layer, pixelSize);
58
+ const interval = TRAIL_INTERVAL_AT_BASE_PIXEL_SIZE * currentPixelSize;
24
59
  const points = computeTrailPoints(record.from, record.to, interval);
25
60
  const canvas = record.canvas;
26
- const newDots = points.map((point) => rendererPort.createImageDef(canvas, new Rect({ x: point.x, y: point.y }, { x: canvas.width * scale, y: canvas.height * scale })));
61
+ const size = { x: canvas.width * currentPixelSize, y: canvas.height * currentPixelSize };
62
+ const newDots = points.map((point) => port.createImageDef(canvas, new Rect({ x: point.x, y: point.y }, size)));
27
63
  removeDotsFromLayer(record);
28
64
  layer.children.push(...newDots);
29
65
  record.dotDefs = newDots;
@@ -50,7 +86,7 @@ export function createTrailManager(layerManager, rendererPort) {
50
86
  }
51
87
  existing.from = from;
52
88
  existing.to = to;
53
- redraw(existing, rendererPort.getScale());
89
+ redraw(existing);
54
90
  return;
55
91
  }
56
92
  const record = {
@@ -61,12 +97,13 @@ export function createTrailManager(layerManager, rendererPort) {
61
97
  dotDefs: [],
62
98
  };
63
99
  trails.set(name, record);
64
- redraw(record, rendererPort.getScale());
100
+ redraw(record);
65
101
  },
66
- applyScale(scale) {
102
+ applyScale(pixelSize) {
103
+ currentPixelSize = pixelSize;
67
104
  const layers = new Set();
68
105
  for (const record of trails.values()) {
69
- redraw(record, scale);
106
+ redraw(record);
70
107
  layers.add(layerManager.touchLayer(record.layerName));
71
108
  }
72
109
  return [...layers];
@@ -1,29 +1,55 @@
1
1
  import type { Point2Like, Rect } from '@expofp/geometry';
2
- import type { ImageDef, ImageSource, LayerDef, LineDef, Rect as RendererRect, RenderableDef } from '@expofp/renderer';
2
+ import type { ImageDef, ImageSource, LayerDef, LineDef, RenderableDef } from '@expofp/renderer';
3
3
  /**
4
- * The renderer this layer talks to, injected so it stays free of a concrete
5
- * renderer. Typed only against `@expofp/renderer` / `@expofp/geometry` so it
6
- * carries no dependency on `src/renderer`.
4
+ * What the wayfinding renderer needs from whatever is drawing the scene — the whole of it.
5
+ *
6
+ * The def factories are injected rather than imported because they live in the host
7
+ * (floorplan's `src/renderer`), which depends on this package — importing them back would close a
8
+ * cycle. What the port no longer carries is the camera: sizes are pushed in by the layout handlers
9
+ * the defs declare (`onScale`), never pulled through a `getScale()`, and a geometry `Rect` is the
10
+ * bounds type, so nothing needs converting on the way in either.
7
11
  */
8
12
  export interface RendererPort {
13
+ /**
14
+ * Builds an image def. Its bounds are laid out for the camera later, by `onScale`.
15
+ * @param source - The canvas or image to draw.
16
+ * @param bounds - Where it sits; size is a placeholder until the first layout.
17
+ * @param options - Initial visibility, dimming and anchor.
18
+ * @returns The def.
19
+ */
9
20
  createImageDef(source: ImageSource, bounds: Rect, options?: {
10
21
  hidden?: boolean;
11
- dim?: boolean;
22
+ dim?: number;
12
23
  interactive?: boolean;
13
24
  origin?: [number, number];
14
25
  }): ImageDef;
26
+ /**
27
+ * Builds a line def between two points.
28
+ * @param line - Its endpoints.
29
+ * @param color - Stroke color.
30
+ * @param width - Stroke width.
31
+ * @returns The def.
32
+ */
15
33
  createLineDef(line: {
16
34
  p0: Point2Like;
17
35
  p1: Point2Like;
18
36
  }, color?: string, width?: number): LineDef;
37
+ /**
38
+ * The cardinal-snap delta for a camera angle.
39
+ * @param newAngle - The camera's angle.
40
+ * @param currentRotation - The def's current rotation.
41
+ * @returns The delta to apply, or nothing while the camera is under the 45° threshold.
42
+ */
19
43
  getRotation(newAngle: number, currentRotation: number): number | undefined;
20
- /** Convert a geometry rect to the renderer's `Rect`. */
21
- rectToRenderer(rect: Rect): RendererRect;
22
- /** Current camera point-scale. */
23
- getScale(): number;
24
- /** Scene root-layer children, used to resolve wf layers by name. */
25
- getLayers(): LayerDef[];
26
- /** Commit changed defs/layers to the renderer. */
44
+ /** The scene's root layers; the `wf-*` layers are resolved out of these by name. */
45
+ readonly layers: readonly LayerDef[];
46
+ /**
47
+ * Commits changed defs.
48
+ *
49
+ * Injected rather than imported for one reason: the deprecated imperative path must reach an
50
+ * imperative renderer as well as renderer's `update`, and only its caller knows which.
51
+ * @param defs - The defs that changed.
52
+ */
27
53
  update(...defs: RenderableDef[]): void;
28
54
  }
29
55
  export interface RouteColors {
@@ -111,17 +137,34 @@ export interface WayfindingRenderer {
111
137
  readonly animatedLinesLayer: string;
112
138
  readonly resetAnimation?: boolean;
113
139
  }): void;
114
- /** Recompute icon/trail bounds for `scale`; returns changed defs to render. */
115
- applyScale(scale: number): RenderableDef[];
140
+ /**
141
+ * Lays every icon and trail out for `pixelSize`, returning the defs that changed.
142
+ *
143
+ * **Not the mechanism** — the defs declare their own `onScale` and a renderer that dispatches them
144
+ * needs nothing from here. This runs those same handlers in a loop, for the deprecated imperative
145
+ * path, which has neither dispatch nor the "laid out before first drawn" guarantee registration
146
+ * gives.
147
+ * @param pixelSize - Plan units per device pixel.
148
+ * @returns The defs that changed.
149
+ */
150
+ applyScale(pixelSize: number): RenderableDef[];
116
151
  /** Apply camera roll to cardinal-snap icons; returns changed defs to render. */
117
152
  applyRoll(cameraAngle: number): RenderableDef[];
118
- /** Fire the hit clickable icon's `onClick` from already-picked defs. */
119
- handleClick(defs: readonly RenderableDef[]): void;
120
- /** Whether a clickable icon is under the already-picked defs (for cursor). */
121
- handleHover(defs: readonly RenderableDef[]): boolean;
153
+ /**
154
+ * Fires the def's `onClick`, if it is a clickable icon.
155
+ * @param def - The def that was clicked.
156
+ * @returns Whether it was one — so a caller can stop the click going any further.
157
+ */
158
+ handleClick(def: RenderableDef): boolean;
159
+ /**
160
+ * Whether a def is a clickable icon, which is what a pointer cursor hangs off.
161
+ * @param def - The def under the pointer.
162
+ * @returns Whether it is clickable.
163
+ */
164
+ handleHover(def: RenderableDef): boolean;
122
165
  /** Clear route lines, drop trails, hide icons. Buffered — `flush()` to commit. */
123
166
  clearRoute(): void;
124
- /** Commit buffered mutations to the renderer (via the injected port). */
167
+ /** Commit buffered mutations to the renderer (via the injected `update`). */
125
168
  flush(): void;
126
169
  /** Full teardown (managers cleared). Does NOT flush. */
127
170
  destroy(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/wayfinding",
3
- "version": "3.26.0",
3
+ "version": "3.27.0",
4
4
  "type": "module",
5
5
  "description": "ExpoFP SDK internal: framework-neutral wayfinding (routing, snapping, scene rendering)",
6
6
  "homepage": "https://developer.expofp.com/",
@@ -34,12 +34,12 @@
34
34
  "dependencies": {
35
35
  "ngraph.graph": "^19.1.0",
36
36
  "tslib": "^2.3.0",
37
- "@expofp/geometry": "3.26.0"
37
+ "@expofp/geometry": "3.27.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "@expofp/renderer": "^3.2.4"
40
+ "@expofp/renderer": "3.27.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@expofp/renderer": "^3.2.4"
43
+ "@expofp/renderer": "3.27.0"
44
44
  }
45
45
  }