@expofp/wayfinding 3.31.4 → 3.32.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.
- package/README.md +2 -4
- package/dist/createWayfinding.d.ts +9 -11
- package/dist/createWayfinding.js +0 -9
- package/dist/renderer/createWayfindingRenderer.d.ts +2 -10
- package/dist/renderer/createWayfindingRenderer.js +2 -15
- package/dist/renderer/iconManager.d.ts +8 -27
- package/dist/renderer/iconManager.js +12 -67
- package/dist/renderer/trailManager.d.ts +0 -3
- package/dist/renderer/trailManager.js +109 -33
- package/dist/renderer/types.d.ts +14 -56
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ The package knows nothing concrete about the host renderer, graph data, or app
|
|
|
36
36
|
state. The host supplies four ports via `WayfindingConfig`:
|
|
37
37
|
|
|
38
38
|
- **`dataSource`** (`GraphDataSource`) — graph lines + line ends for pathfinding.
|
|
39
|
-
- **`renderer`** (`RendererPort`) — def factories,
|
|
39
|
+
- **`renderer`** (`RendererPort`) — def factories, scene layers, commit.
|
|
40
40
|
- **`iconProvider`** (`IconProvider`) — canvases for icon names.
|
|
41
41
|
- **`floorContext`** (`FloorContext`) — active floor / visibility predicates.
|
|
42
42
|
|
|
@@ -72,9 +72,7 @@ wayfinding.setRoute({ from, to, accessible: false });
|
|
|
72
72
|
wayfinding.setPosition(position); // or null to hide
|
|
73
73
|
wayfinding.notifyFloorChanged();
|
|
74
74
|
|
|
75
|
-
// Forward
|
|
76
|
-
wayfinding.applyScale(scale);
|
|
77
|
-
wayfinding.applyRoll(angle);
|
|
75
|
+
// Forward pointer events:
|
|
78
76
|
wayfinding.handleClick(pickedDefs);
|
|
79
77
|
|
|
80
78
|
wayfinding.destroy();
|
|
@@ -28,9 +28,9 @@ export interface WayfindingConfig {
|
|
|
28
28
|
readonly onRouteDistance?: (distance: number) => void;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* The single wayfinding instance. Owns the routing engine, the scene renderer,
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* The single wayfinding instance. Owns the routing engine, the scene renderer, and the runtime; the
|
|
32
|
+
* host drives it with route/position/floor updates and forwards its pointer events to the
|
|
33
|
+
* `handle*` methods. Camera moves need nothing from here — the defs carry their own layout rules.
|
|
34
34
|
*/
|
|
35
35
|
export interface Wayfinding {
|
|
36
36
|
/** Build (or update) the active route; `false` when no path exists. */
|
|
@@ -46,18 +46,16 @@ export interface Wayfinding {
|
|
|
46
46
|
notifyFloorChanged(): void;
|
|
47
47
|
/** Whether the position has drifted off the active route far enough to reroute. */
|
|
48
48
|
shouldReroute(position: RoutePoint): boolean;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/** Forward a pointer click (already picked scene defs); fires icon `onClick`, and reports whether one claimed it. */
|
|
49
|
+
/**
|
|
50
|
+
* Forward a pointer click (already picked scene defs); fires icon `onClick`, and reports whether
|
|
51
|
+
* one claimed it.
|
|
52
|
+
*/
|
|
54
53
|
handleClick(defs: readonly RenderableDef[]): boolean;
|
|
55
54
|
/** Forward a pointer move (already picked scene defs); `true` over a clickable icon. */
|
|
56
55
|
handleHover(defs: readonly RenderableDef[]): boolean;
|
|
57
56
|
/**
|
|
58
|
-
* Place, update, or hide (`config: null`) a host annotation icon —
|
|
59
|
-
*
|
|
60
|
-
* icons' scale/roll handling. Buffered — call `flush()` to commit.
|
|
57
|
+
* Place, update, or hide (`config: null`) a host annotation icon — kiosk, you-are-here — on the
|
|
58
|
+
* wayfinding icon layer, laid out like the route's own. Buffered — call `flush()` to commit.
|
|
61
59
|
*/
|
|
62
60
|
setIcon(name: string, config: IconConfig | null, key?: string | number): void;
|
|
63
61
|
/** Commit buffered scene mutations. */
|
package/dist/createWayfinding.js
CHANGED
|
@@ -22,10 +22,6 @@ export function createWayfinding(config) {
|
|
|
22
22
|
onRouteUpdate: (lines, bounds) => config.onRouteUpdate?.(lines, bounds),
|
|
23
23
|
onRouteDistance: (distance) => config.onRouteDistance?.(distance),
|
|
24
24
|
});
|
|
25
|
-
const renderDefs = (defs) => {
|
|
26
|
-
if (defs.length)
|
|
27
|
-
rendererPort.update(...defs);
|
|
28
|
-
};
|
|
29
25
|
return {
|
|
30
26
|
setRoute: (input) => runtime.setRoute(input),
|
|
31
27
|
buildRoute: (from, to, options) => engine.buildRoute(from, to, options),
|
|
@@ -33,11 +29,6 @@ export function createWayfinding(config) {
|
|
|
33
29
|
setPosition: (position) => runtime.onPositionChanged(position),
|
|
34
30
|
notifyFloorChanged: () => runtime.onFloorChanged(),
|
|
35
31
|
shouldReroute: (position) => runtime.shouldReroute(position),
|
|
36
|
-
applyScale: (scale) => renderDefs(renderer.applyScale(scale)),
|
|
37
|
-
applyRoll: (angle) => renderDefs(renderer.applyRoll(angle)),
|
|
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
32
|
handleClick: (defs) => {
|
|
42
33
|
for (const def of defs) {
|
|
43
34
|
if (renderer.handleClick(def))
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import type { RendererPort, WayfindingRenderer, WayfindingRendererConfig } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Create a {@link WayfindingRenderer} — the composition root
|
|
4
|
-
*
|
|
5
|
-
* and {@link RouteLineManager}.
|
|
6
|
-
*
|
|
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.
|
|
3
|
+
* Create a {@link WayfindingRenderer} — the composition root over {@link LayerManager},
|
|
4
|
+
* {@link IconManager}, {@link TrailManager} and {@link RouteLineManager}.
|
|
13
5
|
*/
|
|
14
6
|
export declare function createWayfindingRenderer(port: RendererPort, config?: WayfindingRendererConfig): WayfindingRenderer;
|
|
15
7
|
//# sourceMappingURL=createWayfindingRenderer.d.ts.map
|
|
@@ -8,16 +8,8 @@ const DEFAULT_COLORS = {
|
|
|
8
8
|
active: '#0794EA',
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* Create a {@link WayfindingRenderer} — the composition root
|
|
12
|
-
*
|
|
13
|
-
* and {@link RouteLineManager}.
|
|
14
|
-
*
|
|
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.
|
|
11
|
+
* Create a {@link WayfindingRenderer} — the composition root over {@link LayerManager},
|
|
12
|
+
* {@link IconManager}, {@link TrailManager} and {@link RouteLineManager}.
|
|
21
13
|
*/
|
|
22
14
|
export function createWayfindingRenderer(port, config = {}) {
|
|
23
15
|
const colors = { ...DEFAULT_COLORS, ...config.colors };
|
|
@@ -30,11 +22,6 @@ export function createWayfindingRenderer(port, config = {}) {
|
|
|
30
22
|
clearIcons: (...args) => iconManager.clearIcons(...args),
|
|
31
23
|
setTrail: (...args) => trailManager.setTrail(...args),
|
|
32
24
|
setRouteLines: (...args) => routeLineManager.setRouteLines(...args),
|
|
33
|
-
applyScale: (pixelSize) => [
|
|
34
|
-
...iconManager.applyScale(pixelSize),
|
|
35
|
-
...trailManager.applyScale(pixelSize),
|
|
36
|
-
],
|
|
37
|
-
applyRoll: (cameraAngle) => iconManager.applyRoll(cameraAngle),
|
|
38
25
|
handleClick: (def) => iconManager.handleClick(def),
|
|
39
26
|
handleHover: (def) => iconManager.handleHover(def),
|
|
40
27
|
clearRoute() {
|
|
@@ -2,28 +2,19 @@ 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
4
|
/**
|
|
5
|
-
* Lays one wayfinding icon out for a new pixel size:
|
|
6
|
-
* the source's pixel dimensions times the pixel size, about its own centre
|
|
7
|
-
* untouched.
|
|
5
|
+
* Lays one wayfinding icon out for a new pixel size: an icon holds its on-screen size, so its
|
|
6
|
+
* bounds are the source's pixel dimensions times the pixel size, about its own centre.
|
|
8
7
|
*
|
|
9
|
-
* The **only** writer of an icon's bounds size, which is what lets everything else here write
|
|
10
|
-
* fields alone and never track a scale.
|
|
11
|
-
*
|
|
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.
|
|
8
|
+
* The **only** writer of an icon's bounds size, which is what lets everything else here write
|
|
9
|
+
* logical fields alone and never track a scale. Returns nothing when the size was already right, so
|
|
10
|
+
* a settled camera costs no slot writes.
|
|
16
11
|
*/
|
|
17
12
|
export declare function rescaleIcon(def: ImageDef, pixelSize: number): readonly RenderableDef[] | undefined;
|
|
18
13
|
/**
|
|
19
14
|
* Manages `ImageDef` icons in the scene graph.
|
|
20
15
|
*
|
|
21
|
-
* Icons are keyed by `(name, key?)`. First `setIcon` creates the `ImageDef`;
|
|
22
|
-
*
|
|
23
|
-
* without destroying it.
|
|
24
|
-
*
|
|
25
|
-
* Exposes pure `applyScale`/`applyRoll`/`handleClick`/`handleHover` that the
|
|
26
|
-
* app glue drives from engine events.
|
|
16
|
+
* Icons are keyed by `(name, key?)`. First `setIcon` creates the `ImageDef`; later calls mutate in
|
|
17
|
+
* place. Pass `config: null` to hide an icon without destroying it.
|
|
27
18
|
*/
|
|
28
19
|
export interface IconManager {
|
|
29
20
|
/** Place, update, or hide an icon. See {@link WayfindingRenderer.setIcon}. */
|
|
@@ -34,21 +25,11 @@ export interface IconManager {
|
|
|
34
25
|
hideAll(): void;
|
|
35
26
|
/** Clear internal state. */
|
|
36
27
|
destroy(): void;
|
|
37
|
-
/** Lay every visible icon out for `pixelSize`; returns changed defs. See {@link rescaleIcon}. */
|
|
38
|
-
applyScale(pixelSize: number): RenderableDef[];
|
|
39
|
-
/** Apply camera roll to cardinal-snap icons; returns changed defs. */
|
|
40
|
-
applyRoll(cameraAngle: number): RenderableDef[];
|
|
41
28
|
/** Fire a clicked icon's `onClick`; returns whether the def was one. */
|
|
42
29
|
handleClick(def: RenderableDef): boolean;
|
|
43
30
|
/** Whether a def is a clickable icon. */
|
|
44
31
|
handleHover(def: RenderableDef): boolean;
|
|
45
32
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Create an {@link IconManager} that places `ImageDef` icons into layers
|
|
48
|
-
* managed by the given {@link LayerManager}.
|
|
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.
|
|
52
|
-
*/
|
|
33
|
+
/** Create an {@link IconManager} that places icons into layers the {@link LayerManager} tracks. */
|
|
53
34
|
export declare function createIconManager(layerManager: LayerManager, port: RendererPort): IconManager;
|
|
54
35
|
//# sourceMappingURL=iconManager.d.ts.map
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { Rect } from '@expofp/geometry';
|
|
2
2
|
/**
|
|
3
|
-
* Lays one wayfinding icon out for a new pixel size:
|
|
4
|
-
* the source's pixel dimensions times the pixel size, about its own centre
|
|
5
|
-
* untouched.
|
|
3
|
+
* Lays one wayfinding icon out for a new pixel size: an icon holds its on-screen size, so its
|
|
4
|
+
* bounds are the source's pixel dimensions times the pixel size, about its own centre.
|
|
6
5
|
*
|
|
7
|
-
* The **only** writer of an icon's bounds size, which is what lets everything else here write
|
|
8
|
-
* fields alone and never track a scale.
|
|
9
|
-
*
|
|
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.
|
|
6
|
+
* The **only** writer of an icon's bounds size, which is what lets everything else here write
|
|
7
|
+
* logical fields alone and never track a scale. Returns nothing when the size was already right, so
|
|
8
|
+
* a settled camera costs no slot writes.
|
|
14
9
|
*/
|
|
15
10
|
export function rescaleIcon(def, pixelSize) {
|
|
16
11
|
const bounds = def.bounds;
|
|
@@ -22,21 +17,12 @@ export function rescaleIcon(def, pixelSize) {
|
|
|
22
17
|
bounds.set(bounds.center.x, bounds.center.y, width, height, bounds.rotation, bounds.elevation);
|
|
23
18
|
return [def];
|
|
24
19
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Create an {@link IconManager} that places `ImageDef` icons into layers
|
|
27
|
-
* managed by the given {@link LayerManager}.
|
|
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.
|
|
31
|
-
*/
|
|
20
|
+
/** Create an {@link IconManager} that places icons into layers the {@link LayerManager} tracks. */
|
|
32
21
|
export function createIconManager(layerManager, port) {
|
|
33
22
|
const icons = new Map();
|
|
34
23
|
const iconKeysByName = new Map();
|
|
35
24
|
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
|
-
*/
|
|
25
|
+
/** The reverse of {@link icons}: a pointer hit names a def, so it is a lookup, not a scan. */
|
|
40
26
|
const iconsByDef = new Map();
|
|
41
27
|
function composeKey(name, key) {
|
|
42
28
|
return key === undefined ? name : `${name}#${key}`;
|
|
@@ -68,26 +54,13 @@ export function createIconManager(layerManager, port) {
|
|
|
68
54
|
hidden: cfg.hidden ?? false,
|
|
69
55
|
dim: cfg.dimmed ? 1 : 0,
|
|
70
56
|
origin: cfg.origin,
|
|
57
|
+
cardinalSnap: cfg.cardinalSnap,
|
|
71
58
|
});
|
|
72
59
|
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
|
-
}
|
|
86
60
|
layer.children.push(def);
|
|
87
61
|
const record = {
|
|
88
62
|
imageDef: def,
|
|
89
63
|
layerName: cfg.layer,
|
|
90
|
-
cardinalSnap: cfg.cardinalSnap ?? false,
|
|
91
64
|
callback: cfg.onClick ?? null,
|
|
92
65
|
};
|
|
93
66
|
icons.set(iconKey, record);
|
|
@@ -118,19 +91,16 @@ export function createIconManager(layerManager, port) {
|
|
|
118
91
|
// Logical fields only — where the icon is, which way it faces. The size it already carries is
|
|
119
92
|
// correct for the camera it was laid out at, and moving an icon does not change that.
|
|
120
93
|
const bounds = record.imageDef.bounds;
|
|
121
|
-
|
|
94
|
+
const rotation = cfg.rotation ?? bounds.rotation;
|
|
95
|
+
bounds.set(cfg.x, cfg.y, bounds.size.x, bounds.size.y, rotation, bounds.elevation);
|
|
122
96
|
record.imageDef.hidden = cfg.hidden ?? false;
|
|
123
97
|
record.imageDef.dim = cfg.dimmed ? 1 : 0;
|
|
124
98
|
record.callback = cfg.onClick ?? null;
|
|
125
99
|
layerManager.touchDef(record.imageDef);
|
|
126
100
|
}
|
|
127
101
|
/**
|
|
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.
|
|
102
|
+
* Hides one icon and records that its **def** changed, not the layer holding it: the layer's
|
|
103
|
+
* children are unchanged, so a renderer reconciling the layer would never reach this instance.
|
|
134
104
|
*/
|
|
135
105
|
function hideIcon(record) {
|
|
136
106
|
record.imageDef.hidden = true;
|
|
@@ -175,31 +145,6 @@ export function createIconManager(layerManager, port) {
|
|
|
175
145
|
iconKeysByName.clear();
|
|
176
146
|
keyToName.clear();
|
|
177
147
|
},
|
|
178
|
-
applyScale(pixelSize) {
|
|
179
|
-
const defs = [];
|
|
180
|
-
for (const record of icons.values()) {
|
|
181
|
-
if (record.imageDef.hidden)
|
|
182
|
-
continue;
|
|
183
|
-
const changed = rescaleIcon(record.imageDef, pixelSize);
|
|
184
|
-
if (changed)
|
|
185
|
-
defs.push(...changed);
|
|
186
|
-
}
|
|
187
|
-
return defs;
|
|
188
|
-
},
|
|
189
|
-
applyRoll(cameraAngle) {
|
|
190
|
-
const defs = [];
|
|
191
|
-
for (const record of icons.values()) {
|
|
192
|
-
if (!record.cardinalSnap)
|
|
193
|
-
continue;
|
|
194
|
-
const bounds = record.imageDef.bounds;
|
|
195
|
-
const delta = port.getRotation(cameraAngle, bounds.rotation);
|
|
196
|
-
if (delta === undefined)
|
|
197
|
-
continue;
|
|
198
|
-
bounds.rotate(delta, bounds);
|
|
199
|
-
defs.push(record.imageDef);
|
|
200
|
-
}
|
|
201
|
-
return defs;
|
|
202
|
-
},
|
|
203
148
|
handleClick(def) {
|
|
204
149
|
const record = iconsByDef.get(def);
|
|
205
150
|
if (!record?.callback || record.imageDef.hidden)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type Point2Like } from '@expofp/geometry';
|
|
2
|
-
import type { RenderableDef } from '@expofp/renderer';
|
|
3
2
|
import type { LayerManager } from './layerManager.js';
|
|
4
3
|
import type { RendererPort } from './types.js';
|
|
5
4
|
/**
|
|
@@ -19,8 +18,6 @@ export interface TrailManager {
|
|
|
19
18
|
readonly canvas: HTMLCanvasElement;
|
|
20
19
|
readonly layer: string;
|
|
21
20
|
}): void;
|
|
22
|
-
/** Rebuild all trails for `pixelSize`; returns changed layer defs. */
|
|
23
|
-
applyScale(pixelSize: number): RenderableDef[];
|
|
24
21
|
/** Remove all trails. */
|
|
25
22
|
destroy(): void;
|
|
26
23
|
}
|
|
@@ -20,57 +20,137 @@ export function createTrailManager(layerManager, port) {
|
|
|
20
20
|
*/
|
|
21
21
|
let currentPixelSize = 1;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* The layers this manager has declared an `onScale` on.
|
|
24
|
+
*
|
|
25
|
+
* Declaring the handler is not enough to be called: `useLayoutTargets` registers a layer as a
|
|
26
|
+
* layout target from a render, so the layer has to be committed once after the handler appears.
|
|
27
|
+
* That commit is a membership change, which is why it happens once per layer rather than per
|
|
28
|
+
* redraw.
|
|
29
|
+
*/
|
|
30
|
+
const wiredLayers = new Set();
|
|
31
|
+
/**
|
|
32
|
+
* Lays every trail on one layer out for a new pixel size — what each trail layer's `onScale`
|
|
24
33
|
* closure delegates to.
|
|
25
34
|
*
|
|
26
35
|
* One function per manager, not per def: what it reads is this manager's trails, and the closure
|
|
27
36
|
* hands it the one layer it serves.
|
|
28
37
|
* @param layer - The trail layer.
|
|
29
38
|
* @param pixelSize - Plan units per device pixel.
|
|
30
|
-
* @returns The
|
|
39
|
+
* @returns The dots that were written, plus the layer when the dot count changed with them.
|
|
31
40
|
*/
|
|
32
41
|
function layOutTrails(layer, pixelSize) {
|
|
33
42
|
currentPixelSize = pixelSize;
|
|
34
|
-
|
|
43
|
+
const changed = [];
|
|
44
|
+
let membershipChanged = false;
|
|
35
45
|
for (const record of trails.values()) {
|
|
36
46
|
if (record.layerName !== layer.name)
|
|
37
47
|
continue;
|
|
38
|
-
redraw(record);
|
|
39
|
-
|
|
48
|
+
const result = redraw(record);
|
|
49
|
+
changed.push(...result.changed);
|
|
50
|
+
membershipChanged ||= result.membershipChanged;
|
|
40
51
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
// The layer only when the dot count changed: a def write lands in this frame, a membership
|
|
53
|
+
// change a transition later, so a camera step that only shifts dots stays in this one.
|
|
54
|
+
if (membershipChanged)
|
|
55
|
+
changed.push(layer);
|
|
56
|
+
return changed.length ? changed : undefined;
|
|
44
57
|
}
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Takes dots out of their layer, hidden first.
|
|
60
|
+
*
|
|
61
|
+
* The hide is not redundant with the removal: the renderer commits a def write synchronously and
|
|
62
|
+
* defers the membership sweep, so a dot that was only removed stays on screen a frame or more.
|
|
63
|
+
* @param record - The trail the dots belong to.
|
|
64
|
+
* @param dots - The dots to take out.
|
|
65
|
+
*/
|
|
66
|
+
function removeDots(record, dots) {
|
|
67
|
+
if (!dots.length)
|
|
47
68
|
return;
|
|
48
69
|
const layer = layerManager.touchLayer(record.layerName);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
const dropped = new Set(dots);
|
|
71
|
+
for (const dot of dropped)
|
|
72
|
+
dot.hidden = true;
|
|
73
|
+
layer.children = layer.children.filter((child) => !dropped.has(child));
|
|
52
74
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The trail layer, with this manager's `onScale` declared on it the first time it is asked for.
|
|
77
|
+
* @param name - The layer's name.
|
|
78
|
+
* @returns The layer.
|
|
79
|
+
*/
|
|
80
|
+
function wireLayer(name) {
|
|
81
|
+
const layer = layerManager.resolveLayer(name);
|
|
82
|
+
if (wiredLayers.has(name))
|
|
83
|
+
return layer;
|
|
57
84
|
layer.onScale = ({ pixelSize }) => layOutTrails(layer, pixelSize);
|
|
85
|
+
wiredLayers.add(name);
|
|
86
|
+
// Committed with the layer, which is what makes the renderer register the handler.
|
|
87
|
+
layerManager.touchLayer(name);
|
|
88
|
+
return layer;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Lays one trail out at the current pixel size, reusing the dots it already holds.
|
|
92
|
+
*
|
|
93
|
+
* Spacing is fixed in screen pixels, so a camera step moves every dot and changes how many there
|
|
94
|
+
* are. Moving the survivors rather than replacing them is what keeps the common case a def write:
|
|
95
|
+
* only the difference in count reaches the layer's membership.
|
|
96
|
+
* @param record - The trail to lay out.
|
|
97
|
+
* @returns The dots that were written, and whether the layer's membership changed with them.
|
|
98
|
+
*/
|
|
99
|
+
function redraw(record) {
|
|
100
|
+
const layer = wireLayer(record.layerName);
|
|
58
101
|
const interval = TRAIL_INTERVAL_AT_BASE_PIXEL_SIZE * currentPixelSize;
|
|
59
102
|
const points = computeTrailPoints(record.from, record.to, interval);
|
|
60
103
|
const canvas = record.canvas;
|
|
61
104
|
const size = { x: canvas.width * currentPixelSize, y: canvas.height * currentPixelSize };
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
105
|
+
const dots = record.dotDefs;
|
|
106
|
+
const kept = Math.min(dots.length, points.length);
|
|
107
|
+
const changed = [];
|
|
108
|
+
for (let i = 0; i < kept; i++) {
|
|
109
|
+
const bounds = dots[i].bounds;
|
|
110
|
+
bounds.set(points[i].x, points[i].y, size.x, size.y, bounds.rotation, bounds.elevation);
|
|
111
|
+
changed.push(dots[i]);
|
|
112
|
+
}
|
|
113
|
+
if (points.length > kept) {
|
|
114
|
+
const added = points
|
|
115
|
+
.slice(kept)
|
|
116
|
+
.map((point) => port.createImageDef(canvas, new Rect({ x: point.x, y: point.y }, size)));
|
|
117
|
+
layer.children.push(...added);
|
|
118
|
+
dots.push(...added);
|
|
119
|
+
layerManager.touchLayer(record.layerName);
|
|
120
|
+
return { changed, membershipChanged: true };
|
|
121
|
+
}
|
|
122
|
+
if (dots.length > kept) {
|
|
123
|
+
const dropped = dots.splice(kept);
|
|
124
|
+
removeDots(record, dropped);
|
|
125
|
+
// Hidden, and reported with the rest: the hide is a def write, and only a def the handler
|
|
126
|
+
// returns is written in this frame — the removal itself lands a transition later.
|
|
127
|
+
changed.push(...dropped);
|
|
128
|
+
return { changed, membershipChanged: true };
|
|
129
|
+
}
|
|
130
|
+
return { changed, membershipChanged: false };
|
|
66
131
|
}
|
|
67
132
|
function drop(name) {
|
|
68
133
|
const record = trails.get(name);
|
|
69
134
|
if (!record)
|
|
70
135
|
return;
|
|
71
|
-
|
|
136
|
+
removeDots(record, record.dotDefs);
|
|
137
|
+
for (const dot of record.dotDefs)
|
|
138
|
+
layerManager.touchDef(dot);
|
|
139
|
+
record.dotDefs = [];
|
|
72
140
|
trails.delete(name);
|
|
73
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Queues what a redraw wrote for the next {@link LayerManager.flush} — the buffered path only.
|
|
144
|
+
*
|
|
145
|
+
* A redraw driven by the camera reports the same defs to the renderer, which writes them in that
|
|
146
|
+
* frame; queuing them here as well would leave them in the dirty set for an unrelated flush to
|
|
147
|
+
* commit again.
|
|
148
|
+
* @param result - What the redraw changed.
|
|
149
|
+
*/
|
|
150
|
+
function bufferRedraw(result) {
|
|
151
|
+
for (const def of result.changed)
|
|
152
|
+
layerManager.touchDef(def);
|
|
153
|
+
}
|
|
74
154
|
return {
|
|
75
155
|
setTrail(name, from, to, options) {
|
|
76
156
|
if (from === null || to === null) {
|
|
@@ -86,7 +166,7 @@ export function createTrailManager(layerManager, port) {
|
|
|
86
166
|
}
|
|
87
167
|
existing.from = from;
|
|
88
168
|
existing.to = to;
|
|
89
|
-
redraw(existing);
|
|
169
|
+
bufferRedraw(redraw(existing));
|
|
90
170
|
return;
|
|
91
171
|
}
|
|
92
172
|
const record = {
|
|
@@ -97,20 +177,16 @@ export function createTrailManager(layerManager, port) {
|
|
|
97
177
|
dotDefs: [],
|
|
98
178
|
};
|
|
99
179
|
trails.set(name, record);
|
|
100
|
-
redraw(record);
|
|
101
|
-
},
|
|
102
|
-
applyScale(pixelSize) {
|
|
103
|
-
currentPixelSize = pixelSize;
|
|
104
|
-
const layers = new Set();
|
|
105
|
-
for (const record of trails.values()) {
|
|
106
|
-
redraw(record);
|
|
107
|
-
layers.add(layerManager.touchLayer(record.layerName));
|
|
108
|
-
}
|
|
109
|
-
return [...layers];
|
|
180
|
+
bufferRedraw(redraw(record));
|
|
110
181
|
},
|
|
111
182
|
destroy() {
|
|
112
183
|
for (const trailName of [...trails.keys()])
|
|
113
184
|
drop(trailName);
|
|
185
|
+
// The layer outlives this manager, so the handler has to go with it: left in place it keeps
|
|
186
|
+
// the manager, its layers and its canvases reachable, and runs on every camera step.
|
|
187
|
+
for (const name of wiredLayers)
|
|
188
|
+
layerManager.resolveLayer(name).onScale = undefined;
|
|
189
|
+
wiredLayers.clear();
|
|
114
190
|
},
|
|
115
191
|
};
|
|
116
192
|
}
|
package/dist/renderer/types.d.ts
CHANGED
|
@@ -1,54 +1,34 @@
|
|
|
1
1
|
import type { Point2Like, Rect } from '@expofp/geometry';
|
|
2
2
|
import type { ImageDef, ImageSource, LayerDef, LineDef, RenderableDef } from '@expofp/renderer';
|
|
3
3
|
/**
|
|
4
|
-
* What the wayfinding renderer needs from whatever is drawing the scene
|
|
4
|
+
* What the wayfinding renderer needs from whatever is drawing the scene.
|
|
5
5
|
*
|
|
6
6
|
* The def factories are injected rather than imported because they live in the host
|
|
7
7
|
* (floorplan's `src/renderer`), which depends on this package — importing them back would close a
|
|
8
|
-
* cycle.
|
|
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.
|
|
8
|
+
* cycle.
|
|
11
9
|
*/
|
|
12
10
|
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
|
-
*/
|
|
11
|
+
/** Builds an image def. `bounds` carries a placeholder size until the first `onScale`. */
|
|
20
12
|
createImageDef(source: ImageSource, bounds: Rect, options?: {
|
|
21
13
|
hidden?: boolean;
|
|
22
14
|
dim?: number;
|
|
23
15
|
interactive?: boolean;
|
|
24
16
|
origin?: [number, number];
|
|
17
|
+
/** Forwarded from {@link IconConfig.cardinalSnap}; the host attaches the rule. */
|
|
18
|
+
cardinalSnap?: boolean;
|
|
25
19
|
}): 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
|
-
*/
|
|
20
|
+
/** Builds a line def between two points. */
|
|
33
21
|
createLineDef(line: {
|
|
34
22
|
p0: Point2Like;
|
|
35
23
|
p1: Point2Like;
|
|
36
24
|
}, 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
|
-
*/
|
|
43
|
-
getRotation(newAngle: number, currentRotation: number): number | undefined;
|
|
44
25
|
/** The scene's root layers; the `wf-*` layers are resolved out of these by name. */
|
|
45
26
|
readonly layers: readonly LayerDef[];
|
|
46
27
|
/**
|
|
47
28
|
* Commits changed defs.
|
|
48
29
|
*
|
|
49
|
-
* Injected rather than imported
|
|
50
|
-
*
|
|
51
|
-
* @param defs - The defs that changed.
|
|
30
|
+
* Injected rather than imported: `@expofp/renderer` is a type-only peer here, so the concrete
|
|
31
|
+
* `update` comes from the host.
|
|
52
32
|
*/
|
|
53
33
|
update(...defs: RenderableDef[]): void;
|
|
54
34
|
}
|
|
@@ -80,13 +60,9 @@ export interface IconConfig {
|
|
|
80
60
|
*/
|
|
81
61
|
readonly origin?: [number, number];
|
|
82
62
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* (source/destination/transition) that should remain readable.
|
|
87
|
-
*
|
|
88
|
-
* Do NOT enable for icons whose `rotation` carries meaning (e.g. position
|
|
89
|
-
* arrow heading, kiosk heading) — the snap would override that meaning.
|
|
63
|
+
* Keeps the icon readable as the camera rolls; the host's `createImageDef` attaches the rule.
|
|
64
|
+
* The turn is relative — the icon holds its starting heading plus whole quarters — so do NOT
|
|
65
|
+
* enable it where `rotation` carries meaning (position arrow, kiosk heading).
|
|
90
66
|
*
|
|
91
67
|
* Set at first setIcon for an icon key; ignored on subsequent updates.
|
|
92
68
|
*/
|
|
@@ -138,29 +114,11 @@ export interface WayfindingRenderer {
|
|
|
138
114
|
readonly resetAnimation?: boolean;
|
|
139
115
|
}): void;
|
|
140
116
|
/**
|
|
141
|
-
*
|
|
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[];
|
|
151
|
-
/** Apply camera roll to cardinal-snap icons; returns changed defs to render. */
|
|
152
|
-
applyRoll(cameraAngle: number): RenderableDef[];
|
|
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.
|
|
117
|
+
* Fires the def's `onClick`; reports whether it was a clickable icon, so a caller can stop the
|
|
118
|
+
* click going any further.
|
|
157
119
|
*/
|
|
158
120
|
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
|
-
*/
|
|
121
|
+
/** Whether a def is a clickable icon, which is what a pointer cursor hangs off. */
|
|
164
122
|
handleHover(def: RenderableDef): boolean;
|
|
165
123
|
/** Clear route lines, drop trails, hide icons. Buffered — `flush()` to commit. */
|
|
166
124
|
clearRoute(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/wayfinding",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.32.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.
|
|
37
|
+
"@expofp/geometry": "3.32.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@expofp/renderer": "3.
|
|
40
|
+
"@expofp/renderer": "3.32.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@expofp/renderer": "3.
|
|
43
|
+
"@expofp/renderer": "3.32.0"
|
|
44
44
|
}
|
|
45
45
|
}
|