fuzzyworld

tower / files / src / terrain / tile.ts

tile.ts

Blame: the commit that last changed each line · file · history

376f2ce6 2026-09-16 Pavel Safronov1/**
2 * Terrain tiles: the quadtree scheme and the mesh data for one tile.
3 * See world-generation.md §6.1.
4 *
5 * The unrolled surface is cut into a quadtree. Level 0 is 16 tiles along the axis by
6 * 10 around it — 33.75 × 31.4 km each — and each level halves them, down to level 10
7 * at 33 × 31 m. Every tile is the same grid of vertices, so a near tile has fine
8 * spacing and a far one coarse.
9 *
10 * A tile is built in a worker and handed back as typed arrays. Its vertices are
11 * already bent onto the cylinder and given **relative to the tile's own centre**, in
12 * kilometres. That is a precision decision, not a convenience: float32 at 50 km is
13 * about 6 mm, which is visible jitter on anything small, while relative to a centre
14 * at most 24 km away it is under a millimetre, and at level 10 it is nothing.
15 */
16import { CIRCUMFERENCE_M, LENGTH_M, RADIUS_M } from '../canon/constants.js'
17import type { Runtime, Sample } from './field.js'
18
14d35b7d 2026-09-16 Pavel Safronov19/**
20 * The deepest anything generated may reach, metres below the datum. Tower's wall is a
21 * kilometre thick (D-044), and its outer skin sits at that depth: sea floors are cut to
22 * about -330 m, and nothing — not a basin, not a tile's skirt — may reach the skin, or
23 * it shows through the hull from outside (D-064).
24 */
25export const LOWEST_GROUND_M = -950
26
376f2ce6 2026-09-16 Pavel Safronov27export const ROOTS_ALONG = 16
28export const ROOTS_AROUND = 10
29export const MAX_LEVEL = 10
30
31export interface TileKey { level: number; i: number; j: number }
32
33export const tileKey = (t: TileKey): string => `${t.level}/${t.i}/${t.j}`
34
35/** Tile size in metres along the axis and around it. */
36export function tileSize(level: number): { along: number; around: number } {
37 const n = 2 ** level
38 return { along: LENGTH_M / (ROOTS_ALONG * n), around: CIRCUMFERENCE_M / (ROOTS_AROUND * n) }
39}
40
41/** The tile's centre on the datum cylinder, in kilometres, tower frame. */
42export function tileCentreKm(t: TileKey): [number, number, number] {
43 const s = tileSize(t.level)
44 const z = (t.i + 0.5) * s.along
45 const theta = ((t.j + 0.5) * s.around) / RADIUS_M
46 const r = RADIUS_M / 1000
47 return [r * Math.cos(theta), r * Math.sin(theta), z / 1000]
48}
49
50/**
51 * Materials, in the order the shader reads them. Weights, not colours: the fragment
52 * shader turns them into colour, so a tile carries what the ground *is* and the look
53 * of it can change without regenerating anything.
54 */
51c39d8b 2026-09-17 Pavel Safronov55export const MATERIALS = [
abc0aad0 2026-10-01 Pavel Safronov56 'plain', 'desert', 'forest', 'rock', 'snow', 'water', 'depth', 'bed', 'salt', 'sandstone', 'farm',
51c39d8b 2026-09-17 Pavel Safronov57] as const
80bcc97a 2026-09-17 Pavel Safronov58
59/**
60 * Per water vertex, in this order: depth of water over the ground, main wave amplitude,
e18dad04 2026-09-30 Pavel Safronov61 * wavelength and direction; flow along z and around; whiteness; ice cover (field.ts).
80bcc97a 2026-09-17 Pavel Safronov62 */
63export const WATER_ATTRS = 8
64
00ceeedb 2026-09-27 Pavel Safronov65/**
66 * Per canopy vertex: how high the canopy stands over the ground here, how full the stand
67 * is, 0..1, and how much of it is conifer.
68 */
69export const CANOPY_ATTRS = 3
70
71export interface TileCanopy {
72 /** The *ground* vertices, relative to the tile centre, km: the shader lifts them. */
73 positions: Float32Array
74 attrs: Float32Array
75 indices: Uint16Array
76}
77
80bcc97a 2026-09-17 Pavel Safronov78export interface TileWater {
79 /** Water surface vertices, relative to the tile centre, km: the same grid as the ground. */
80 positions: Float32Array
81 attrs: Float32Array
82 indices: Uint16Array
83}
376f2ce6 2026-09-16 Pavel Safronov84
85export interface TileData {
86 key: string
87 /** Vertices relative to the tile centre, km, tower frame. Grid first, then skirts. */
88 positions: Float32Array
89 /** Unit normals, tower frame, as signed bytes: a normal needs no more than 1/127. */
90 normals: Int8Array
91 /** MATERIALS.length weights per vertex, 0..255. */
92 materials: Uint8Array
80bcc97a 2026-09-17 Pavel Safronov93 /** The water surfaces on this tile, or null where there is none. */
94 water: TileWater | null
00ceeedb 2026-09-27 Pavel Safronov95 /** The forest canopy over this tile, seen from far off, or null where nothing grows. */
96 canopy: TileCanopy | null
80bcc97a 2026-09-17 Pavel Safronov97 /** Lowest and highest displayed elevation on the tile, ground or water, metres. */
376f2ce6 2026-09-16 Pavel Safronov98 minM: number
99 maxM: number
100}
101
102/** Vertices per side of a tile's grid. */
103export type TileGrid = 33 | 65
104
105const smoothstep = (e0: number, e1: number, x: number): number => {
106 const t = Math.min(1, Math.max(0, (x - e0) / (e1 - e0)))
107 return t * t * (3 - 2 * t)
108}
109
110/**
111 * Surface material weights from a sample and its slope. Also used by the map, so
112 * the map and the view cannot disagree about what the ground is.
113 *
114 * `out` receives MATERIALS.length values in 0..1.
115 */
14d35b7d 2026-09-16 Pavel Safronov116export function materialsAt(s: Sample, slope: number, out: Float32Array | number[], offset = 0): void {
376f2ce6 2026-09-16 Pavel Safronov117 const shown = Math.max(s.h, s.water)
80bcc97a 2026-09-17 Pavel Safronov118 /**
119 * Water that is drawn as a surface of its own leaves the ground under it as bed. Water
120 * too narrow to draw — a river under a coarse tile's vertex spacing — tints the ground
121 * by how much of it the river covers.
122 */
123 // A narrow river's share of a cell reads far weaker than a line that thin looks to the
124 // eye: 40 m of river in a 200 m cell is a 20% tint, and it vanished where tiles coarsened.
125 // The square root keeps a sub-resolution river continuous with the surface drawn nearer.
126 const water = s.waterMesh ? 0 : Math.min(1, Math.sqrt(s.waterCover) * 1.2)
51c39d8b 2026-09-17 Pavel Safronov127 /**
128 * Bare rock: high ground in a range, whatever the desert claims as rock (plateau tops,
129 * cliff bands, a wash's gravel floor), and any steep desert face — a mesa's riser reads
130 * as rock because it is too steep to hold sand, which is the same reason snow slides off.
131 */
132 const rock = Math.min(1, Math.max(
133 smoothstep(1300, 1900, s.relief),
134 Math.max(s.rock, smoothstep(0.45, 0.85, slope) * s.desert)))
135 // A salt crust is not sand, rock or anything else: it covers them.
136 const salt = s.salt * (1 - water)
137 const desert = s.desert * (1 - rock) * (1 - salt)
00ceeedb 2026-09-27 Pavel Safronov138 /**
139 * Forest floor only where trees actually stand: a meadow inside a forest is grass, and
140 * the palette's plain ramp is the grass (world-generation.md §4.6).
141 */
142 const canopy = Math.min(1, s.stems / 150)
143 const forest = s.forest * (1 - rock) * (1 - desert) * (1 - salt) * canopy
51c39d8b 2026-09-17 Pavel Safronov144 const plain = Math.max(0, 1 - rock - desert - forest - salt)
376f2ce6 2026-09-16 Pavel Safronov145 // Snow does not lie on cliffs. Blending it out with slope is what turns a white blob
146 // into a mountain with rock showing through.
147 const holds = 1 - smoothstep(0.55, 1.1, slope)
e18dad04 2026-09-30 Pavel Safronov148 // Grounded sea ice is white whatever its slope: an ice front is ice, not a cliff of rock.
149 const snow = Math.max((1 - water) * smoothstep(s.snowlineM, s.snowlineM + 550, shown) * holds, s.glacier)
376f2ce6 2026-09-16 Pavel Safronov150 out[offset] = plain
151 out[offset + 1] = desert
152 out[offset + 2] = forest
51c39d8b 2026-09-17 Pavel Safronov153 // Rock in a desert is sandstone: warm, and bedded. Rock in a range is grey stone.
154 const sandstone = rock * (1 - salt) * s.desert
155 out[offset + 3] = rock * (1 - salt) - sandstone
376f2ce6 2026-09-16 Pavel Safronov156 out[offset + 4] = snow
157 out[offset + 5] = water
158 // Water deepens to its darkest over the first 90 m, as the v1 map did.
14d35b7d 2026-09-16 Pavel Safronov159 out[offset + 6] = s.water > s.h ? Math.min(1, (s.water - s.h) / 90) : 0
80bcc97a 2026-09-17 Pavel Safronov160 out[offset + 7] = s.waterMesh ? 1 : 0
51c39d8b 2026-09-17 Pavel Safronov161 out[offset + 8] = salt
162 out[offset + 9] = sandstone
abc0aad0 2026-10-01 Pavel Safronov163 // Fields lie over whatever the ground would otherwise be; the shader draws the crop.
164 out[offset + 10] = s.farm * (1 - water) * (1 - snow)
376f2ce6 2026-09-16 Pavel Safronov165}
166
167/**
168 * Build one tile. Samples an (N+2)² grid — one ring beyond the edge — so that normals
169 * at the edge are taken from the real neighbouring ground, and match the tile next
170 * door instead of creasing along every seam.
171 */
172export function buildTile(rt: Runtime, t: TileKey, n: TileGrid): TileData {
173 const size = tileSize(t.level)
174 const z0 = t.i * size.along
175 const a0 = t.j * size.around
176 const dz = size.along / (n - 1)
177 const da = size.around / (n - 1)
178 // Shortest wavelength worth computing: twice the vertex spacing.
179 const detail = 2 * Math.min(dz, da)
180 const W = n + 2
181
182 const shown = new Float64Array(W * W)
183 const mats = new Float32Array(W * W * MATERIALS.length)
e18dad04 2026-09-30 Pavel Safronov184 // Grounded sea ice (D-093), which stays white however steep its front.
185 const glacier = new Uint8Array(W * W)
80bcc97a 2026-09-17 Pavel Safronov186 // The water on the tile's own grid: its level (NaN where none is drawn) and attributes.
187 const wLevel = new Float64Array(n * n).fill(NaN)
188 const wAttrs = new Float32Array(n * n * WATER_ATTRS)
189 const wet = new Uint8Array(n * n)
00ceeedb 2026-09-27 Pavel Safronov190 // The canopy over the tile's own grid: how high it stands, and what it is made of.
191 const cAttrs = new Float32Array(n * n * CANOPY_ATTRS)
192 let wooded = false
376f2ce6 2026-09-16 Pavel Safronov193 for (let v = 0; v < W; v++) {
194 for (let u = 0; u < W; u++) {
195 // z is clamped to the world: the ring beyond a tile at either cap samples the
196 // edge again rather than the space beyond the end of the cylinder.
197 const z = Math.min(LENGTH_M, Math.max(0, z0 + (u - 1) * dz))
198 const theta = (a0 + (v - 1) * da) / RADIUS_M
199 const s = rt.sample(theta, z, detail)
200 const k = v * W + u
80bcc97a 2026-09-17 Pavel Safronov201 // The ground is the ground, under water or not: the water is its own mesh now.
202 shown[k] = s.h
203 if (s.waterMesh && u >= 1 && u <= n && v >= 1 && v <= n) {
204 const w = (v - 1) * n + (u - 1)
205 wet[w] = 1
206 wLevel[w] = s.water
207 const o = w * WATER_ATTRS
208 wAttrs[o] = s.water - s.h
a088e5d5 2026-09-17 Pavel Safronov209 if (s.body >= 0) {
210 // A lake or sea: its waves follow the weather, so the vertex names the body and
211 // the shader looks its sea state up (sea-state.ts). Negative, so 0 stays "none".
212 wAttrs[o + 1] = -(s.body + 1)
213 } else {
214 // A river's own chop: short and low, and running with it.
215 wAttrs[o + 1] = 0.04
216 wAttrs[o + 2] = 3
217 wAttrs[o + 3] = Math.atan2(s.flowA, s.flowZ)
218 }
80bcc97a 2026-09-17 Pavel Safronov219 wAttrs[o + 4] = s.flowZ
220 wAttrs[o + 5] = s.flowA
221 wAttrs[o + 6] = s.foam
e18dad04 2026-09-30 Pavel Safronov222 wAttrs[o + 7] = s.ice
80bcc97a 2026-09-17 Pavel Safronov223 }
00ceeedb 2026-09-27 Pavel Safronov224 if (s.canopyM > 0 && u >= 1 && u <= n && v >= 1 && v <= n) {
225 const c = ((v - 1) * n + (u - 1)) * CANOPY_ATTRS
226 /**
227 * A rough top. The canopy of a stand is not a sheet at one height — from a
228 * distance what reads as forest is the broken line of it — so each vertex is
229 * knocked about by a hash of where it is, which every tile agrees on.
230 */
231 const jitter = 1 + (hash01(z, a0 + (v - 1) * da) - 0.5) * 0.35
232 cAttrs[c] = s.canopyM * jitter
233 cAttrs[c + 1] = Math.min(1, s.stems / 450)
234 cAttrs[c + 2] = s.forest
235 wooded = true
236 }
376f2ce6 2026-09-16 Pavel Safronov237 // Slope is not known until the neighbours are, so snow is thinned by it below.
14d35b7d 2026-09-16 Pavel Safronov238 materialsAt(s, 0, mats, k * MATERIALS.length)
e18dad04 2026-09-30 Pavel Safronov239 glacier[k] = s.glacier > 0.5 ? 1 : 0
376f2ce6 2026-09-16 Pavel Safronov240 }
241 }
242
243 const vertCount = n * n + 4 * n
244 const positions = new Float32Array(vertCount * 3)
245 const normals = new Int8Array(vertCount * 3)
246 const materials = new Uint8Array(vertCount * MATERIALS.length)
247 const [cx, cy, cz] = tileCentreKm(t)
248 let minM = Infinity, maxM = -Infinity
249
250 const write = (index: number, u: number, v: number, drop: number): void => {
251 const k = (v + 1) * W + (u + 1)
252 const z = Math.min(LENGTH_M, Math.max(0, z0 + u * dz))
253 const theta = (a0 + v * da) / RADIUS_M
254 const cos = Math.cos(theta), sin = Math.sin(theta)
255 const h = shown[k]!
14d35b7d 2026-09-16 Pavel Safronov256 // A skirt hangs down past any crack, but never through the wall.
257 const r = (RADIUS_M - Math.max(LOWEST_GROUND_M, h - drop)) / 1000
376f2ce6 2026-09-16 Pavel Safronov258 positions[3 * index] = r * cos - cx
259 positions[3 * index + 1] = r * sin - cy
260 positions[3 * index + 2] = z / 1000 - cz
261
262 // Rise per metre of run along the axis and around it, from the neighbours.
263 const gz = (shown[k + 1]! - shown[k - 1]!) / (2 * dz)
264 const ga = (shown[k + W]! - shown[k - W]!) / (2 * da)
265 // up = -radial; deepward = +z; spinward = (-sin, cos, 0).
266 let nx = -cos + ga * sin
267 let ny = -sin - ga * cos
268 let nz = -gz
269 const len = Math.hypot(nx, ny, nz)
270 nx /= len; ny /= len; nz /= len
271 normals[3 * index] = Math.round(nx * 127)
272 normals[3 * index + 1] = Math.round(ny * 127)
273 normals[3 * index + 2] = Math.round(nz * 127)
274
275 // Slope matters for snow, and only now is it known.
276 const slope = Math.hypot(gz, ga)
277 const base = k * MATERIALS.length
e18dad04 2026-09-30 Pavel Safronov278 const holds = glacier[k] ? 1 : 1 - smoothstep(0.55, 1.1, slope)
376f2ce6 2026-09-16 Pavel Safronov279 for (let m = 0; m < MATERIALS.length; m++) {
280 const w = m === 4 ? mats[base + m]! * holds : mats[base + m]!
281 materials[index * MATERIALS.length + m] = Math.round(w * 255)
282 }
283 if (!drop) {
284 minM = Math.min(minM, h)
285 maxM = Math.max(maxM, h)
286 }
287 }
288
289 for (let v = 0; v < n; v++) for (let u = 0; u < n; u++) write(v * n + u, u, v, 0)
290
291 /**
292 * Skirts: a curtain hung from every edge, reaching down past any crack a coarser
293 * neighbour could open. Deep enough for the height error one level coarser, which
294 * scales with the tile, and never less than a few metres.
295 */
80bcc97a 2026-09-17 Pavel Safronov296 // On steep ground a coarser neighbour's edge can be off by far more than 4% of the
297 // tile: as much as the steepest step between two vertices here, and then some. A 50°
298 // gorge wall showed its cracks as a row of dark panels until skirts reached that far.
299 let steepest = 0
300 for (let v = 1; v <= n; v++) {
301 for (let u = 1; u <= n; u++) {
302 const k = v * W + u
303 steepest = Math.max(steepest, Math.abs(shown[k]! - shown[k + 1]!), Math.abs(shown[k]! - shown[k + W]!))
304 }
305 }
306 const drop = Math.max(8, 0.04 * Math.max(size.along, size.around), 2 * steepest)
376f2ce6 2026-09-16 Pavel Safronov307 let idx = n * n
308 for (let e = 0; e < n; e++) write(idx++, e, 0, drop)
309 for (let e = 0; e < n; e++) write(idx++, n - 1, e, drop)
310 for (let e = 0; e < n; e++) write(idx++, n - 1 - e, n - 1, drop)
311 for (let e = 0; e < n; e++) write(idx++, 0, n - 1 - e, drop)
312
00ceeedb 2026-09-27 Pavel Safronov313 const canopy = wooded ? buildCanopy(n, positions, cAttrs) : null
80bcc97a 2026-09-17 Pavel Safronov314 const water = buildWater(n, z0, a0, dz, da, [cx, cy, cz], wLevel, wAttrs, wet)
315 if (water) {
316 for (let w = 0; w < n * n; w++) {
317 if (Number.isFinite(wLevel[w]!)) { minM = Math.min(minM, wLevel[w]!); maxM = Math.max(maxM, wLevel[w]!) }
318 }
319 }
00ceeedb 2026-09-27 Pavel Safronov320 return { key: tileKey(t), positions, normals, materials, water, canopy, minM, maxM }
321}
322
323/** A stable 0..1 from a surface position: the same everywhere that position is asked about. */
324function hash01(z: number, a: number): number {
325 const x = Math.sin(z * 0.0173 + a * 0.0411) * 43758.5453
326 return x - Math.floor(x)
327}
328
329/**
330 * The canopy over a tile: the *ground* vertices again, with the canopy's height beside
331 * them. The shader lifts them and drops them back as the eye comes close, where the real
332 * trees take over (views/forest-view.ts) — so this mesh is the far view of a forest, and
333 * the ground it is built from is the ground it collapses onto exactly.
334 */
335function buildCanopy(n: number, ground: Float32Array, attrs: Float32Array): TileCanopy | null {
336 const positions = Float32Array.from(ground)
337 const idx: number[] = []
338 const has = (k: number): boolean => attrs[k * CANOPY_ATTRS]! > 0.5
339 for (let v = 0; v < n - 1; v++) {
340 for (let u = 0; u < n - 1; u++) {
341 const a = v * n + u, b = a + 1, c = a + n, d = c + 1
342 if (has(a) && has(b) && has(c)) idx.push(a, b, c)
343 if (has(b) && has(d) && has(c)) idx.push(b, d, c)
344 }
345 }
346 if (!idx.length) return null
347 return { positions, attrs, indices: new Uint16Array(idx) }
80bcc97a 2026-09-17 Pavel Safronov348}
349
350/**
351 * The water surface for a tile. Every wet vertex carries its water's level; a dry vertex
352 * next to one borrows that level, so the surface runs a little way under the bank and the
353 * depth test draws the shoreline exactly where water meets ground. A triangle is kept if
354 * all three corners have a level and at least one is wet.
355 */
356function buildWater(
357 n: number, z0: number, a0: number, dz: number, da: number, centre: [number, number, number],
358 level: Float64Array, attrs: Float32Array, wet: Uint8Array,
359): TileWater | null {
360 if (!wet.some((w) => w)) return null
361 for (let v = 0; v < n; v++) {
362 for (let u = 0; u < n; u++) {
363 const w = v * n + u
364 if (wet[w]) continue
365 let best = -Infinity, from = -1
366 for (let dv = -1; dv <= 1; dv++) {
367 for (let du = -1; du <= 1; du++) {
368 const uu = u + du, vv = v + dv
369 if (uu < 0 || vv < 0 || uu >= n || vv >= n) continue
370 const q = vv * n + uu
371 if (wet[q] && level[q]! > best) { best = level[q]!; from = q }
372 }
373 }
374 if (from < 0) continue
375 level[w] = best
376 attrs.copyWithin(w * WATER_ATTRS, from * WATER_ATTRS, (from + 1) * WATER_ATTRS)
377 attrs[w * WATER_ATTRS] = 0 // no depth on the bank
378 }
379 }
380 const positions = new Float32Array(n * n * 3)
381 const [cx, cy, cz] = centre
382 for (let v = 0; v < n; v++) {
383 const theta = (a0 + v * da) / RADIUS_M
384 const cos = Math.cos(theta), sin = Math.sin(theta)
385 for (let u = 0; u < n; u++) {
386 const w = v * n + u
387 if (!Number.isFinite(level[w]!)) continue
388 const r = (RADIUS_M - level[w]!) / 1000
389 const z = Math.min(LENGTH_M, Math.max(0, z0 + u * dz))
390 positions[3 * w] = r * cos - cx
391 positions[3 * w + 1] = r * sin - cy
392 positions[3 * w + 2] = z / 1000 - cz
393 }
394 }
395 const idx: number[] = []
396 const has = (w: number): boolean => Number.isFinite(level[w]!)
397 for (let v = 0; v < n - 1; v++) {
398 for (let u = 0; u < n - 1; u++) {
399 const a = v * n + u, b = a + 1, c = a + n, d = c + 1
400 if (has(a) && has(b) && has(c) && (wet[a] || wet[b] || wet[c])) idx.push(a, b, c)
401 if (has(b) && has(d) && has(c) && (wet[b] || wet[d] || wet[c])) idx.push(b, d, c)
402 }
403 }
404 if (!idx.length) return null
405 return { positions, attrs, indices: new Uint16Array(idx) }
376f2ce6 2026-09-16 Pavel Safronov406}
407
408const indexCache = new Map<number, Uint16Array>()
409
410/**
411 * Triangle indices for a tile, shared by every tile of the same grid. Wound so the
412 * face points up — toward the axis — which is the side anyone ever sees.
413 *
414 * At vertex (u, v), +u is deepward and +v spinward, and deepward × spinward is up.
415 * So (u,v) → (u+1,v) → (u,v+1) is counter-clockwise seen from above: a front face.
416 */
417export function tileIndices(n: TileGrid): Uint16Array {
418 let idx = indexCache.get(n)
419 if (idx) return idx
420 const out: number[] = []
421 const at = (u: number, v: number): number => v * n + u
422 for (let v = 0; v < n - 1; v++) {
423 for (let u = 0; u < n - 1; u++) {
424 const a = at(u, v), b = at(u + 1, v), c = at(u, v + 1), d = at(u + 1, v + 1)
425 out.push(a, b, c, b, d, c)
426 }
427 }
428 // Skirts: each edge vertex joined to its dropped copy, following the edge.
429 const edge = (e: number): [number, number] => {
430 // Grid vertex and skirt vertex for step e along the perimeter, in skirt order.
431 const side = Math.floor(e / n), k = e % n
432 const grid = side === 0 ? at(k, 0)
433 : side === 1 ? at(n - 1, k)
434 : side === 2 ? at(n - 1 - k, n - 1)
435 : at(0, n - 1 - k)
436 return [grid, n * n + e]
437 }
438 for (let e = 0; e < 4 * n; e++) {
439 if (e % n === n - 1) continue
440 const [g0, s0] = edge(e)
441 const [g1, s1] = edge(e + 1)
442 // Both windings: a skirt is seen from whichever side the crack is on.
443 out.push(g0, s0, g1, g1, s0, s1, g0, g1, s0, g1, s1, s0)
444 }
445 // 65² + 4·65 vertices fits in 16 bits with room to spare.
446 idx = new Uint16Array(out)
447 indexCache.set(n, idx)
448 return idx
449}