@expofp/renderer 3.44.8 → 3.45.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.
Files changed (33) hide show
  1. package/dist/Renderer.js +7 -1
  2. package/dist/controls/hooks.d.ts +2 -1
  3. package/dist/core/sprites/BatchedSprites.js +2 -34
  4. package/dist/core/text/MemberTexture.js +49 -8
  5. package/dist/events/LayoutDriver.d.ts +5 -0
  6. package/dist/events/LayoutDriver.js +136 -29
  7. package/dist/events/layout-targets.d.ts +45 -3
  8. package/dist/events/layout-targets.js +129 -9
  9. package/dist/events/placement/box.d.ts +59 -0
  10. package/dist/events/placement/box.js +96 -0
  11. package/dist/events/placement/grid.d.ts +161 -0
  12. package/dist/events/placement/grid.js +235 -0
  13. package/dist/events/placement/index.d.ts +2 -0
  14. package/dist/events/placement/index.js +1 -0
  15. package/dist/events/placement/pass.d.ts +70 -0
  16. package/dist/events/placement/pass.js +617 -0
  17. package/dist/objects/images/ImageInstance.js +1 -0
  18. package/dist/objects/images/Images.js +2 -2
  19. package/dist/objects/images/image-container.d.ts +12 -0
  20. package/dist/objects/images/image-container.js +20 -4
  21. package/dist/objects/materials/TextureMaterial.d.ts +11 -8
  22. package/dist/objects/materials/TextureMaterial.js +24 -17
  23. package/dist/objects/text/text-container.d.ts +16 -1
  24. package/dist/objects/text/text-container.js +23 -1
  25. package/dist/space/layout-measure.d.ts +12 -10
  26. package/dist/space/layout-measure.js +18 -16
  27. package/dist/types/defs.d.ts +157 -147
  28. package/dist/types/events.d.ts +5 -1
  29. package/dist/types/guards.d.ts +7 -0
  30. package/dist/types/guards.js +10 -0
  31. package/dist/utils/math.d.ts +18 -1
  32. package/dist/utils/math.js +35 -0
  33. package/package.json +2 -2
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Whether a def in mode `a` is allowed to cover one already placed in mode `b`.
3
+ *
4
+ * Ported from maplibre's `overlapAllowed` in `src/symbol/grid_index.ts`. With two modes the rule
5
+ * reduces to `a === 'always'`, and this stays as the one place it is written.
6
+ *
7
+ * The `_b` parameter is unused: an `always` entry is never inserted into the grid, so the mode of
8
+ * the def already there is always `never`. Re-adding a third mode brings it back.
9
+ * @param a - The mode of the def being tested.
10
+ * @param _b - The mode of the def already in the grid. Unused, see above.
11
+ * @returns Whether the overlap is allowed.
12
+ */
13
+ export function overlapAllowed(a, _b) {
14
+ return a === 'always';
15
+ }
16
+ /**
17
+ * Whether two boxes overlap, by the rule {@link PlacementGrid.blockerOf} applies: edge contact is
18
+ * clear, so two boxes sharing a border do not overlap.
19
+ * @param a - One box.
20
+ * @param b - The other.
21
+ * @returns Whether they overlap.
22
+ */
23
+ export function overlaps(a, b) {
24
+ return a.minX < b.maxX && a.maxX > b.minX && a.minY < b.maxY && a.maxY > b.minY;
25
+ }
26
+ /**
27
+ * A uniform-cell index of screen rectangles, which answers which rectangle already in it, if any,
28
+ * blocks a new one.
29
+ *
30
+ * Modeled on maplibre's `GridIndex`, without the circle half. Circles exist there for labels that
31
+ * follow a curved line, which this renderer does not draw. Entries live in flat arrays that a reset
32
+ * keeps at their length and refills from the front under a count cursor: setting `length = 0`
33
+ * instead lets V8 trim the backing store, so every pass would grow it again through its doublings.
34
+ * The per-cell id lists are short and are still cleared with `length = 0`, but only the cells an
35
+ * entry was filed under: a pass resets the grid once per collision scope, and every registered
36
+ * layer is a scope, most of them with no participant at all. Clearing all 1820 cells of a 1440 by
37
+ * 900 canvas for each of imexamerica25-3d's 21 scopes was 0.9–1.2 ms of a 1.1–1.4 ms pass.
38
+ *
39
+ * The index covers the canvas plus {@link GRID_MARGIN_PX} on every side, so a def crossing the
40
+ * edge of the screen decides off screen, where the churn is invisible.
41
+ */
42
+ export class PlacementGrid {
43
+ /** Each cell's entry ids. Index is `y * xCells + x`. */
44
+ cells = [];
45
+ /** The index of every cell that holds an entry since the last reset, which is all it clears. */
46
+ filled = [];
47
+ /**
48
+ * Four numbers per entry: `minX`, `minY`, `maxX`, `maxY`. Only the first `count * 4` are live; the
49
+ * rest are the last pass's, kept so the next pass overwrites them in place.
50
+ */
51
+ boxes = [];
52
+ /** One mode per entry, parallel to {@link boxes} and live up to `count` in the same way. */
53
+ modes = [];
54
+ count = 0;
55
+ xCells = 0;
56
+ yCells = 0;
57
+ /** The canvas width the cells were built for. `-1` until the first `reset`, so it always rebuilds. */
58
+ width = -1;
59
+ /** The canvas height the cells were built for. `-1` until the first `reset`, so it always rebuilds. */
60
+ height = -1;
61
+ /**
62
+ * @param width - The canvas width in CSS pixels.
63
+ * @param height - The canvas height in CSS pixels.
64
+ */
65
+ constructor(width, height) {
66
+ this.reset(width, height);
67
+ }
68
+ /**
69
+ * Empties the index and sizes it for a canvas, which is what every pass starts with.
70
+ *
71
+ * The cell array is rebuilt only when the canvas size changed, so an unresized canvas pays only
72
+ * for clearing the cells the last fill used.
73
+ * @param width - The canvas width in CSS pixels.
74
+ * @param height - The canvas height in CSS pixels.
75
+ */
76
+ reset(width, height) {
77
+ this.count = 0;
78
+ for (const index of this.filled)
79
+ this.cells[index].length = 0;
80
+ this.filled.length = 0;
81
+ if (width !== this.width || height !== this.height) {
82
+ this.width = width;
83
+ this.height = height;
84
+ this.xCells = Math.max(1, Math.ceil((width + 2 * GRID_MARGIN_PX) / GRID_CELL_PX));
85
+ this.yCells = Math.max(1, Math.ceil((height + 2 * GRID_MARGIN_PX) / GRID_CELL_PX));
86
+ this.cells = [];
87
+ for (let i = 0; i < this.xCells * this.yCells; i++)
88
+ this.cells.push([]);
89
+ }
90
+ }
91
+ /**
92
+ * Whether the box intersects the indexed area, which is the canvas plus its margin. A box that
93
+ * only partly overlaps it passes; only one wholly outside fails.
94
+ *
95
+ * A box wholly outside it is not placed and not inserted. That is a correctness rule rather than a
96
+ * priority one: the index cannot hold it, so it cannot be tested against later defs either.
97
+ * @param box - The box to test.
98
+ * @returns Whether the index covers it.
99
+ */
100
+ intersects(box) {
101
+ return (box.maxX >= -GRID_MARGIN_PX &&
102
+ box.minX <= this.width + GRID_MARGIN_PX &&
103
+ box.maxY >= -GRID_MARGIN_PX &&
104
+ box.minY <= this.height + GRID_MARGIN_PX);
105
+ }
106
+ /**
107
+ * The earliest-inserted entry that blocks this box, or nothing when it is clear.
108
+ *
109
+ * **Earliest, not first found.** The cell walk reaches a box filed under a later cell before one
110
+ * filed under an earlier cell, so returning the first match would make the answer depend on cell
111
+ * iteration order — and the answer is load-bearing: in a clustering scope the blocker is the
112
+ * cluster's seed, and first come, first served means the earliest. Scanning every cell the box
113
+ * touches costs the early exit a boolean test could take, against a few hundred entries in a
114
+ * handful of cells.
115
+ *
116
+ * Edge contact does not block: two boxes sharing a border are treated as clear, so a row of
117
+ * abutting labels is placeable.
118
+ *
119
+ * Test {@link intersects} first: a box wholly outside the indexed area is clamped into an edge cell,
120
+ * which is safe only because stored coordinates are then compared exactly.
121
+ * @param box - The box to test.
122
+ * @param overlap - How the def being tested competes.
123
+ * @returns The blocking entry's id, as {@link insert} returned it, or `undefined` when clear.
124
+ */
125
+ blockerOf(box, overlap) {
126
+ if (overlap === 'always')
127
+ return undefined;
128
+ const x0 = this.cellX(box.minX);
129
+ const x1 = this.cellX(box.maxX);
130
+ const y0 = this.cellY(box.minY);
131
+ const y1 = this.cellY(box.maxY);
132
+ // Ids are assigned in insert order, so the earliest entry is the lowest id.
133
+ let blocker = -1;
134
+ for (let y = y0; y <= y1; y++) {
135
+ for (let x = x0; x <= x1; x++) {
136
+ for (const id of this.cells[y * this.xCells + x]) {
137
+ if (blocker !== -1 && id >= blocker)
138
+ continue;
139
+ if (overlapAllowed(overlap, this.modes[id]))
140
+ continue;
141
+ const at = id * 4;
142
+ if (box.minX < this.boxes[at + 2] &&
143
+ box.maxX > this.boxes[at] &&
144
+ box.minY < this.boxes[at + 3] &&
145
+ box.maxY > this.boxes[at + 1]) {
146
+ blocker = id;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ return blocker === -1 ? undefined : blocker;
152
+ }
153
+ /**
154
+ * Adds a box, so that later tests see it.
155
+ *
156
+ * Separate from {@link blockerOf} because asking whether a box is blocked and claiming its room are
157
+ * different questions: a group tests its padded union and inserts its plain one, and maplibre
158
+ * separates `placeCollisionBox` from `insertCollisionBox` for the same reason.
159
+ *
160
+ * Only insert a box that {@link intersects} accepted: one wholly outside the indexed area would be
161
+ * clamped into an edge cell, and relies on the exact comparison in {@link blockerOf} to stay
162
+ * harmless.
163
+ * @param box - The box to add.
164
+ * @param overlap - How the def competes.
165
+ * @returns The entry's id: the number of entries inserted before it since the last reset.
166
+ */
167
+ insert(box, overlap) {
168
+ const id = this.count++;
169
+ const at = id * 4;
170
+ this.boxes[at] = box.minX;
171
+ this.boxes[at + 1] = box.minY;
172
+ this.boxes[at + 2] = box.maxX;
173
+ this.boxes[at + 3] = box.maxY;
174
+ this.modes[id] = overlap;
175
+ const x0 = this.cellX(box.minX);
176
+ const x1 = this.cellX(box.maxX);
177
+ const y0 = this.cellY(box.minY);
178
+ const y1 = this.cellY(box.maxY);
179
+ for (let y = y0; y <= y1; y++) {
180
+ for (let x = x0; x <= x1; x++) {
181
+ const index = y * this.xCells + x;
182
+ const cell = this.cells[index];
183
+ if (cell.length === 0)
184
+ this.filled.push(index);
185
+ cell.push(id);
186
+ }
187
+ }
188
+ return id;
189
+ }
190
+ /**
191
+ * Reads back the box an entry was inserted with.
192
+ * @param id - The entry's id, as {@link insert} returned it since the last reset.
193
+ * @param out - The box to write into.
194
+ * @returns `out`.
195
+ */
196
+ boxOf(id, out) {
197
+ const at = id * 4;
198
+ out.minX = this.boxes[at];
199
+ out.minY = this.boxes[at + 1];
200
+ out.maxX = this.boxes[at + 2];
201
+ out.maxY = this.boxes[at + 3];
202
+ return out;
203
+ }
204
+ /**
205
+ * Which cell column a CSS x lands in, clamped into the index.
206
+ * @param x - The coordinate.
207
+ * @returns The column.
208
+ */
209
+ cellX(x) {
210
+ const cell = Math.floor((x + GRID_MARGIN_PX) / GRID_CELL_PX);
211
+ return Math.min(Math.max(cell, 0), this.xCells - 1);
212
+ }
213
+ /**
214
+ * Which cell row a CSS y lands in, clamped into the index.
215
+ * @param y - The coordinate.
216
+ * @returns The row.
217
+ */
218
+ cellY(y) {
219
+ const cell = Math.floor((y + GRID_MARGIN_PX) / GRID_CELL_PX);
220
+ return Math.min(Math.max(cell, 0), this.yCells - 1);
221
+ }
222
+ }
223
+ /**
224
+ * How wide one cell is, in CSS pixels. More cells means fewer boxes to compare per cell, and more
225
+ * cells for one box to be filed under. Maplibre uses 25 against symbols a few pixels across; our
226
+ * pins are 32, so one pin usually touches one cell.
227
+ */
228
+ export const GRID_CELL_PX = 32;
229
+ /**
230
+ * How far past the canvas the index reaches, in CSS pixels.
231
+ *
232
+ * Maplibre's `viewportPadding`, for its reason: a def crossing the edge of the screen decides off
233
+ * screen, so the flicker that a crossing causes happens where nobody sees it.
234
+ */
235
+ export const GRID_MARGIN_PX = 100;
@@ -0,0 +1,2 @@
1
+ export { createPlacement, type Placement, type PlacementContext } from './pass';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export { createPlacement } from './pass';
@@ -0,0 +1,70 @@
1
+ import { type Camera } from 'three';
2
+ import { type Space } from '../../space/Space';
3
+ import { type Cluster, type ClusterOptions, type ImageDef, type LayerDef, type RenderableDef, type TextDef } from '../../types/defs';
4
+ /** What one placement pass needs to know about the frame it runs for. */
5
+ export interface PlacementContext {
6
+ /** The scene's space, for the projection. */
7
+ space: Space;
8
+ /** The camera in use. */
9
+ camera: Camera;
10
+ /** Plan units per CSS pixel. */
11
+ pixelSize: number;
12
+ /** The canvas width in CSS pixels. */
13
+ width: number;
14
+ /** The canvas height in CSS pixels. */
15
+ height: number;
16
+ }
17
+ /**
18
+ * The renderer's placement pass: which screen-space defs win room on screen, and which stand down.
19
+ *
20
+ * One object per renderer, holding the grid and the verdict each def was last given. A pass is
21
+ * synchronous and complete, so there is no partial index for anything to read: the cost of that is
22
+ * paid by running the pass at most a few times a second rather than every frame, which is the
23
+ * driver's business rather than this object's.
24
+ */
25
+ export interface Placement {
26
+ /**
27
+ * Runs a whole pass and reports the defs whose verdict changed, and the clusters of each scope
28
+ * whose set changed.
29
+ *
30
+ * The registered arrays are bucketed into **collision scopes** first: one per
31
+ * {@link LayerDef.collisionGroup} key, and one per layer that names none. A def competes for room
32
+ * with the defs of its own scope and with nothing else, so each scope starts from an empty grid.
33
+ *
34
+ * Order is the argument's own iteration and nothing else: the scopes in the order their first array
35
+ * was registered, within a scope its arrays in registration order, and each array in the order
36
+ * its defs were authored. First come, first served, as maplibre does it, so a caller ranks its defs
37
+ * by ordering its layers and its children.
38
+ *
39
+ * A scope where any layer declares {@link LayerDef.clustering} **clusters**: a participant that
40
+ * collides hides together with the one it collided with, which becomes the seed of a cluster that
41
+ * later colliders join. A seed's verdict is therefore revised after it was decided, so nothing is
42
+ * reported during the walk. Once a scope's walk ends, each cluster's marker box absorbs the
43
+ * placed units it overlaps and merges with the clusters whose marker boxes it overlaps, until
44
+ * nothing changes (see {@link ClusterOptions.marker}). Only then are its flips reported, first,
45
+ * in walk order, and then its cluster set, to every clustering layer in it.
46
+ * @param entries - Each registered def array with the layer that registered it, in registration
47
+ * order. The registry's own map entries, so calling this allocates nothing.
48
+ * @param context - What the camera shows this pass.
49
+ * @param onFlip - Called for each def whose verdict differs from the one it last had.
50
+ * @param onCluster - Called once per clustering layer of a scope whose cluster set differs from
51
+ * the one last reported for it, with a fresh array each time; also with the current set for a
52
+ * clustering layer that joined a scope after its last report, and with an empty array for each
53
+ * layer that was told of clusters and has since left: its scope is no longer registered, the
54
+ * layer is no longer in it, or it no longer declares `clustering`. The third argument is the
55
+ * {@link ClusterOptions} the layer was told under, which is the one to call even where the
56
+ * layer has dropped it since.
57
+ */
58
+ run(entries: Iterable<readonly [readonly RenderableDef[], LayerDef]>, context: PlacementContext, onFlip: (def: ImageDef | TextDef, placed: boolean) => void, onCluster?: (layer: LayerDef, clusters: readonly Cluster[], clustering: ClusterOptions) => void): void;
59
+ /**
60
+ * Drops a def's verdict, so that registering the same object again reports its verdict afresh.
61
+ * @param def - The def to forget.
62
+ */
63
+ forget(def: RenderableDef): void;
64
+ }
65
+ /**
66
+ * Builds a renderer's placement pass.
67
+ * @returns The pass.
68
+ */
69
+ export declare function createPlacement(): Placement;
70
+ //# sourceMappingURL=pass.d.ts.map