@tangram.gl/basemap-layers 0.0.0-stage → 0.0.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.
Files changed (105) hide show
  1. package/README.md +34 -2
  2. package/THIRD-PARTY-NOTICES.md +205 -0
  3. package/dist/geojson-worker.js +2160 -0
  4. package/dist/geojson-worker.js.map +6 -0
  5. package/dist/index.cjs +11388 -0
  6. package/dist/index.cjs.map +6 -0
  7. package/dist/index.js +11369 -0
  8. package/dist/index.js.map +6 -0
  9. package/dist/map-style.cjs +445 -0
  10. package/dist/map-style.cjs.map +6 -0
  11. package/dist/map-style.js +431 -0
  12. package/dist/map-style.js.map +6 -0
  13. package/dist/types/arabic-shaping.d.ts +7 -0
  14. package/dist/types/atmosphere-layer.d.ts +10 -0
  15. package/dist/types/basemap-layer.d.ts +150 -0
  16. package/dist/types/bidi.d.ts +8 -0
  17. package/dist/types/circle-layer.d.ts +49 -0
  18. package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
  19. package/dist/types/fill-outline-extension.d.ts +22 -0
  20. package/dist/types/fill-pattern.d.ts +99 -0
  21. package/dist/types/fill-style-extensions.d.ts +100 -0
  22. package/dist/types/geojson-source.d.ts +29 -0
  23. package/dist/types/geojson-tiler.d.ts +48 -0
  24. package/dist/types/geojson-worker.d.ts +40 -0
  25. package/dist/types/globe-horizon-extension.d.ts +61 -0
  26. package/dist/types/globe-layers.d.ts +239 -0
  27. package/dist/types/globe-line-angle-extension.d.ts +37 -0
  28. package/dist/types/glyph-font-renderer.d.ts +37 -0
  29. package/dist/types/glyph-pbf.d.ts +37 -0
  30. package/dist/types/glyph-store.d.ts +103 -0
  31. package/dist/types/grid-index.d.ts +47 -0
  32. package/dist/types/hillshade-layer.d.ts +154 -0
  33. package/dist/types/index.d.ts +9 -0
  34. package/dist/types/is-globe-viewport.d.ts +2 -0
  35. package/dist/types/label-font-renderer.d.ts +18 -0
  36. package/dist/types/label-placement.d.ts +247 -0
  37. package/dist/types/label-text-layer.d.ts +231 -0
  38. package/dist/types/line-blur-extension.d.ts +73 -0
  39. package/dist/types/line-glyphs.d.ts +60 -0
  40. package/dist/types/line-placement.d.ts +93 -0
  41. package/dist/types/map-style-loader.d.ts +48 -0
  42. package/dist/types/map-style-schema.d.ts +72 -0
  43. package/dist/types/map-style.d.ts +6 -0
  44. package/dist/types/mapbox-style.d.ts +40 -0
  45. package/dist/types/mvt-label-layer.d.ts +923 -0
  46. package/dist/types/polylabel.d.ts +17 -0
  47. package/dist/types/raster-color-extension.d.ts +146 -0
  48. package/dist/types/shared-texture.d.ts +34 -0
  49. package/dist/types/sort-key.d.ts +10 -0
  50. package/dist/types/sprite.d.ts +82 -0
  51. package/dist/types/style-accessor.d.ts +45 -0
  52. package/dist/types/style-expression.d.ts +83 -0
  53. package/dist/types/style-resolver.d.ts +94 -0
  54. package/dist/types/text-font.d.ts +36 -0
  55. package/dist/types/text-halo-blur-extension.d.ts +55 -0
  56. package/dist/types/text-layout.d.ts +71 -0
  57. package/dist/types/util.d.ts +22 -0
  58. package/dist/types/zoom-paint-extension.d.ts +72 -0
  59. package/package.json +80 -3
  60. package/src/arabic-shaping.ts +366 -0
  61. package/src/atmosphere-layer.ts +19 -0
  62. package/src/basemap-layer.ts +533 -0
  63. package/src/bidi.ts +199 -0
  64. package/src/circle-layer.ts +191 -0
  65. package/src/fill-extrusion-lighting-extension.ts +206 -0
  66. package/src/fill-outline-extension.ts +70 -0
  67. package/src/fill-pattern.ts +400 -0
  68. package/src/fill-style-extensions.ts +177 -0
  69. package/src/geojson-source.ts +318 -0
  70. package/src/geojson-tiler.ts +185 -0
  71. package/src/geojson-worker.ts +98 -0
  72. package/src/globe-horizon-extension.ts +64 -0
  73. package/src/globe-layers.ts +3487 -0
  74. package/src/globe-line-angle-extension.ts +131 -0
  75. package/src/glyph-font-renderer.ts +173 -0
  76. package/src/glyph-pbf.ts +164 -0
  77. package/src/glyph-store.ts +326 -0
  78. package/src/grid-index.ts +177 -0
  79. package/src/hillshade-layer.ts +775 -0
  80. package/src/index.ts +18 -0
  81. package/src/is-globe-viewport.ts +24 -0
  82. package/src/label-font-renderer.ts +159 -0
  83. package/src/label-placement.ts +727 -0
  84. package/src/label-text-layer.ts +913 -0
  85. package/src/line-blur-extension.ts +237 -0
  86. package/src/line-glyphs.ts +364 -0
  87. package/src/line-placement.ts +446 -0
  88. package/src/map-style-loader.ts +56 -0
  89. package/src/map-style-schema.ts +83 -0
  90. package/src/map-style.ts +23 -0
  91. package/src/mapbox-style.ts +168 -0
  92. package/src/mvt-label-layer.ts +3744 -0
  93. package/src/polylabel.ts +312 -0
  94. package/src/raster-color-extension.ts +245 -0
  95. package/src/shared-texture.ts +153 -0
  96. package/src/sort-key.ts +63 -0
  97. package/src/sprite.ts +244 -0
  98. package/src/style-accessor.ts +95 -0
  99. package/src/style-expression.ts +327 -0
  100. package/src/style-resolver.ts +301 -0
  101. package/src/text-font.ts +150 -0
  102. package/src/text-halo-blur-extension.ts +115 -0
  103. package/src/text-layout.ts +441 -0
  104. package/src/util.ts +47 -0
  105. package/src/zoom-paint-extension.ts +207 -0
