@expofp/renderer 3.44.7 → 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.
- 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/lighting/Lighting.js +5 -6
- package/dist/objects/materials/TextureMaterial.d.ts +11 -8
- package/dist/objects/materials/TextureMaterial.js +24 -17
- package/dist/objects/materials/effects/ao/index.d.ts +1 -1
- package/dist/objects/materials/effects/ao/index.js +3 -3
- package/dist/objects/materials/effects/contact-shadow.js +2 -2
- 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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createContext, use, useEffect } from 'react';
|
|
2
|
-
import { isLayerDef } from '../types/guards';
|
|
3
|
-
import { writeChanged } from '../update';
|
|
2
|
+
import { isImageDef, isLayerDef, isPlacementTarget } from '../types/guards';
|
|
3
|
+
import { subscribeDef, writeChanged } from '../update';
|
|
4
4
|
import { createLogger } from '../utils/logging';
|
|
5
5
|
import { culledBy } from './def-culling';
|
|
6
|
+
import { createPlacement } from './placement';
|
|
6
7
|
const logger = createLogger('layout');
|
|
7
8
|
/**
|
|
8
9
|
* The renderer's re-layout registry. Absent outside a renderer, where a layer simply registers
|
|
@@ -11,7 +12,8 @@ const logger = createLogger('layout');
|
|
|
11
12
|
export const LayoutTargetsContext = createContext(undefined);
|
|
12
13
|
/**
|
|
13
14
|
* Registers a layer's re-layout targets for as long as the layer is mounted: the defs it holds that
|
|
14
|
-
* declare an `onScale`, and the layer itself when it declares
|
|
15
|
+
* declare an `onScale`, an `onRoll` or a `placement`, and the layer itself when it declares a
|
|
16
|
+
* layout rule.
|
|
15
17
|
*
|
|
16
18
|
* A child *layer* is never registered here even when it declares a handler — it registers itself, and
|
|
17
19
|
* it has to: a handler assigned to a layer after mount re-renders that layer alone, so a registration
|
|
@@ -34,12 +36,13 @@ export function useLayoutTargets(layer, children) {
|
|
|
34
36
|
useEffect(() => {
|
|
35
37
|
if (!targets)
|
|
36
38
|
return undefined;
|
|
37
|
-
const defs = children.filter((def) => (def.onScale !== undefined || def.onRoll !== undefined
|
|
39
|
+
const defs = children.filter((def) => (def.onScale !== undefined || def.onRoll !== undefined || declaresPlacement(def)) &&
|
|
40
|
+
!isLayerDef(def));
|
|
38
41
|
if (layer.onScale || layer.onRoll)
|
|
39
42
|
defs.push(layer);
|
|
40
43
|
if (!defs.length)
|
|
41
44
|
return undefined;
|
|
42
|
-
return targets.register(defs);
|
|
45
|
+
return targets.register(layer, defs);
|
|
43
46
|
}, [targets, layer, children]);
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
@@ -50,7 +53,16 @@ export function useLayoutTargets(layer, children) {
|
|
|
50
53
|
* @returns The registry.
|
|
51
54
|
*/
|
|
52
55
|
export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
53
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Every registered def array, with the layer that registered it. Map entries are exactly what
|
|
58
|
+
* the placement pass takes, so a pass is handed the registry itself.
|
|
59
|
+
*/
|
|
60
|
+
const groups = new Map();
|
|
61
|
+
const placement = createPlacement();
|
|
62
|
+
/** Whether a participant registered since the last pass, so one is owed. */
|
|
63
|
+
let placementDirty = false;
|
|
64
|
+
/** Whether a participant's box moved since the last pass, so one is owed. */
|
|
65
|
+
let placementDrifted = false;
|
|
54
66
|
/**
|
|
55
67
|
* The pixel size each def was last laid out at — the registry's layout state, because the rule
|
|
56
68
|
* that lays a def out is the def's own. Keyed by def rather than by group, so a membership change
|
|
@@ -160,7 +172,7 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
160
172
|
const start = performance.now();
|
|
161
173
|
const before = flipped.size + queued.size + deferred.size;
|
|
162
174
|
let ran = 0;
|
|
163
|
-
for (const defs of groups) {
|
|
175
|
+
for (const defs of groups.keys()) {
|
|
164
176
|
for (const def of defs) {
|
|
165
177
|
if (!def[handler])
|
|
166
178
|
continue;
|
|
@@ -186,8 +198,14 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
186
198
|
get hasWork() {
|
|
187
199
|
return flipped.size > 0 || queued.size > 0 || deferred.size > 0;
|
|
188
200
|
},
|
|
189
|
-
|
|
190
|
-
|
|
201
|
+
get needsPlacement() {
|
|
202
|
+
return placementDirty;
|
|
203
|
+
},
|
|
204
|
+
get placementDrifted() {
|
|
205
|
+
return placementDrifted;
|
|
206
|
+
},
|
|
207
|
+
register(layer, defs) {
|
|
208
|
+
groups.set(defs, layer);
|
|
191
209
|
let wrote = false;
|
|
192
210
|
for (const def of defs) {
|
|
193
211
|
if (!drawnHidden.has(def))
|
|
@@ -200,18 +218,54 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
200
218
|
wrote = true;
|
|
201
219
|
});
|
|
202
220
|
}
|
|
221
|
+
// A verdict cannot be computed for one def alone, because it depends on every other box, and
|
|
222
|
+
// the registry holds no camera. So registration only marks a pass owed; the frame the
|
|
223
|
+
// `invalidate` below asks for is what runs it, and it runs before that frame draws.
|
|
224
|
+
//
|
|
225
|
+
// Owed on any `placement`, not only on a participant: a flat def that carries one is exactly
|
|
226
|
+
// the mistake the pass's development warning exists to catch, and the pass has to run to say so.
|
|
227
|
+
if (defs.some(declaresPlacement)) {
|
|
228
|
+
placementDirty = true;
|
|
229
|
+
wrote = true;
|
|
230
|
+
}
|
|
203
231
|
// Registration happens in an effect rather than inside the frame loop, so anything written here
|
|
204
232
|
// needs a frame asking for it.
|
|
205
233
|
if (wrote)
|
|
206
234
|
invalidate();
|
|
235
|
+
// A participant that moves owes a pass as one that joins does: its verdict, and those of the
|
|
236
|
+
// defs it used to block or be blocked by, were earned at the old position. Only participants
|
|
237
|
+
// are watched, so a write to anything else costs nothing, and only a change to what the pass
|
|
238
|
+
// measures owes one: an `update()` that moved no box, such as a fade writing `opacity` on every
|
|
239
|
+
// animation frame, would otherwise run the pass at frame rate on a still camera, past the rate
|
|
240
|
+
// gate that is there for the camera. The registry's own writes (`onPlace` returns, which go
|
|
241
|
+
// through `syncNow`) never notify, so a verdict flip cannot owe the pass that would flip it
|
|
242
|
+
// again.
|
|
243
|
+
const unsubscribes = defs.filter(isPlacementTarget).map((def) => {
|
|
244
|
+
const seen = measuredBy(def);
|
|
245
|
+
return subscribeDef(def, () => {
|
|
246
|
+
if (!remeasure(def, seen))
|
|
247
|
+
return;
|
|
248
|
+
placementDrifted = true;
|
|
249
|
+
invalidate();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
207
252
|
return () => {
|
|
253
|
+
for (const unsubscribe of unsubscribes)
|
|
254
|
+
unsubscribe();
|
|
208
255
|
groups.delete(defs);
|
|
256
|
+
// A participant that leaves frees its room, so the defs it beat may now fit: owe a pass, and
|
|
257
|
+
// ask for the frame that runs it, as registration does.
|
|
258
|
+
if (defs.some(isPlacementTarget)) {
|
|
259
|
+
placementDirty = true;
|
|
260
|
+
invalidate();
|
|
261
|
+
}
|
|
209
262
|
// A queued def whose layer is gone must not be written from the backlog: its instances are
|
|
210
263
|
// unmounting.
|
|
211
264
|
for (const def of defs) {
|
|
212
265
|
flipped.delete(def);
|
|
213
266
|
queued.delete(def);
|
|
214
267
|
deferred.delete(def);
|
|
268
|
+
placement.forget(def);
|
|
215
269
|
}
|
|
216
270
|
};
|
|
217
271
|
},
|
|
@@ -225,6 +279,23 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
225
279
|
view.roll = nextRoll;
|
|
226
280
|
runHandlers(`roll ${+nextRoll.toFixed(3)}`, 'onRoll');
|
|
227
281
|
},
|
|
282
|
+
applyPlacement(context) {
|
|
283
|
+
placementDirty = false;
|
|
284
|
+
placementDrifted = false;
|
|
285
|
+
// Flips and clusters are reported in one synchronous call, flips first, so one drain writes
|
|
286
|
+
// a seed's hiding and the cluster standing for it together.
|
|
287
|
+
placement.run(groups, context, (def, placed) => {
|
|
288
|
+
const changed = def.placement?.onPlace(placed, view);
|
|
289
|
+
if (changed?.length)
|
|
290
|
+
enqueue(changed);
|
|
291
|
+
}, (_layer, clusters, clustering) => {
|
|
292
|
+
// The options the pass hands over, not the layer's current ones: a layer that dropped
|
|
293
|
+
// `clustering` is still owed the empty set through the handler that drew its clusters.
|
|
294
|
+
const changed = clustering.onCluster(clusters, view);
|
|
295
|
+
if (changed?.length)
|
|
296
|
+
enqueue(changed);
|
|
297
|
+
});
|
|
298
|
+
},
|
|
228
299
|
drain(frustum, budgetMs = -1) {
|
|
229
300
|
if (flipped.size === 0 && queued.size === 0 && deferred.size === 0)
|
|
230
301
|
return false;
|
|
@@ -275,3 +346,52 @@ export function createLayoutTargets(invalidate, pixelSize, roll = 0) {
|
|
|
275
346
|
},
|
|
276
347
|
};
|
|
277
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Whether a def declares a placement, participating in the pass or not.
|
|
351
|
+
* @param def - The def to test.
|
|
352
|
+
* @returns Whether it carries `placement`.
|
|
353
|
+
*/
|
|
354
|
+
function declaresPlacement(def) {
|
|
355
|
+
return def.placement !== undefined;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Reads what the pass measures of a participant: the box its bounds, origin and padding describe.
|
|
359
|
+
* @param def - The participant.
|
|
360
|
+
* @returns The reading.
|
|
361
|
+
*/
|
|
362
|
+
function measuredBy(def) {
|
|
363
|
+
const measured = { numbers: new Float64Array(9), padding: undefined };
|
|
364
|
+
remeasure(def, measured);
|
|
365
|
+
// The first reading is the baseline, not a change.
|
|
366
|
+
return measured;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Re-reads a participant into an earlier reading and says whether anything the pass measures moved.
|
|
370
|
+
* @param def - The participant.
|
|
371
|
+
* @param into - The reading to compare against and overwrite.
|
|
372
|
+
* @returns Whether the reading changed.
|
|
373
|
+
*/
|
|
374
|
+
function remeasure(def, into) {
|
|
375
|
+
const { bounds } = def;
|
|
376
|
+
const origin = isImageDef(def) ? def.origin : undefined;
|
|
377
|
+
const padding = def.placement?.padding;
|
|
378
|
+
const next = _measure;
|
|
379
|
+
next[0] = bounds.center.x;
|
|
380
|
+
next[1] = bounds.center.y;
|
|
381
|
+
next[2] = bounds.size.x;
|
|
382
|
+
next[3] = bounds.size.y;
|
|
383
|
+
next[4] = bounds.rotation;
|
|
384
|
+
next[5] = bounds.elevation;
|
|
385
|
+
next[6] = origin?.[0] ?? 0.5;
|
|
386
|
+
next[7] = origin?.[1] ?? 0.5;
|
|
387
|
+
let changed = padding !== into.padding;
|
|
388
|
+
for (let i = 0; i < 8; i++) {
|
|
389
|
+
if (next[i] !== into.numbers[i])
|
|
390
|
+
changed = true;
|
|
391
|
+
into.numbers[i] = next[i];
|
|
392
|
+
}
|
|
393
|
+
into.padding = padding;
|
|
394
|
+
return changed;
|
|
395
|
+
}
|
|
396
|
+
// Scratch for the reading, filled and compared within one call.
|
|
397
|
+
const _measure = new Float64Array(9);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Camera } from 'three';
|
|
2
|
+
import { type Space } from '../../space/Space';
|
|
3
|
+
import { type ImageDef, type TextDef } from '../../types/defs';
|
|
4
|
+
import { type PlacementBox } from './grid';
|
|
5
|
+
/**
|
|
6
|
+
* The screen rectangle one def occupies, in CSS pixels from the canvas's top-left corner.
|
|
7
|
+
*
|
|
8
|
+
* There is one path, because only a screen-space def is ever tested. Its anchor projects through
|
|
9
|
+
* the camera, and its extent comes from the pixel size rather than from the projection: constant
|
|
10
|
+
* pixel size means a sprite is the same number of pixels across at every depth, so the only
|
|
11
|
+
* camera-dependent term is where the anchor lands.
|
|
12
|
+
*
|
|
13
|
+
* `origin` shifts the rectangle relative to the anchor exactly as it shifts the quad, in the
|
|
14
|
+
* plan's y-down sense, which is also the canvas's. A bottom-center origin therefore puts the
|
|
15
|
+
* anchor on the rectangle's bottom edge and the rectangle above it.
|
|
16
|
+
*
|
|
17
|
+
* This is the def's own extent, without its `placement.padding`: that is what the grid stores, and
|
|
18
|
+
* {@link paddedBox} grows it into the box that is tested. Storing the grown box as well would make
|
|
19
|
+
* two padded defs keep twice the padding apart.
|
|
20
|
+
* @param def - The def to measure.
|
|
21
|
+
* @param space - The scene's space, for the projection.
|
|
22
|
+
* @param camera - The camera in use.
|
|
23
|
+
* @param pixelSize - Plan units per CSS pixel.
|
|
24
|
+
* @param out - The box to write into, reused across a pass.
|
|
25
|
+
* @returns The box, or nothing when the def has no extent or its anchor does not project.
|
|
26
|
+
*/
|
|
27
|
+
export declare function screenBoxOf(def: ImageDef | TextDef, space: Space, camera: Camera, pixelSize: number, out: PlacementBox): PlacementBox | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The screen rectangle a constant-pixel-size sprite occupies when its anchor stands at a plan
|
|
30
|
+
* point, in CSS pixels from the canvas's top-left corner.
|
|
31
|
+
*
|
|
32
|
+
* What {@link screenBoxOf} measures a def with, taken apart so that a box can stand at an anchor
|
|
33
|
+
* that is not a def's own: a cluster's marker stands at the mean of its units' anchors.
|
|
34
|
+
* @param x - The anchor's plan x.
|
|
35
|
+
* @param y - The anchor's plan y.
|
|
36
|
+
* @param elevation - The anchor's elevation, in plan units.
|
|
37
|
+
* @param width - The sprite's width in CSS pixels.
|
|
38
|
+
* @param height - The sprite's height in CSS pixels.
|
|
39
|
+
* @param origin - Which point of the sprite stands on the anchor, centered when omitted.
|
|
40
|
+
* @param rotation - Its spin about the screen normal, in radians.
|
|
41
|
+
* @param space - The scene's space, for the projection.
|
|
42
|
+
* @param camera - The camera in use.
|
|
43
|
+
* @param out - The box to write into.
|
|
44
|
+
* @returns The box, or nothing when the sprite has no extent or its anchor does not project.
|
|
45
|
+
*/
|
|
46
|
+
export declare function spriteBoxAt(x: number, y: number, elevation: number, width: number, height: number, origin: readonly [number, number] | undefined, rotation: number, space: Space, camera: Camera, out: PlacementBox): PlacementBox | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The box a def is tested with: its extent grown by `placement.padding`, by one number on all four
|
|
49
|
+
* sides or by a `[horizontal, vertical]` pair.
|
|
50
|
+
*
|
|
51
|
+
* The grid stores the extent and tests this, so each def's padding applies once, on the side that is
|
|
52
|
+
* testing. A def with no padding gets a copy of its extent.
|
|
53
|
+
* @param def - The def whose padding grows the box.
|
|
54
|
+
* @param box - The def's extent, from {@link screenBoxOf}.
|
|
55
|
+
* @param out - The box to write into, which may be `box` itself.
|
|
56
|
+
* @returns `out`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function paddedBox(def: ImageDef | TextDef, box: PlacementBox, out: PlacementBox): PlacementBox;
|
|
59
|
+
//# sourceMappingURL=box.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Box, Point, pointRotate, Rect, rectBounds } from '@expofp/geometry';
|
|
2
|
+
import { isImageDef } from '../../types/guards';
|
|
3
|
+
/**
|
|
4
|
+
* The screen rectangle one def occupies, in CSS pixels from the canvas's top-left corner.
|
|
5
|
+
*
|
|
6
|
+
* There is one path, because only a screen-space def is ever tested. Its anchor projects through
|
|
7
|
+
* the camera, and its extent comes from the pixel size rather than from the projection: constant
|
|
8
|
+
* pixel size means a sprite is the same number of pixels across at every depth, so the only
|
|
9
|
+
* camera-dependent term is where the anchor lands.
|
|
10
|
+
*
|
|
11
|
+
* `origin` shifts the rectangle relative to the anchor exactly as it shifts the quad, in the
|
|
12
|
+
* plan's y-down sense, which is also the canvas's. A bottom-center origin therefore puts the
|
|
13
|
+
* anchor on the rectangle's bottom edge and the rectangle above it.
|
|
14
|
+
*
|
|
15
|
+
* This is the def's own extent, without its `placement.padding`: that is what the grid stores, and
|
|
16
|
+
* {@link paddedBox} grows it into the box that is tested. Storing the grown box as well would make
|
|
17
|
+
* two padded defs keep twice the padding apart.
|
|
18
|
+
* @param def - The def to measure.
|
|
19
|
+
* @param space - The scene's space, for the projection.
|
|
20
|
+
* @param camera - The camera in use.
|
|
21
|
+
* @param pixelSize - Plan units per CSS pixel.
|
|
22
|
+
* @param out - The box to write into, reused across a pass.
|
|
23
|
+
* @returns The box, or nothing when the def has no extent or its anchor does not project.
|
|
24
|
+
*/
|
|
25
|
+
export function screenBoxOf(def, space, camera, pixelSize, out) {
|
|
26
|
+
const scale = pixelSize;
|
|
27
|
+
if (!Number.isFinite(scale) || scale <= 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
const bounds = def.bounds;
|
|
30
|
+
const origin = isImageDef(def) ? def.origin : undefined;
|
|
31
|
+
return spriteBoxAt(bounds.center.x, bounds.center.y, bounds.elevation, bounds.size.x / scale, bounds.size.y / scale, origin, bounds.rotation, space, camera, out);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The screen rectangle a constant-pixel-size sprite occupies when its anchor stands at a plan
|
|
35
|
+
* point, in CSS pixels from the canvas's top-left corner.
|
|
36
|
+
*
|
|
37
|
+
* What {@link screenBoxOf} measures a def with, taken apart so that a box can stand at an anchor
|
|
38
|
+
* that is not a def's own: a cluster's marker stands at the mean of its units' anchors.
|
|
39
|
+
* @param x - The anchor's plan x.
|
|
40
|
+
* @param y - The anchor's plan y.
|
|
41
|
+
* @param elevation - The anchor's elevation, in plan units.
|
|
42
|
+
* @param width - The sprite's width in CSS pixels.
|
|
43
|
+
* @param height - The sprite's height in CSS pixels.
|
|
44
|
+
* @param origin - Which point of the sprite stands on the anchor, centered when omitted.
|
|
45
|
+
* @param rotation - Its spin about the screen normal, in radians.
|
|
46
|
+
* @param space - The scene's space, for the projection.
|
|
47
|
+
* @param camera - The camera in use.
|
|
48
|
+
* @param out - The box to write into.
|
|
49
|
+
* @returns The box, or nothing when the sprite has no extent or its anchor does not project.
|
|
50
|
+
*/
|
|
51
|
+
export function spriteBoxAt(x, y, elevation, width, height, origin, rotation, space, camera, out) {
|
|
52
|
+
if (!(width > 0) || !(height > 0))
|
|
53
|
+
return undefined;
|
|
54
|
+
const screen = space.planToCanvas(_anchor.set(x, y, elevation), camera, _screen);
|
|
55
|
+
if (!screen || !Number.isFinite(screen.x) || !Number.isFinite(screen.y))
|
|
56
|
+
return undefined;
|
|
57
|
+
// The sprite's center sits off the anchor by `origin`, and turns with the sprite about it.
|
|
58
|
+
_offset.set((0.5 - (origin?.[0] ?? 0.5)) * width, (0.5 - (origin?.[1] ?? 0.5)) * height);
|
|
59
|
+
pointRotate(_offset, rotation, ZERO, _offset);
|
|
60
|
+
_rect.set(screen.x + _offset.x, screen.y + _offset.y, width, height, rotation, 0);
|
|
61
|
+
const { min, max } = rectBounds(_rect, _bounds);
|
|
62
|
+
out.minX = min.x;
|
|
63
|
+
out.maxX = max.x;
|
|
64
|
+
out.minY = min.y;
|
|
65
|
+
out.maxY = max.y;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The box a def is tested with: its extent grown by `placement.padding`, by one number on all four
|
|
70
|
+
* sides or by a `[horizontal, vertical]` pair.
|
|
71
|
+
*
|
|
72
|
+
* The grid stores the extent and tests this, so each def's padding applies once, on the side that is
|
|
73
|
+
* testing. A def with no padding gets a copy of its extent.
|
|
74
|
+
* @param def - The def whose padding grows the box.
|
|
75
|
+
* @param box - The def's extent, from {@link screenBoxOf}.
|
|
76
|
+
* @param out - The box to write into, which may be `box` itself.
|
|
77
|
+
* @returns `out`.
|
|
78
|
+
*/
|
|
79
|
+
export function paddedBox(def, box, out) {
|
|
80
|
+
const padding = def.placement?.padding ?? 0;
|
|
81
|
+
const padX = typeof padding === 'number' ? padding : padding[0];
|
|
82
|
+
const padY = typeof padding === 'number' ? padding : padding[1];
|
|
83
|
+
out.minX = box.minX - padX;
|
|
84
|
+
out.maxX = box.maxX + padX;
|
|
85
|
+
out.minY = box.minY - padY;
|
|
86
|
+
out.maxY = box.maxY + padY;
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/** The point a sprite's center offset turns about. */
|
|
90
|
+
const ZERO = { x: 0, y: 0 };
|
|
91
|
+
// Scratch, each filled and consumed within one synchronous call.
|
|
92
|
+
const _anchor = new Point();
|
|
93
|
+
const _screen = new Point();
|
|
94
|
+
const _offset = new Point();
|
|
95
|
+
const _rect = new Rect({ x: 0, y: 0 }, { x: 0, y: 0 });
|
|
96
|
+
const _bounds = new Box();
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type OverlapMode } from '../../types/defs';
|
|
2
|
+
/** A rectangle in CSS pixels from the canvas's top-left corner, as the placement pass tests it. */
|
|
3
|
+
export interface PlacementBox {
|
|
4
|
+
/** The left edge. */
|
|
5
|
+
minX: number;
|
|
6
|
+
/** The top edge. */
|
|
7
|
+
minY: number;
|
|
8
|
+
/** The right edge. */
|
|
9
|
+
maxX: number;
|
|
10
|
+
/** The bottom edge. */
|
|
11
|
+
maxY: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether a def in mode `a` is allowed to cover one already placed in mode `b`.
|
|
15
|
+
*
|
|
16
|
+
* Ported from maplibre's `overlapAllowed` in `src/symbol/grid_index.ts`. With two modes the rule
|
|
17
|
+
* reduces to `a === 'always'`, and this stays as the one place it is written.
|
|
18
|
+
*
|
|
19
|
+
* The `_b` parameter is unused: an `always` entry is never inserted into the grid, so the mode of
|
|
20
|
+
* the def already there is always `never`. Re-adding a third mode brings it back.
|
|
21
|
+
* @param a - The mode of the def being tested.
|
|
22
|
+
* @param _b - The mode of the def already in the grid. Unused, see above.
|
|
23
|
+
* @returns Whether the overlap is allowed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function overlapAllowed(a: OverlapMode, _b: OverlapMode): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether two boxes overlap, by the rule {@link PlacementGrid.blockerOf} applies: edge contact is
|
|
28
|
+
* clear, so two boxes sharing a border do not overlap.
|
|
29
|
+
* @param a - One box.
|
|
30
|
+
* @param b - The other.
|
|
31
|
+
* @returns Whether they overlap.
|
|
32
|
+
*/
|
|
33
|
+
export declare function overlaps(a: PlacementBox, b: PlacementBox): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* A uniform-cell index of screen rectangles, which answers which rectangle already in it, if any,
|
|
36
|
+
* blocks a new one.
|
|
37
|
+
*
|
|
38
|
+
* Modeled on maplibre's `GridIndex`, without the circle half. Circles exist there for labels that
|
|
39
|
+
* follow a curved line, which this renderer does not draw. Entries live in flat arrays that a reset
|
|
40
|
+
* keeps at their length and refills from the front under a count cursor: setting `length = 0`
|
|
41
|
+
* instead lets V8 trim the backing store, so every pass would grow it again through its doublings.
|
|
42
|
+
* The per-cell id lists are short and are still cleared with `length = 0`, but only the cells an
|
|
43
|
+
* entry was filed under: a pass resets the grid once per collision scope, and every registered
|
|
44
|
+
* layer is a scope, most of them with no participant at all. Clearing all 1820 cells of a 1440 by
|
|
45
|
+
* 900 canvas for each of imexamerica25-3d's 21 scopes was 0.9–1.2 ms of a 1.1–1.4 ms pass.
|
|
46
|
+
*
|
|
47
|
+
* The index covers the canvas plus {@link GRID_MARGIN_PX} on every side, so a def crossing the
|
|
48
|
+
* edge of the screen decides off screen, where the churn is invisible.
|
|
49
|
+
*/
|
|
50
|
+
export declare class PlacementGrid {
|
|
51
|
+
/** Each cell's entry ids. Index is `y * xCells + x`. */
|
|
52
|
+
private cells;
|
|
53
|
+
/** The index of every cell that holds an entry since the last reset, which is all it clears. */
|
|
54
|
+
private readonly filled;
|
|
55
|
+
/**
|
|
56
|
+
* Four numbers per entry: `minX`, `minY`, `maxX`, `maxY`. Only the first `count * 4` are live; the
|
|
57
|
+
* rest are the last pass's, kept so the next pass overwrites them in place.
|
|
58
|
+
*/
|
|
59
|
+
private readonly boxes;
|
|
60
|
+
/** One mode per entry, parallel to {@link boxes} and live up to `count` in the same way. */
|
|
61
|
+
private readonly modes;
|
|
62
|
+
private count;
|
|
63
|
+
private xCells;
|
|
64
|
+
private yCells;
|
|
65
|
+
/** The canvas width the cells were built for. `-1` until the first `reset`, so it always rebuilds. */
|
|
66
|
+
private width;
|
|
67
|
+
/** The canvas height the cells were built for. `-1` until the first `reset`, so it always rebuilds. */
|
|
68
|
+
private height;
|
|
69
|
+
/**
|
|
70
|
+
* @param width - The canvas width in CSS pixels.
|
|
71
|
+
* @param height - The canvas height in CSS pixels.
|
|
72
|
+
*/
|
|
73
|
+
constructor(width: number, height: number);
|
|
74
|
+
/**
|
|
75
|
+
* Empties the index and sizes it for a canvas, which is what every pass starts with.
|
|
76
|
+
*
|
|
77
|
+
* The cell array is rebuilt only when the canvas size changed, so an unresized canvas pays only
|
|
78
|
+
* for clearing the cells the last fill used.
|
|
79
|
+
* @param width - The canvas width in CSS pixels.
|
|
80
|
+
* @param height - The canvas height in CSS pixels.
|
|
81
|
+
*/
|
|
82
|
+
reset(width: number, height: number): void;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the box intersects the indexed area, which is the canvas plus its margin. A box that
|
|
85
|
+
* only partly overlaps it passes; only one wholly outside fails.
|
|
86
|
+
*
|
|
87
|
+
* A box wholly outside it is not placed and not inserted. That is a correctness rule rather than a
|
|
88
|
+
* priority one: the index cannot hold it, so it cannot be tested against later defs either.
|
|
89
|
+
* @param box - The box to test.
|
|
90
|
+
* @returns Whether the index covers it.
|
|
91
|
+
*/
|
|
92
|
+
intersects(box: PlacementBox): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The earliest-inserted entry that blocks this box, or nothing when it is clear.
|
|
95
|
+
*
|
|
96
|
+
* **Earliest, not first found.** The cell walk reaches a box filed under a later cell before one
|
|
97
|
+
* filed under an earlier cell, so returning the first match would make the answer depend on cell
|
|
98
|
+
* iteration order — and the answer is load-bearing: in a clustering scope the blocker is the
|
|
99
|
+
* cluster's seed, and first come, first served means the earliest. Scanning every cell the box
|
|
100
|
+
* touches costs the early exit a boolean test could take, against a few hundred entries in a
|
|
101
|
+
* handful of cells.
|
|
102
|
+
*
|
|
103
|
+
* Edge contact does not block: two boxes sharing a border are treated as clear, so a row of
|
|
104
|
+
* abutting labels is placeable.
|
|
105
|
+
*
|
|
106
|
+
* Test {@link intersects} first: a box wholly outside the indexed area is clamped into an edge cell,
|
|
107
|
+
* which is safe only because stored coordinates are then compared exactly.
|
|
108
|
+
* @param box - The box to test.
|
|
109
|
+
* @param overlap - How the def being tested competes.
|
|
110
|
+
* @returns The blocking entry's id, as {@link insert} returned it, or `undefined` when clear.
|
|
111
|
+
*/
|
|
112
|
+
blockerOf(box: PlacementBox, overlap: OverlapMode): number | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Adds a box, so that later tests see it.
|
|
115
|
+
*
|
|
116
|
+
* Separate from {@link blockerOf} because asking whether a box is blocked and claiming its room are
|
|
117
|
+
* different questions: a group tests its padded union and inserts its plain one, and maplibre
|
|
118
|
+
* separates `placeCollisionBox` from `insertCollisionBox` for the same reason.
|
|
119
|
+
*
|
|
120
|
+
* Only insert a box that {@link intersects} accepted: one wholly outside the indexed area would be
|
|
121
|
+
* clamped into an edge cell, and relies on the exact comparison in {@link blockerOf} to stay
|
|
122
|
+
* harmless.
|
|
123
|
+
* @param box - The box to add.
|
|
124
|
+
* @param overlap - How the def competes.
|
|
125
|
+
* @returns The entry's id: the number of entries inserted before it since the last reset.
|
|
126
|
+
*/
|
|
127
|
+
insert(box: PlacementBox, overlap: OverlapMode): number;
|
|
128
|
+
/**
|
|
129
|
+
* Reads back the box an entry was inserted with.
|
|
130
|
+
* @param id - The entry's id, as {@link insert} returned it since the last reset.
|
|
131
|
+
* @param out - The box to write into.
|
|
132
|
+
* @returns `out`.
|
|
133
|
+
*/
|
|
134
|
+
boxOf(id: number, out: PlacementBox): PlacementBox;
|
|
135
|
+
/**
|
|
136
|
+
* Which cell column a CSS x lands in, clamped into the index.
|
|
137
|
+
* @param x - The coordinate.
|
|
138
|
+
* @returns The column.
|
|
139
|
+
*/
|
|
140
|
+
private cellX;
|
|
141
|
+
/**
|
|
142
|
+
* Which cell row a CSS y lands in, clamped into the index.
|
|
143
|
+
* @param y - The coordinate.
|
|
144
|
+
* @returns The row.
|
|
145
|
+
*/
|
|
146
|
+
private cellY;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* How wide one cell is, in CSS pixels. More cells means fewer boxes to compare per cell, and more
|
|
150
|
+
* cells for one box to be filed under. Maplibre uses 25 against symbols a few pixels across; our
|
|
151
|
+
* pins are 32, so one pin usually touches one cell.
|
|
152
|
+
*/
|
|
153
|
+
export declare const GRID_CELL_PX = 32;
|
|
154
|
+
/**
|
|
155
|
+
* How far past the canvas the index reaches, in CSS pixels.
|
|
156
|
+
*
|
|
157
|
+
* Maplibre's `viewportPadding`, for its reason: a def crossing the edge of the screen decides off
|
|
158
|
+
* screen, so the flicker that a crossing causes happens where nobody sees it.
|
|
159
|
+
*/
|
|
160
|
+
export declare const GRID_MARGIN_PX = 100;
|
|
161
|
+
//# sourceMappingURL=grid.d.ts.map
|