@expofp/wayfinding 3.26.0 → 3.27.1
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/dist/createWayfinding.d.ts +2 -2
- package/dist/createWayfinding.js +11 -2
- package/dist/renderer/createWayfindingRenderer.d.ts +7 -6
- package/dist/renderer/createWayfindingRenderer.js +17 -13
- package/dist/renderer/iconManager.d.ts +25 -10
- package/dist/renderer/iconManager.js +100 -49
- package/dist/renderer/layerManager.d.ts +22 -11
- package/dist/renderer/layerManager.js +13 -6
- package/dist/renderer/lineAnimation.d.ts +19 -5
- package/dist/renderer/lineAnimation.js +94 -33
- package/dist/renderer/routeLineManager.d.ts +5 -4
- package/dist/renderer/routeLineManager.js +64 -22
- package/dist/renderer/trailManager.d.ts +9 -7
- package/dist/renderer/trailManager.js +49 -12
- package/dist/renderer/types.d.ts +62 -19
- package/package.json +4 -4
|
@@ -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[]):
|
|
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
|
/**
|
package/dist/createWayfinding.js
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
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 `
|
|
8
|
-
*
|
|
9
|
-
* @
|
|
10
|
-
* @param
|
|
11
|
-
* @
|
|
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(
|
|
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 `
|
|
16
|
-
*
|
|
17
|
-
* @
|
|
18
|
-
* @param
|
|
19
|
-
* @
|
|
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(
|
|
22
|
+
export function createWayfindingRenderer(port, config = {}) {
|
|
22
23
|
const colors = { ...DEFAULT_COLORS, ...config.colors };
|
|
23
|
-
const layerManager = createLayerManager(
|
|
24
|
-
const iconManager = createIconManager(layerManager,
|
|
25
|
-
const trailManager = createTrailManager(layerManager,
|
|
26
|
-
const routeLineManager = createRouteLineManager(layerManager, colors,
|
|
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: (
|
|
33
|
+
applyScale: (pixelSize) => [
|
|
34
|
+
...iconManager.applyScale(pixelSize),
|
|
35
|
+
...trailManager.applyScale(pixelSize),
|
|
36
|
+
],
|
|
33
37
|
applyRoll: (cameraAngle) => iconManager.applyRoll(cameraAngle),
|
|
34
|
-
handleClick: (
|
|
35
|
-
handleHover: (
|
|
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
|
-
/**
|
|
24
|
-
applyScale(
|
|
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
|
|
28
|
-
handleClick(
|
|
29
|
-
/** Whether a
|
|
30
|
-
handleHover(
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
140
|
-
const delta =
|
|
194
|
+
const bounds = record.imageDef.bounds;
|
|
195
|
+
const delta = port.getRotation(cameraAngle, bounds.rotation);
|
|
141
196
|
if (delta === undefined)
|
|
142
197
|
continue;
|
|
143
|
-
|
|
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(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
7
|
-
*
|
|
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
|
|
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
|
|
20
|
+
/** Resolve a layer and mark its membership as dirty for the next {@link flush}. */
|
|
16
21
|
touchLayer(name: string): LayerDef;
|
|
17
|
-
/**
|
|
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
|
|
29
|
+
/** Clear caches and dirty sets. */
|
|
20
30
|
destroy(): void;
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
|
-
* Create a {@link LayerManager}
|
|
24
|
-
* @param
|
|
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(
|
|
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}
|
|
3
|
-
* @param
|
|
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(
|
|
6
|
-
const allLayers =
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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 (!
|
|
27
|
+
if (!segments.length)
|
|
7
28
|
return;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
57
|
-
// A detached canvas (host element removed from the DOM on SPA navigation)
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
if (!(Number.isFinite(
|
|
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(
|
|
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
|
|
69
|
-
// whatever
|
|
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 >=
|
|
137
|
+
if (currentSegmentIndex >= segments.length) {
|
|
75
138
|
currentSegmentIndex = 0;
|
|
76
139
|
}
|
|
77
140
|
}
|
|
78
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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) =>
|
|
28
|
-
const remainingDefs = remaining.map((seg) =>
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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, (
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
/**
|
|
21
|
-
applyScale(
|
|
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
|
-
* @
|
|
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,
|
|
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
|
|
4
|
-
const
|
|
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
|
-
* @
|
|
8
|
+
* @param layerManager - Where the trails are placed and dirt is tracked.
|
|
9
|
+
* @returns The manager.
|
|
10
10
|
*/
|
|
11
|
-
export function createTrailManager(layerManager,
|
|
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
|
|
53
|
+
function redraw(record) {
|
|
22
54
|
const layer = layerManager.touchLayer(record.layerName);
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
|
100
|
+
redraw(record);
|
|
65
101
|
},
|
|
66
|
-
applyScale(
|
|
102
|
+
applyScale(pixelSize) {
|
|
103
|
+
currentPixelSize = pixelSize;
|
|
67
104
|
const layers = new Set();
|
|
68
105
|
for (const record of trails.values()) {
|
|
69
|
-
redraw(record
|
|
106
|
+
redraw(record);
|
|
70
107
|
layers.add(layerManager.touchLayer(record.layerName));
|
|
71
108
|
}
|
|
72
109
|
return [...layers];
|
package/dist/renderer/types.d.ts
CHANGED
|
@@ -1,29 +1,55 @@
|
|
|
1
1
|
import type { Point2Like, Rect } from '@expofp/geometry';
|
|
2
|
-
import type { ImageDef, ImageSource, LayerDef, LineDef,
|
|
2
|
+
import type { ImageDef, ImageSource, LayerDef, LineDef, RenderableDef } from '@expofp/renderer';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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?:
|
|
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
|
-
/**
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
/**
|
|
115
|
-
|
|
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
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "3.27.1",
|
|
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.27.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@expofp/renderer": "
|
|
40
|
+
"@expofp/renderer": "3.27.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@expofp/renderer": "
|
|
43
|
+
"@expofp/renderer": "3.27.1"
|
|
44
44
|
}
|
|
45
45
|
}
|