@@ -0,0 +1,326 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ import {GLYPH_RANGE_SIZE, parseGlyphRange} from './glyph-pbf';
10
+ import type {Glyph} from './glyph-pbf';
11
+
12
+ /** Options for a {@link GlyphStore}. */
13
+ export type GlyphStoreOptions = {
14
+ /** The style's `glyphs` URL, with `{fontstack}` and `{range}`. */
15
+ url: string;
16
+ /** Base URL that a relative `glyphs` URL resolves against, normally the style URL. */
17
+ baseUrl?: string;
18
+ fetch?: typeof fetch;
19
+ fetchOptions?: RequestInit;
20
+ };
21
+
22
+ /** The highest code point of a glyph range, as in MapLibre GL JS. */
23
+ const MAX_CODE_POINT = 0xffff;
24
+
25
+ /**
26
+ * Bound on the font stacks a store keeps glyphs for: stacks come from untrusted style and tile
27
+ * data. When full, the stack used least recently is dropped, and loaded again if asked for,
28
+ * unless a consumer retains it (see {@link GlyphStore.retain}).
29
+ */
30
+ export const MAX_GLYPH_STACKS = 64;
31
+
32
+ /** Bound on the font stacks one consumer, such as a label layer, can retain. */
33
+ export const MAX_RETAINED_GLYPH_STACKS = 2 * MAX_GLYPH_STACKS;
34
+
35
+ /**
36
+ * Bound on the font stacks all the consumers of a store can retain together: the store never
37
+ * holds more than these, or {@link MAX_GLYPH_STACKS} when more.
38
+ */
39
+ export const MAX_STORE_RETAINED_GLYPH_STACKS = 4 * MAX_GLYPH_STACKS;
40
+
41
+ /**
42
+ * Whether MapLibre GL JS draws a character with the browser's fonts rather than the style's
43
+ * glyphs: CJK ideographs, hangul, hiragana and katakana, by default (`localIdeographFontFamily`).
44
+ */
45
+ export function isLocalGlyph(codePoint: number): boolean {
46
+ return /\p{Ideo}|\p{sc=Hang}|\p{sc=Hira}|\p{sc=Kana}/u.test(String.fromCodePoint(codePoint));
47
+ }
48
+
49
+ /** The glyphs of one font stack: each loaded range, by its index, as a map from code point. */
50
+ type StackEntry = {
51
+ ranges: Map<number, Map<number, Glyph>>;
52
+ requests: Map<number, Promise<void>>;
53
+ };
54
+
55
+ /**
56
+ * The glyph ranges of a style's `glyphs` URL, loaded per font stack and range as labels need
57
+ * them, as MapLibre GL JS loads them. A range that fails to load holds no glyphs, so its
58
+ * characters are not drawn, as in MapLibre.
59
+ */
60
+ let lastStoreId = 0;
61
+
62
+ export class GlyphStore {
63
+ /**
64
+ * Names this store among others, as of a style loaded again: a new store for the same URL holds
65
+ * none of the glyphs this one loaded.
66
+ */
67
+ readonly id = ++lastStoreId;
68
+ readonly url: string;
69
+ private readonly fetch: typeof fetch;
70
+ private readonly fetchOptions?: RequestInit;
71
+ private readonly stacks = new Map<string, StackEntry>();
72
+ /** The font stacks each consumer retains (see `retain`). */
73
+ private readonly retained = new Map<object, Set<string>>();
74
+ /** The number of consumers that retain each font stack retained. */
75
+ private readonly retainCounts = new Map<string, number>();
76
+ /** Called once when a consumer next gives back a font stack (see `onRetainedFreed`). */
77
+ private readonly freedListeners = new Map<object, () => void>();
78
+
79
+ constructor({url, baseUrl, fetch: fetchFn, fetchOptions}: GlyphStoreOptions) {
80
+ this.url = resolveGlyphsUrl(url, baseUrl);
81
+ this.fetch = fetchFn || ((...args: Parameters<typeof fetch>) => fetch(...args));
82
+ this.fetchOptions = fetchOptions;
83
+ }
84
+
85
+ /**
86
+ * The glyph of a code point: `undefined` while its range is not loaded, `null` when the range
87
+ * has no glyph for it.
88
+ */
89
+ getGlyph(fontStack: string, codePoint: number): Glyph | null | undefined {
90
+ const range = this.getStack(fontStack, false)?.ranges.get(getGlyphRange(codePoint));
91
+ return range ? (range.get(codePoint) ?? null) : undefined;
92
+ }
93
+
94
+ /** Whether the ranges of every code point of `text` are loaded, except local glyphs. */
95
+ hasGlyphs(fontStack: string, text: string): boolean {
96
+ const stack = this.getStack(fontStack, false);
97
+ for (const character of text) {
98
+ const codePoint = character.codePointAt(0)!;
99
+ if (codePoint <= MAX_CODE_POINT && !isLocalGlyph(codePoint)) {
100
+ if (!stack?.ranges.has(getGlyphRange(codePoint))) {
101
+ return false;
102
+ }
103
+ }
104
+ }
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Loads the ranges of the code points of `texts` that are not loaded yet. Resolves when they
110
+ * are, or have failed; returns null when there is nothing to load.
111
+ */
112
+ load(fontStack: string, texts: Iterable<string>): Promise<void> | null {
113
+ const stack = this.getStack(fontStack, true)!;
114
+ const pending = new Set<Promise<void>>();
115
+ for (const text of texts) {
116
+ for (const character of text) {
117
+ const codePoint = character.codePointAt(0)!;
118
+ if (codePoint > MAX_CODE_POINT || isLocalGlyph(codePoint)) {
119
+ continue;
120
+ }
121
+ const range = getGlyphRange(codePoint);
122
+ if (stack.ranges.has(range)) {
123
+ continue;
124
+ }
125
+ let request = stack.requests.get(range);
126
+ if (!request) {
127
+ request = this.loadRange(fontStack, range).then(glyphs => {
128
+ stack.ranges.set(range, glyphs);
129
+ stack.requests.delete(range);
130
+ });
131
+ stack.requests.set(range, request);
132
+ }
133
+ pending.add(request);
134
+ }
135
+ }
136
+ return pending.size ? Promise.all(pending).then(() => {}) : null;
137
+ }
138
+
139
+ /**
140
+ * Keeps the glyphs of `fontStack` for `owner` until it gives the stack back (see `retainOnly`
141
+ * and `release`). A label layer retains the font stack of each label it draws or waits for
142
+ * before it loads any, so that loading some of them does not drop the others: a batch of
143
+ * labels in more than {@link MAX_GLYPH_STACKS} font stacks could otherwise never load.
144
+ *
145
+ * Returns false when the stack cannot be retained: `owner` retains
146
+ * {@link MAX_RETAINED_GLYPH_STACKS} stacks already, or the store's consumers
147
+ * {@link MAX_STORE_RETAINED_GLYPH_STACKS}. Font stacks come from untrusted tile data, so
148
+ * those bounds hold whatever a tile names; a label layer leaves the labels of a font stack it
149
+ * cannot retain undrawn, as those of a glyph range that fails to load.
150
+ */
151
+ retain(owner: object, fontStack: string): boolean {
152
+ let owned = this.retained.get(owner);
153
+ if (owned?.has(fontStack)) {
154
+ return true;
155
+ }
156
+ const count = this.retainCounts.get(fontStack) || 0;
157
+ if (
158
+ (owned?.size ?? 0) >= MAX_RETAINED_GLYPH_STACKS ||
159
+ (!count && this.retainCounts.size >= MAX_STORE_RETAINED_GLYPH_STACKS)
160
+ ) {
161
+ return false;
162
+ }
163
+ if (!owned) {
164
+ owned = new Set();
165
+ this.retained.set(owner, owned);
166
+ }
167
+ owned.add(fontStack);
168
+ this.retainCounts.set(fontStack, count + 1);
169
+ return true;
170
+ }
171
+
172
+ /** Gives back the font stacks `owner` retains other than `fontStacks` (see `retain`). */
173
+ retainOnly(owner: object, fontStacks: ReadonlySet<string>): void {
174
+ const owned = this.retained.get(owner);
175
+ if (!owned) {
176
+ return;
177
+ }
178
+ let freed = false;
179
+ for (const fontStack of owned) {
180
+ if (!fontStacks.has(fontStack)) {
181
+ owned.delete(fontStack);
182
+ this.unretain(fontStack);
183
+ freed = true;
184
+ }
185
+ }
186
+ if (!owned.size) {
187
+ this.retained.delete(owner);
188
+ }
189
+ this.evict();
190
+ if (freed) {
191
+ this.generation++;
192
+ const listeners = [...this.freedListeners.values()];
193
+ this.freedListeners.clear();
194
+ for (const listener of listeners) {
195
+ listener();
196
+ }
197
+ }
198
+ }
199
+
200
+ /** Gives back the font stacks `owner` retains (see `retain`). */
201
+ release(owner: object): void {
202
+ this.freedListeners.delete(owner);
203
+ this.retainOnly(owner, new Set());
204
+ }
205
+
206
+ /**
207
+ * Counts the times a consumer gave back font stacks: a `retain` refused before may succeed
208
+ * once it has changed.
209
+ */
210
+ generation = 0;
211
+
212
+ /**
213
+ * Calls `listener` once, the next time a consumer gives back font stacks, in place of the
214
+ * listener `owner` set before; null removes it. A label layer whose font stacks were refused
215
+ * updates again then, to retain and load them.
216
+ */
217
+ onRetainedFreed(owner: object, listener: (() => void) | null): void {
218
+ if (listener) {
219
+ this.freedListeners.set(owner, listener);
220
+ } else {
221
+ this.freedListeners.delete(owner);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * The URL of a range: the font names of the stack, each URL-encoded, joined by commas. Font
227
+ * stacks can come from tile data (a data-driven `text-font`), so a name cannot add path
228
+ * segments, a query or a fragment to the URL.
229
+ */
230
+ getRangeUrl(fontStack: string, range: number): string {
231
+ const begin = range * GLYPH_RANGE_SIZE;
232
+ // `encodeURIComponent` keeps dots: a name of only dots would be a `.` or `..` path segment.
233
+ const encoded = fontStack
234
+ .split(',')
235
+ .map(name => (/^\.+$/.test(name) ? name.replace(/\./g, '%2E') : encodeURIComponent(name)))
236
+ .join(',');
237
+ return this.url.replace(/\{(fontstack|range)\}/g, (_, name) =>
238
+ name === 'range' ? `${begin}-${begin + GLYPH_RANGE_SIZE - 1}` : encoded
239
+ );
240
+ }
241
+
242
+ private async loadRange(fontStack: string, range: number): Promise<Map<number, Glyph>> {
243
+ const glyphs = new Map<number, Glyph>();
244
+ try {
245
+ const response = await this.fetch(this.getRangeUrl(fontStack, range), this.fetchOptions);
246
+ if (!response.ok) {
247
+ throw new Error(`HTTP ${response.status}`);
248
+ }
249
+ for (const glyph of parseGlyphRange(await response.arrayBuffer())) {
250
+ if (getGlyphRange(glyph.id) === range) {
251
+ glyphs.set(glyph.id, glyph);
252
+ }
253
+ }
254
+ } catch (error) {
255
+ // biome-ignore lint/suspicious/noConsole: a missing range leaves its characters undrawn.
256
+ console.warn(`Failed to load glyphs ${this.getRangeUrl(fontStack, range)}`, error);
257
+ }
258
+ return glyphs;
259
+ }
260
+
261
+ private getStack(fontStack: string, create: boolean): StackEntry | undefined {
262
+ let stack = this.stacks.get(fontStack);
263
+ if (stack) {
264
+ // Most recently used last.
265
+ this.stacks.delete(fontStack);
266
+ this.stacks.set(fontStack, stack);
267
+ } else if (create) {
268
+ stack = {ranges: new Map(), requests: new Map()};
269
+ this.stacks.set(fontStack, stack);
270
+ this.evict(fontStack);
271
+ }
272
+ return stack;
273
+ }
274
+
275
+ /**
276
+ * Drops the font stacks used least recently, beyond the bound, except those retained and
277
+ * `keep`, which is being loaded.
278
+ */
279
+ private evict(keep?: string): void {
280
+ for (const fontStack of this.stacks.keys()) {
281
+ if (this.stacks.size <= MAX_GLYPH_STACKS) {
282
+ return;
283
+ }
284
+ if (fontStack !== keep && !this.isRetained(fontStack)) {
285
+ this.stacks.delete(fontStack);
286
+ }
287
+ }
288
+ }
289
+
290
+ private isRetained(fontStack: string): boolean {
291
+ return this.retainCounts.has(fontStack);
292
+ }
293
+
294
+ private unretain(fontStack: string): void {
295
+ const count = this.retainCounts.get(fontStack)! - 1;
296
+ if (count) {
297
+ this.retainCounts.set(fontStack, count);
298
+ } else {
299
+ this.retainCounts.delete(fontStack);
300
+ }
301
+ }
302
+ }
303
+
304
+ /** The index of the range that holds a code point. */
305
+ export function getGlyphRange(codePoint: number): number {
306
+ return Math.floor(codePoint / GLYPH_RANGE_SIZE);
307
+ }
308
+
309
+ /**
310
+ * Resolves a relative `glyphs` URL against the style URL. The template's braces are kept, which
311
+ * `URL` would encode.
312
+ */
313
+ function resolveGlyphsUrl(url: string, baseUrl?: string): string {
314
+ if (!baseUrl || /^[a-z][a-z\d+.-]*:/i.test(url)) {
315
+ return url;
316
+ }
317
+ try {
318
+ const resolved = new URL(
319
+ url.replace(/[{}]/g, brace => (brace === '{' ? '%7B' : '%7D')),
320
+ baseUrl
321
+ );
322
+ return resolved.href.replace(/%7B/gi, '{').replace(/%7D/gi, '}');
323
+ } catch {
324
+ return url;
325
+ }
326
+ }
@@ -0,0 +1,177 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ /**
10
+ * How a placed part lets others overlap it, as MapLibre GL JS's `text-overlap` / `icon-overlap`:
11
+ * `never` overlaps nothing and is overlapped by nothing; `always` overlaps anything; `cooperative`
12
+ * overlaps `always` and `cooperative` parts, but not `never` ones.
13
+ */
14
+ export type OverlapMode = 'never' | 'always' | 'cooperative';
15
+
16
+ /** Whether a part placed with `mode` may overlap one placed with `placedMode`. */
17
+ export function allowsOverlap(mode: OverlapMode, placedMode: OverlapMode): boolean {
18
+ return mode === 'always' || (mode !== 'never' && placedMode !== 'never');
19
+ }
20
+
21
+ /**
22
+ * A grid of screen-space boxes and circles for label placement, as MapLibre GL JS's `GridIndex`:
23
+ * the plane is cut into square cells, each holding the shapes that reach into it, so that a test
24
+ * compares a shape only with the shapes of the cells it covers. Coordinates are pixels; a shape
25
+ * that reaches outside the grid is clamped to its edge cells.
26
+ */
27
+ export class GridIndex {
28
+ readonly width: number;
29
+ readonly height: number;
30
+ private readonly cellSize: number;
31
+ private readonly columns: number;
32
+ private readonly rows: number;
33
+ /** Per cell, the indices of the shapes in it. */
34
+ private readonly cells: number[][];
35
+ /** Per shape: `x1, y1, x2, y2` for a box, `x, y, radius, NaN` for a circle. */
36
+ private readonly shapes: number[] = [];
37
+ private readonly modes: OverlapMode[] = [];
38
+ /** The query count at which each shape was last compared, so a query compares it once. */
39
+ private readonly seen: number[] = [];
40
+ private query = 0;
41
+
42
+ constructor(width: number, height: number, cellSize: number) {
43
+ this.width = width;
44
+ this.height = height;
45
+ this.cellSize = cellSize;
46
+ this.columns = Math.max(1, Math.ceil(width / cellSize));
47
+ this.rows = Math.max(1, Math.ceil(height / cellSize));
48
+ this.cells = Array.from({length: this.columns * this.rows}, () => []);
49
+ }
50
+
51
+ /** The number of shapes inserted. */
52
+ get size(): number {
53
+ return this.modes.length;
54
+ }
55
+
56
+ /** Adds a box from `x1, y1` to `x2, y2`. */
57
+ insert(x1: number, y1: number, x2: number, y2: number, mode: OverlapMode = 'never'): void {
58
+ this.add([x1, y1, x2, y2], mode, x1, y1, x2, y2);
59
+ }
60
+
61
+ /** Adds a circle. */
62
+ insertCircle(x: number, y: number, radius: number, mode: OverlapMode = 'never'): void {
63
+ this.add([x, y, radius, Number.NaN], mode, x - radius, y - radius, x + radius, y + radius);
64
+ }
65
+
66
+ /** Whether a box placed with `mode` would overlap a shape that it may not overlap. */
67
+ hitTest(x1: number, y1: number, x2: number, y2: number, mode: OverlapMode = 'never'): boolean {
68
+ return this.test(x1, y1, x2, y2, mode, (sx1, sy1, sx2, sy2) =>
69
+ Number.isNaN(sy2)
70
+ ? circleIntersectsBox(sx1, sy1, sx2, x1, y1, x2, y2)
71
+ : x1 <= sx2 && y1 <= sy2 && x2 >= sx1 && y2 >= sy1
72
+ );
73
+ }
74
+
75
+ /** Whether a circle placed with `mode` would overlap a shape that it may not overlap. */
76
+ hitTestCircle(x: number, y: number, radius: number, mode: OverlapMode = 'never'): boolean {
77
+ return this.test(x - radius, y - radius, x + radius, y + radius, mode, (sx1, sy1, sx2, sy2) =>
78
+ Number.isNaN(sy2)
79
+ ? (sx1 - x) ** 2 + (sy1 - y) ** 2 <= (sx2 + radius) ** 2
80
+ : circleIntersectsBox(x, y, radius, sx1, sy1, sx2, sy2)
81
+ );
82
+ }
83
+
84
+ /** Whether a box lies at least partly within the grid. */
85
+ isInside(x1: number, y1: number, x2: number, y2: number): boolean {
86
+ return x2 >= 0 && y2 >= 0 && x1 < this.width && y1 < this.height;
87
+ }
88
+
89
+ private add(
90
+ shape: number[],
91
+ mode: OverlapMode,
92
+ x1: number,
93
+ y1: number,
94
+ x2: number,
95
+ y2: number
96
+ ): void {
97
+ const index = this.modes.length;
98
+ this.shapes.push(...shape);
99
+ this.modes.push(mode);
100
+ this.seen.push(-1);
101
+ this.forEachCell(x1, y1, x2, y2, cell => {
102
+ this.cells[cell].push(index);
103
+ return false;
104
+ });
105
+ }
106
+
107
+ private test(
108
+ x1: number,
109
+ y1: number,
110
+ x2: number,
111
+ y2: number,
112
+ mode: OverlapMode,
113
+ intersects: (sx1: number, sy1: number, sx2: number, sy2: number) => boolean
114
+ ): boolean {
115
+ if (!this.isInside(x1, y1, x2, y2)) {
116
+ return false;
117
+ }
118
+ const query = ++this.query;
119
+ const {shapes, modes, seen} = this;
120
+ return this.forEachCell(x1, y1, x2, y2, cell => {
121
+ for (const index of this.cells[cell]) {
122
+ if (seen[index] === query) {
123
+ continue;
124
+ }
125
+ seen[index] = query;
126
+ const offset = index * 4;
127
+ if (
128
+ !allowsOverlap(mode, modes[index]) &&
129
+ intersects(shapes[offset], shapes[offset + 1], shapes[offset + 2], shapes[offset + 3])
130
+ ) {
131
+ return true;
132
+ }
133
+ }
134
+ return false;
135
+ });
136
+ }
137
+
138
+ /** Calls `visit` for each cell the box reaches, until it returns `true`. */
139
+ private forEachCell(
140
+ x1: number,
141
+ y1: number,
142
+ x2: number,
143
+ y2: number,
144
+ visit: (cell: number) => boolean
145
+ ): boolean {
146
+ const column1 = this.toCell(x1, this.columns);
147
+ const row1 = this.toCell(y1, this.rows);
148
+ const column2 = this.toCell(x2, this.columns);
149
+ const row2 = this.toCell(y2, this.rows);
150
+ for (let column = column1; column <= column2; column++) {
151
+ for (let row = row1; row <= row2; row++) {
152
+ if (visit(row * this.columns + column)) {
153
+ return true;
154
+ }
155
+ }
156
+ }
157
+ return false;
158
+ }
159
+
160
+ private toCell(value: number, count: number): number {
161
+ return Math.max(0, Math.min(count - 1, Math.floor(value / this.cellSize)));
162
+ }
163
+ }
164
+
165
+ function circleIntersectsBox(
166
+ x: number,
167
+ y: number,
168
+ radius: number,
169
+ x1: number,
170
+ y1: number,
171
+ x2: number,
172
+ y2: number
173
+ ): boolean {
174
+ const dx = x - Math.max(x1, Math.min(x, x2));
175
+ const dy = y - Math.max(y1, Math.min(y, y2));
176
+ return dx * dx + dy * dy <= radius * radius;
177
+ }