@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.
Files changed (37) 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/lighting/Lighting.js +5 -6
  22. package/dist/objects/materials/TextureMaterial.d.ts +11 -8
  23. package/dist/objects/materials/TextureMaterial.js +24 -17
  24. package/dist/objects/materials/effects/ao/index.d.ts +1 -1
  25. package/dist/objects/materials/effects/ao/index.js +3 -3
  26. package/dist/objects/materials/effects/contact-shadow.js +2 -2
  27. package/dist/objects/text/text-container.d.ts +16 -1
  28. package/dist/objects/text/text-container.js +23 -1
  29. package/dist/space/layout-measure.d.ts +12 -10
  30. package/dist/space/layout-measure.js +18 -16
  31. package/dist/types/defs.d.ts +157 -147
  32. package/dist/types/events.d.ts +5 -1
  33. package/dist/types/guards.d.ts +7 -0
  34. package/dist/types/guards.js +10 -0
  35. package/dist/utils/math.d.ts +18 -1
  36. package/dist/utils/math.js +35 -0
  37. 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 one.
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) && !isLayerDef(def));
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
- const groups = new Set();
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
- register(defs) {
190
- groups.add(defs);
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