@expofp/renderer 3.44.8 → 3.45.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/Renderer.js +7 -1
- package/dist/controls/hooks.d.ts +2 -1
- package/dist/core/sprites/BatchedSprites.js +2 -34
- package/dist/core/text/MemberTexture.js +49 -8
- package/dist/events/LayoutDriver.d.ts +5 -0
- package/dist/events/LayoutDriver.js +136 -29
- package/dist/events/layout-targets.d.ts +45 -3
- package/dist/events/layout-targets.js +129 -9
- package/dist/events/placement/box.d.ts +59 -0
- package/dist/events/placement/box.js +96 -0
- package/dist/events/placement/grid.d.ts +161 -0
- package/dist/events/placement/grid.js +235 -0
- package/dist/events/placement/index.d.ts +2 -0
- package/dist/events/placement/index.js +1 -0
- package/dist/events/placement/pass.d.ts +70 -0
- package/dist/events/placement/pass.js +617 -0
- package/dist/objects/images/ImageInstance.js +1 -0
- package/dist/objects/images/Images.js +2 -2
- package/dist/objects/images/image-container.d.ts +12 -0
- package/dist/objects/images/image-container.js +20 -4
- package/dist/objects/materials/TextureMaterial.d.ts +11 -8
- package/dist/objects/materials/TextureMaterial.js +24 -17
- package/dist/objects/text/text-container.d.ts +16 -1
- package/dist/objects/text/text-container.js +23 -1
- package/dist/space/layout-measure.d.ts +12 -10
- package/dist/space/layout-measure.js +18 -16
- package/dist/types/defs.d.ts +157 -147
- package/dist/types/events.d.ts +5 -1
- package/dist/types/guards.d.ts +7 -0
- package/dist/types/guards.js +10 -0
- package/dist/utils/math.d.ts +18 -1
- package/dist/utils/math.js +35 -0
- 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 @@
|
|
|
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
|