@ortho-earth/voxel 0.4.0 → 0.6.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/README.md CHANGED
@@ -9,8 +9,9 @@ Voxel grids for [`@ortho-earth/globe`](../../packages/globe). The plugin fills t
9
9
  - They are **picked** by the unified picking: `handle.pick(x, y)`, `map.on("click", id, …)` and `queryRenderedFeatures({ layers: [id] })`. A hit carries the cell `index`, its `[i, j, k]` `cell`, the cell's `value` (scalar grids, via `handle.pick`) and your `properties`. Picking marches only the pixels under the cursor (scissor), so it stays cheap.
10
10
  - **Preparation** runs in a worker (`voxel-reader.ts`): values are quantised to 8 bits (`R8`, or `RGBA8` for `components: 4`), the colormap is baked, the box is placed or the region shell is tessellated; the main thread only forwards the buffers.
11
11
  - **Large grids** are split into **bricks**: a grid with an axis beyond 256 cells (the 3D texture size every WebGL2 device guarantees) becomes bricks of 126 cells per axis, one texture each with a 1-cell apron so trilinear sampling and iso gradients run across the seams. Bricks are drawn far to near, a brick whose cells are all below `threshold` (or empty) is skipped, and `clip`, `threshold`, picking (`index` / `cell` of the whole grid) and every style change work across them. `handle.bricks` says how many.
12
- - **Time**: `times` (ms, ascending) makes `values` a run of frames, one per time. The layer follows `map.clock` by default (the last frame at or before the clock's time; scrub or play the clock and the frame changes without re-uploading), or `time: <ms>` pins one frame. One `range` covers every frame so colours compare across time.
13
- - Stages 1–4 are done (box and region grids, blocks / volume / iso, clipping, WebGPU and WebGL2, 3D Tiles `EXT_primitive_voxels`); stage 5 has bricks and time, while level of detail and file readers (NetCDF, Zarr) follow. See [PLAN.md](./PLAN.md).
12
+ - **Time**: `times` (ms, ascending) makes `values` a run of frames, one per time. The layer follows `map.clock` by default (the last frame at or before the clock's time; scrub or play the clock and the frame changes without re-uploading), or `time: <ms>` pins one frame. One `range` covers every frame so colours compare across time. Only the frame shown is on the GPU; the others wait in the overlay's memory and go up when the clock reaches them.
13
+ - **GPU budget**: `voxelPlugin({ budget })` (MB, default 512, 128 on touch devices) caps the voxel textures of a map. A grid bigger than half of it also gets coarser levels (1/2, 1/4… cells per axis, averaging the non-empty cells), per brick. When the layers in view would not fit, the farthest ones (by distance over cell size) are drawn from a coarser level, and the textures of layers out of view are let go. `handle.levels` says how many coarser levels were made.
14
+ - Stages 1–4 are done (box and region grids, blocks / volume / iso, clipping, WebGPU and WebGL2, 3D Tiles `EXT_primitive_voxels`); stage 5 has bricks, time, the GPU budget with levels of detail, and NetCDF / Zarr readers. See [PLAN.md](./PLAN.md).
14
15
 
15
16
  ```js
16
17
  import { createGlobe } from "@ortho-earth/globe";
@@ -40,6 +41,15 @@ field.backend; // "webgpu" | "webgl2
40
41
  map.on("click", "field", e => console.log(e.features[0].properties.cell));
41
42
  ```
42
43
 
44
+ ## Files: NetCDF and Zarr
45
+
46
+ `map.addData(url, { kind: "voxel", variable?, frames?, heights? })` reads a grid file in the reader worker and draws it as a `region` grid:
47
+
48
+ - **NetCDF classic** (CDF-1, CDF-2, CDF-5): a `.nc` URL, or its bytes (`ArrayBuffer` / `Uint8Array`). NetCDF-4 (HDF5) is not read yet; `nccopy -k classic in.nc out.nc` converts it.
49
+ - **Zarr** v2 and v3: a `.zarr` URL (the store's root). Chunks may be uncompressed or gzip / zlib (v2) and `bytes` + `gzip` (v3); blosc, zstd and sharding are not read yet. With consolidated metadata (`.zmetadata`, or `consolidated_metadata` in `zarr.json`) the variable is found by itself; otherwise pass `variable`. Only the chunks of the frames asked for are fetched.
50
+ - The variable is `(time?, z?, y, x)`, with x the last (fastest) dimension. Coordinates follow CF: `degrees_east` / `degrees_north` (or `lon` / `lat`), a vertical coordinate in m / km (`positive: "down"` and depth go below zero) or Pa / hPa (the standard atmosphere, spacing taken as even), and `<unit> since <date>` times, which become `times` for `map.clock`. Descending latitude or height is flipped, cell centres become the region's edges, and `scale_factor`, `add_offset`, `_FillValue`, `missing_value` and `valid_*` are applied (missing cells are empty).
51
+ - `variable` picks the variable (default: the non-coordinate numeric variable with the most dimensions), `frames: [start, end)` reads part of the time dimension, and `heights: [h0, h1]` (m) overrides the vertical extent. `handle.source` holds what was read: `format`, `variable`, `units`, `longName`, `dims`, `attrs`.
52
+
43
53
  ## VoxelData
44
54
 
45
55
  | field | meaning |
@@ -74,12 +84,12 @@ map.on("click", "field", e => console.log(e.features[0].properties.cell));
74
84
 
75
85
  ## Handle
76
86
 
77
- `id`, `kind`, `shape`, `dims`, `n` (cells), `bricks` (0 = one texture), `times` (null without frames), `frame` (the frame shown), `anchor`, `range`, `backend` (`"webgpu"` | `"webgl2"`, null until the overlay has chosen), `reader` (`"worker"` | `"main"`), `error` (the renderer refused the layer, e.g. a grid beyond the 3D texture limit), `ready`, `setData(data)`, `setProps(opts)` (style-only changes do not re-upload the grid), `setVisible(on)`, `pick(x, y, r?)` (the value of the frame shown), `remove()`.
87
+ `id`, `kind`, `shape`, `dims`, `n` (cells), `bricks` (0 = one texture), `levels` (coarser levels for the budget, 0 = none), `source` (what a NetCDF / Zarr file was read as, null for VoxelData), `times` (null without frames), `frame` (the frame shown), `anchor`, `range`, `backend` (`"webgpu"` | `"webgl2"`, null until the overlay has chosen), `reader` (`"worker"` | `"main"`), `error` (the renderer refused the layer, e.g. a grid beyond the 3D texture limit), `ready`, `setData(data)`, `setProps(opts)` (style-only changes do not re-upload the grid), `setVisible(on)`, `pick(x, y, r?)` (the value of the frame shown), `remove()`.
78
88
 
79
89
  ## Notes
80
90
 
81
91
  - Scalar values are quantised to 8 bits for the GPU (`range` → 1..255, 0 = empty). The colormap resolves 256 steps and an iso threshold lands on one of them; `handle.pick` reports the original value from the data kept on the main thread.
82
- - A grid with every axis within 256 cells is one texture; a larger one is split into bricks (above). The bricks are kept in GPU memory as a whole (1 byte per cell, 4 for RGBA): a 1024 × 1024 × 128 grid needs 128 MB, so very large grids need level of detail (still to come) on phones.
92
+ - A grid with every axis within 256 cells is one texture; a larger one is split into bricks (above). The bricks are kept in GPU memory as a whole (1 byte per cell, 4 for RGBA): a 1024 × 1024 × 128 grid needs 128 MB; the GPU budget (above) draws far bricks from coarser levels when that does not fit.
83
93
  - Region grids are marched in meters along a straight ray and each sample is converted back to longitude / latitude / height, so cell borders are the true meridians, parallels and height surfaces. Their blocks mode samples the nearest cell every quarter cell instead of a DDA (slightly soft edges). The shell is built on a sphere; under the ellipsoid display (`?ell=1`) a country-sized region can sit a few km off, which stage 3 will tighten. Very large regions (thousands of km) shade with the anchor's east / north / up.
84
94
  - All grids of one map share one overlay canvas (the render worker's WebGL contexts are limited); the overlay goes away a moment (`OVERLAY_LINGER_MS`, 250 ms) after the last handle is removed, so `remove()` followed at once by `addData()` (switching scenes) keeps the same overlay instead of tearing it down and racing a new one with the same name.
85
95
  - **3D Tiles**: a tileset whose glTF content carries `EXT_primitive_voxels` (with `KHR_implicit_shapes` boxes or `EXT_implicit_ellipsoid_region` regions), or whose `root.content` declares `3DTILES_content_voxels` (the grid fills the tile's bounding volume), becomes one layer per tile through `map.add3DTiles(url, { voxel: { render, colormap, threshold, property, … } })`; `property` picks the attribute (a class property name works too). Cylinders are not drawn yet; without this plugin the globe skips the grids and warns once.
package/dist/grid.d.ts CHANGED
@@ -16,6 +16,8 @@ export interface PrepareOptions {
16
16
  threshold?: number | null;
17
17
  /** 楕円体表示の b/a(region の殻を本体と同じ形で作る・既定 1=球) */
18
18
  rAx?: number;
19
+ /** LoD=一こまの GPU の大きさ(bytes)がこれを越える格子は粗い段(各軸 1/2・1/4…)も作る(既定=作らない) */
20
+ lodBytes?: number | null;
19
21
  }
20
22
  /** region(経緯度×高さの格子)の描き手向けの欄 */
21
23
  export interface GeoGrid {
@@ -110,27 +112,35 @@ export declare function frameAt(times: ArrayLike<number>, ms: number): number;
110
112
  export declare const thrOf: (threshold: number | null | undefined, range: [number, number]) => number;
111
113
  /** VoxelData → 描き手へ渡す物(worker で。配列は transfer できる物)。値は閾で切らない(閾は描き手が見る=等値面は場の全体が要る) */
112
114
  export declare function prepare(data: unknown, o?: PrepareOptions): Prepared;
115
+ /** 段(ブリック・粗い段)の配列=transfer する物 */
116
+ export declare const levelBuffers: (l: Level) => ArrayBuffer[];
113
117
  export declare const transferables: (p: Prepared | PreparedGrid) => ArrayBuffer[];
114
118
  /** 一枚のテクスチャで描く格子の上限(各軸) */
115
119
  export declare const BRICK_LIMIT = 256;
116
120
  /** ブリックの持ち分(各軸のセル数)=テクスチャは糊代を足して最大 BRICK + 2 */
117
121
  export declare const BRICK = 126;
118
- export interface Brick {
119
- /** テクスチャの 0 番のセルの親の格子での位置 */ off: [number, number, number];
120
- /** 描く範囲(テクスチャのセル座標)[x0, y0, z0, x1, y1, z1] */ own: [number, number, number, number, number, number];
122
+ /** 一つの段=ブリック(s=1)か、その粗い段(s=2, 4…=セル 1 個が元の s 個ぶん)。座標はその段のセル */
123
+ export interface Level {
124
+ /** 縮めた倍率(1=元の格子) */ s: number;
125
+ /** テクスチャの 0 番のセルの、その段の親の格子での位置 */ off: [number, number, number];
126
+ /** 描く範囲(テクスチャのセル座標・粗い段は端数あり)[x0, y0, z0, x1, y1, z1] */ own: [number, number, number, number, number, number];
121
127
  dims: [number, number, number];
122
- n: number;
123
128
  tex: Uint8Array; /** こまごとのテクスチャ(frames[0]=tex)・こまが無ければ null */
124
129
  frames: Uint8Array[] | null; /** 最大値(R8 の 0〜255・RGBA は α・こまを通して) */
125
130
  vmax: number;
126
- anchor: [number, number, number];
131
+ /** box=親の錨と A×s(セル 1 個ぶん)・region=部分の region */ anchor: [number, number, number];
127
132
  A: Float64Array;
128
133
  radius: number;
129
134
  geo: GeoGrid | null;
130
135
  }
136
+ export interface Brick extends Level {
137
+ n: number; /** 粗い段(s=2, 4…)・作らなければ null */
138
+ lods: Level[] | null;
139
+ }
131
140
  export interface PreparedGrid extends Prepared {
132
141
  vmax: number; /** 分けた時のブリック(tex は空) */
133
- bricks: Brick[] | null;
142
+ bricks: Brick[] | null; /** 一枚の格子の粗い段(ブリックは各ブリックの lods)・作らなければ null */
143
+ lods: Level[] | null;
134
144
  }
135
145
  /** 詰めた値の最大(R8 はそのまま・RGBA は α) */
136
146
  export declare function maxOf(tex: Uint8Array, comps: 1 | 4): number;
@@ -145,7 +155,26 @@ export declare function parentIndex(index: number, dims: ArrayLike<number>, off:
145
155
  index: number;
146
156
  cell: [number, number, number];
147
157
  };
148
- /** prepare の大きな格子版=上限を越えればブリックに分ける(越えなければ prepare と同じ+vmax) */
158
+ /** 粗い段の数の上限(1/16 まで) */
159
+ export declare const LOD_MAX = 4;
160
+ /** 段 s の格子の大きさ(各軸 ceil(d/s)) */
161
+ export declare const levelDims: (dims: ArrayLike<number>, s: number) => [number, number, number];
162
+ /** 作る粗い段の数=一こまが lodBytes を越えれば、一番粗い段が lodBytes/4 以下になるまで(LOD_MAX まで)。越えなければ 0 */
163
+ export declare function levelCount(dims: ArrayLike<number>, comps: number, lodBytes: number | null | undefined): number;
164
+ /** 各軸 1/2 に縮める(R8=空でない子の平均・RGBA=α > 0 の子の平均・全部空なら空) */
165
+ export declare function downsample(tex: Uint8Array, dims: [number, number, number], comps: 1 | 4): {
166
+ tex: Uint8Array;
167
+ dims: [number, number, number];
168
+ };
169
+ /** 段の並び(s=2, 4…)=こまごとに縮める */
170
+ export declare function pyramid(frames: Uint8Array[], dims: [number, number, number], comps: 1 | 4, L: number): Array<{
171
+ s: number;
172
+ dims: [number, number, number];
173
+ frames: Uint8Array[];
174
+ }>;
175
+ /** 段 s の格子(frames)から、元の格子の持ち分 [lo, hi)(元のセル)を糊代 1 セルつきで切り出す。s=1 はブリックそのもの */
176
+ export declare function makeLevel(frames: Uint8Array[], Dl: [number, number, number], comps: 1 | 4, s: number, lo: ArrayLike<number>, hi: ArrayLike<number>, parent: Placement, pgeo: GeoGrid | null, dims: [number, number, number], rAx?: number): Level;
177
+ /** prepare の大きな格子版=上限を越えればブリックに分ける(越えなければ prepare と同じ+vmax)。o.lodBytes を越える格子は粗い段(lods)も作る */
149
178
  export declare function prepareGrid(data: unknown, o?: PrepareOptions, brick?: number, limit?: number): PreparedGrid;
150
179
  export interface RayHit {
151
180
  cell: [number, number, number];
package/dist/grid.js CHANGED
@@ -304,8 +304,12 @@ export function prepare(data, o = {}) {
304
304
  const cmap = buildColormap(o.colormap, o.alphaRamp ?? null);
305
305
  return { shape: d.shape, dims: [dims[0], dims[1], dims[2]], comps, n, tex: frames[0], cmap, anchor, A, radius, range, thr, placed, geo, times: d.times ? Array.from(d.times) : null, frames: T > 1 ? frames : null };
306
306
  }
307
- export const transferables = (p) => [p.tex.buffer, p.cmap.buffer, p.A.buffer, ...(p.geo ? [p.geo.shell.buffer] : []), ...(p.frames ?? []).map(f => f.buffer),
308
- ...("bricks" in p && p.bricks ? p.bricks.flatMap(b => [b.tex.buffer, b.A.buffer, ...(b.geo ? [b.geo.shell.buffer] : []), ...(b.frames ?? []).map(f => f.buffer)]) : [])].filter((b, i, a) => a.indexOf(b) === i);
307
+ /** 段(ブリック・粗い段)の配列=transfer する物 */
308
+ export const levelBuffers = (l) => [l.tex.buffer, l.A.buffer, ...(l.geo ? [l.geo.shell.buffer] : []), ...(l.frames ?? []).map(f => f.buffer)];
309
+ const uniq = (a) => a.filter((b, i) => a.indexOf(b) === i);
310
+ export const transferables = (p) => uniq([p.tex.buffer, p.cmap.buffer, p.A.buffer, ...(p.geo ? [p.geo.shell.buffer] : []), ...(p.frames ?? []).map(f => f.buffer),
311
+ ...("lods" in p && p.lods ? p.lods.flatMap(levelBuffers) : []),
312
+ ...("bricks" in p && p.bricks ? p.bricks.flatMap(b => [...levelBuffers(b), ...(b.lods ?? []).flatMap(levelBuffers)]) : [])]);
309
313
  // ---- 大きな格子=ブリック(第 5 段・2026-10-24)----
310
314
  // 一軸でも BRICK_LIMIT(WebGL2 の 3D テクスチャの最低保証)を越える格子は、持ち分 BRICK セル角のブリックに分けて別々のテクスチャにする。
311
315
  // 各ブリックのテクスチャは持ち分の前後に 1 セルずつ糊代(隣のセル)を足す=三線形・等値面の勾配が継ぎ目で途切れない。描くのは持ち分(own)だけ=体積が継ぎ目で二重に重ならない。
@@ -344,15 +348,109 @@ export function parentIndex(index, dims, off, full) {
344
348
  const cell = [i + off[0], j + off[1], k + off[2]];
345
349
  return { index: cell[0] + full[0] * (cell[1] + full[1] * cell[2]), cell };
346
350
  }
347
- /** prepare の大きな格子版=上限を越えればブリックに分ける(越えなければ prepare と同じ+vmax) */
351
+ // ---- LoD(第 5 段 ③・2026-10-25)----
352
+ // GPU の予算(plugin の budget)を越えそうな格子は、各軸 1/2・1/4…に縮めた段も作る(ブリックはブリックごと・こまもこまごと)。どの段を GPU に置くかは描き手が決める(voxel-gl.ts planLevels=遠い層から粗く)。
353
+ // 縮め方=子の 2×2×2 のうち空でないセルの平均(全部空なら空)=薄い壁や疎な点も消えない。粗い段の箱は元の格子の s 倍のセル=端の奇数は半端な描く範囲(own の端数)で切る。
354
+ /** 粗い段の数の上限(1/16 まで) */
355
+ export const LOD_MAX = 4;
356
+ /** 段 s の格子の大きさ(各軸 ceil(d/s)) */
357
+ export const levelDims = (dims, s) => [Math.ceil(dims[0] / s), Math.ceil(dims[1] / s), Math.ceil(dims[2] / s)];
358
+ /** 作る粗い段の数=一こまが lodBytes を越えれば、一番粗い段が lodBytes/4 以下になるまで(LOD_MAX まで)。越えなければ 0 */
359
+ export function levelCount(dims, comps, lodBytes) {
360
+ const bytes = (s) => { const d = levelDims(dims, s); return d[0] * d[1] * d[2] * comps; };
361
+ if (lodBytes == null || !(bytes(1) > lodBytes))
362
+ return 0;
363
+ let L = 1;
364
+ while (L < LOD_MAX && bytes(2 ** L) > lodBytes / 4)
365
+ L++;
366
+ return L;
367
+ }
368
+ /** 各軸 1/2 に縮める(R8=空でない子の平均・RGBA=α > 0 の子の平均・全部空なら空) */
369
+ export function downsample(tex, dims, comps) {
370
+ const D = levelDims(dims, 2), out = new Uint8Array(D[0] * D[1] * D[2] * comps), acc = [0, 0, 0, 0];
371
+ for (let k = 0; k < D[2]; k++)
372
+ for (let j = 0; j < D[1]; j++)
373
+ for (let i = 0; i < D[0]; i++) {
374
+ let cnt = 0;
375
+ acc[0] = acc[1] = acc[2] = acc[3] = 0;
376
+ for (let dz = 0; dz < 2; dz++) {
377
+ const z = 2 * k + dz;
378
+ if (z >= dims[2])
379
+ break;
380
+ for (let dy = 0; dy < 2; dy++) {
381
+ const y = 2 * j + dy;
382
+ if (y >= dims[1])
383
+ break;
384
+ for (let dx = 0; dx < 2; dx++) {
385
+ const x = 2 * i + dx;
386
+ if (x >= dims[0])
387
+ break;
388
+ const c = (x + dims[0] * (y + dims[1] * z)) * comps;
389
+ if (comps === 1) {
390
+ if (tex[c]) {
391
+ acc[0] += tex[c];
392
+ cnt++;
393
+ }
394
+ }
395
+ else if (tex[c + 3]) {
396
+ for (let a = 0; a < 4; a++)
397
+ acc[a] += tex[c + a];
398
+ cnt++;
399
+ }
400
+ }
401
+ }
402
+ }
403
+ if (!cnt)
404
+ continue;
405
+ const o = (i + D[0] * (j + D[1] * k)) * comps;
406
+ for (let a = 0; a < comps; a++)
407
+ out[o + a] = Math.round(acc[a] / cnt);
408
+ }
409
+ return { tex: out, dims: D };
410
+ }
411
+ /** 段の並び(s=2, 4…)=こまごとに縮める */
412
+ export function pyramid(frames, dims, comps, L) {
413
+ const out = [];
414
+ let cur = frames, d = dims;
415
+ for (let l = 1; l <= L; l++) {
416
+ const r = cur.map(f => downsample(f, d, comps));
417
+ cur = r.map(x => x.tex);
418
+ d = r[0].dims;
419
+ out.push({ s: 2 ** l, dims: d, frames: cur });
420
+ }
421
+ return out;
422
+ }
423
+ /** 段 s の格子(frames)から、元の格子の持ち分 [lo, hi)(元のセル)を糊代 1 セルつきで切り出す。s=1 はブリックそのもの */
424
+ export function makeLevel(frames, Dl, comps, s, lo, hi, parent, pgeo, dims, rAx = 1) {
425
+ const clo = [0, 1, 2].map(i => lo[i] / s), chi = [0, 1, 2].map(i => hi[i] / s);
426
+ const off = [0, 1, 2].map(i => Math.max(0, Math.floor(clo[i]) - 1)), end = [0, 1, 2].map(i => Math.min(Dl[i], Math.ceil(chi[i]) + 1));
427
+ const bd = [0, 1, 2].map(i => end[i] - off[i]);
428
+ const fr = frames.map(f => extract(f, Dl, comps, off, bd)), tex = fr[0];
429
+ const own = [clo[0] - off[0], clo[1] - off[1], clo[2] - off[2], chi[0] - off[0], chi[1] - off[1], chi[2] - off[2]];
430
+ const vmax = Math.max(...fr.map(f => maxOf(f, comps)));
431
+ const base = { s, off, own, dims: bd, tex, frames: fr.length > 1 ? fr : null, vmax };
432
+ if (pgeo) {
433
+ const g = pgeo, f = (i, v) => v * s / dims[i]; // その段のセル → 元の割合(粗い段の端は元の外へ半端に出る=own で切る)
434
+ const r = placeRegion([g.west + g.spanLon * f(0, off[0]), g.south + g.spanLat * f(1, off[1]), g.west + g.spanLon * f(0, end[0]), g.south + g.spanLat * f(1, end[1]), g.h0 + (g.h1 - g.h0) * f(2, off[2]), g.h0 + (g.h1 - g.h0) * f(2, end[2])], bd, rAx);
435
+ return { ...base, anchor: r.anchor, A: r.A, radius: r.radius, geo: r.geo };
436
+ }
437
+ const A = Float64Array.from(parent.A, v => v * s); // セル 1 個=元の s 個ぶん(列ごとに同じ倍率)
438
+ return { ...base, anchor: parent.anchor, A, radius: parent.radius, geo: null };
439
+ }
440
+ /** 値 → 詰めた一こまずつ(R8/RGBA8) */
441
+ const packFrames = (d, range, n) => { const T = d.times?.length ?? 1, out = []; for (let t = 0; t < T; t++) {
442
+ const v = T > 1 ? frameValues(d.values, n, d.components, t) : d.values;
443
+ out.push(d.components === 4 ? toRgba8(v, d.noData) : quantize(v, range, d.noData, null));
444
+ } return out; };
445
+ /** prepare の大きな格子版=上限を越えればブリックに分ける(越えなければ prepare と同じ+vmax)。o.lodBytes を越える格子は粗い段(lods)も作る */
348
446
  export function prepareGrid(data, o = {}, brick = BRICK, limit = BRICK_LIMIT) {
349
- const d = checkVoxelData(data), dims = d.dims;
447
+ const d = checkVoxelData(data), dims = d.dims, comps = d.components, L = levelCount(dims, comps, o.lodBytes);
350
448
  if (!needsBricks(dims, limit)) {
351
- const p = prepare(d, o);
352
- return { ...p, vmax: Math.max(...(p.frames ?? [p.tex]).map(f => maxOf(f, p.comps))), bricks: null };
449
+ const p = prepare(d, o), fr = p.frames ?? [p.tex], vmax = Math.max(...fr.map(f => maxOf(f, p.comps)));
450
+ const lods = L ? pyramid(fr, dims, comps, L).map(lv => makeLevel(lv.frames, lv.dims, comps, lv.s, [0, 0, 0], dims, p, p.geo, dims, o.rAx ?? 1)) : null;
451
+ return { ...p, vmax, bricks: null, lods };
353
452
  }
354
- const T = d.times?.length ?? 1, nAll = dims[0] * dims[1] * dims[2];
355
- const comps = d.components;
453
+ const nAll = dims[0] * dims[1] * dims[2];
356
454
  let range = [0, 1];
357
455
  if (comps === 1) {
358
456
  range = o.range ?? d.range ?? valueRange(d.values, d.noData);
@@ -373,35 +471,19 @@ export function prepareGrid(data, o = {}, brick = BRICK, limit = BRICK_LIMIT) {
373
471
  parent = r;
374
472
  pgeo = r.geo;
375
473
  }
474
+ const full = packFrames(d, range, nAll), pyr = L ? pyramid(full, dims, comps, L) : [];
376
475
  const sx = brickSpans(dims[0], brick), sy = brickSpans(dims[1], brick), sz = brickSpans(dims[2], brick), bricks = [];
377
476
  let vmax = 0;
378
477
  for (const [z0, z1] of sz)
379
478
  for (const [y0, y1] of sy)
380
479
  for (const [x0, x1] of sx) {
381
480
  const lo = [x0, y0, z0], hi = [x1, y1, z1];
382
- const off = [0, 1, 2].map(i => Math.max(0, lo[i] - 1)), end = [0, 1, 2].map(i => Math.min(dims[i], hi[i] + 1));
383
- const bd = [0, 1, 2].map(i => end[i] - off[i]);
384
- const fr = [];
385
- for (let t = 0; t < T; t++) {
386
- const sub = extract(T > 1 ? frameValues(d.values, nAll, comps, t) : d.values, dims, comps, off, bd);
387
- fr.push(comps === 4 ? toRgba8(sub, d.noData) : quantize(sub, range, d.noData, null));
388
- }
389
- const tex = fr[0], frames = T > 1 ? fr : null;
390
- const vm = Math.max(...fr.map(f => maxOf(f, comps)));
391
- vmax = Math.max(vmax, vm);
392
- const own = [lo[0] - off[0], lo[1] - off[1], lo[2] - off[2], hi[0] - off[0], hi[1] - off[1], hi[2] - off[2]];
393
- let b;
394
- if (pgeo) {
395
- const g = pgeo, f = (i, v) => v / dims[i];
396
- const r = placeRegion([g.west + g.spanLon * f(0, off[0]), g.south + g.spanLat * f(1, off[1]), g.west + g.spanLon * f(0, end[0]), g.south + g.spanLat * f(1, end[1]), g.h0 + (g.h1 - g.h0) * f(2, off[2]), g.h0 + (g.h1 - g.h0) * f(2, end[2])], bd, o.rAx ?? 1);
397
- b = { off, own, dims: bd, n: bd[0] * bd[1] * bd[2], tex, frames, vmax: vm, anchor: r.anchor, A: r.A, radius: r.radius, geo: r.geo };
398
- }
399
- else
400
- b = { off, own, dims: bd, n: bd[0] * bd[1] * bd[2], tex, frames, vmax: vm, anchor: parent.anchor, A: Float64Array.from(parent.A), radius: parent.radius, geo: null };
401
- bricks.push(b);
481
+ const b = makeLevel(full, dims, comps, 1, lo, hi, parent, pgeo, dims, o.rAx ?? 1);
482
+ vmax = Math.max(vmax, b.vmax);
483
+ bricks.push({ ...b, n: b.dims[0] * b.dims[1] * b.dims[2], lods: L ? pyr.map(lv => makeLevel(lv.frames, lv.dims, comps, lv.s, lo, hi, parent, pgeo, dims, o.rAx ?? 1)) : null });
402
484
  }
403
485
  const cmap = buildColormap(o.colormap, o.alphaRamp ?? null);
404
- return { shape: d.shape, dims: [dims[0], dims[1], dims[2]], comps, n: dims[0] * dims[1] * dims[2], tex: new Uint8Array(0), cmap, anchor: parent.anchor, A: parent.A, radius: parent.radius, range, thr: comps === 4 ? 0 : thrOf(o.threshold, range), placed: parent.placed, geo: null, times: d.times ? Array.from(d.times) : null, frames: null, vmax, bricks };
486
+ return { shape: d.shape, dims: [dims[0], dims[1], dims[2]], comps, n: nAll, tex: new Uint8Array(0), cmap, anchor: parent.anchor, A: parent.A, radius: parent.radius, range, thr: comps === 4 ? 0 : thrOf(o.threshold, range), placed: parent.placed, geo: null, times: d.times ? Array.from(d.times) : null, frames: null, vmax, bricks, lods: null };
405
487
  }
406
488
  /** 光線と箱 [0, dims] の交わり(t0, t1)。無ければ null */
407
489
  export function rayBox(ro, rd, dims) {
package/dist/index.d.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  import type { GlobeMap, OrthoPlugin, OverlayHandle, OverlayPick, VoxelData } from "@ortho-earth/globe";
2
2
  import type { ColormapSpec, RenderMode, Clip } from "./grid.js";
3
- export { prepare, checkVoxelData, checkClip, thrOf, buildColormap, colorStops, parseColor, quantize, toRgba8, valueRange, placeBox, placeRegion, unitAt, hprMat, enuBasis, ecefToLngLat, ecefEnuRows, inv3, mul3, rayBox, marchBlocks, COLORMAPS, COLORMAP_NAMES, SHELL_N, prepareGrid, maxOf, needsBricks, brickSpans, extract, parentIndex, frameValues, frameAt, BRICK, BRICK_LIMIT } from "./grid.js";
4
- export type { PrepareOptions, Prepared, PreparedGrid, Brick, ColormapSpec, ColorStop, RenderMode, Clip, Hpr, Placement, GeoGrid, RayHit, EnuBasis, Mat3 } from "./grid.js";
5
- export type { VoxelMessage, VoxelLayerBody, VoxelStyle, VoxelPickResult, VoxelAltitude, OverlayHost, CamState, VoxelApi, PickContext } from "./voxel-gl.js";
3
+ import type { SourceMeta } from "./sources.js";
4
+ export { prepare, checkVoxelData, checkClip, thrOf, buildColormap, colorStops, parseColor, quantize, toRgba8, valueRange, placeBox, placeRegion, unitAt, hprMat, enuBasis, ecefToLngLat, ecefEnuRows, inv3, mul3, rayBox, marchBlocks, COLORMAPS, COLORMAP_NAMES, SHELL_N, prepareGrid, maxOf, needsBricks, brickSpans, extract, parentIndex, frameValues, frameAt, BRICK, BRICK_LIMIT, LOD_MAX, levelDims, levelCount, downsample, pyramid, makeLevel, levelBuffers } from "./grid.js";
5
+ export type { PrepareOptions, Prepared, PreparedGrid, Brick, Level, ColormapSpec, ColorStop, RenderMode, Clip, Hpr, Placement, GeoGrid, RayHit, EnuBasis, Mat3 } from "./grid.js";
6
+ export type { VoxelMessage, VoxelLayerBody, LevelBody, VoxelStyle, VoxelPickResult, VoxelAltitude, OverlayHost, CamState, VoxelApi, PickContext } from "./voxel-gl.js";
6
7
  export type { VoxelBackend, VoxelDrawOpts } from "./voxel-draw.js";
8
+ export { readSource, parseNetCDF, sniffNetCDF, openZarr, zarrArray, readZarr, toVoxelData, pickVariable, axisOf, cfTimes, heightsOf, pressureToHeight, edges, unpack, isZarrUrl } from "./sources.js";
9
+ export type { SourceOptions, SourceMeta, SourceData } from "./sources.js";
7
10
  export type { VoxelGpuModule } from "./voxel-gpu.js";
8
11
  import type { VoxelBackend } from "./voxel-draw.js";
9
- export declare const VERSION = "0.4.0";
12
+ export declare const VERSION = "0.6.0";
10
13
  export type VoxelSource = VoxelData | string | URL | ArrayBuffer | Uint8Array;
11
14
  export interface VoxelOptions {
12
15
  kind?: "voxel";
@@ -39,11 +42,22 @@ export interface VoxelOptions {
39
42
  /** grids with `times` (a time dimension): "clock" (default) shows the frame of the map's clock (map.clock: the last frame at or before its time) and follows it as it plays;
40
43
  * a number (ms) pins that time. Changes without re-uploading the grid */
41
44
  time?: number | "clock";
45
+ /** NetCDF / Zarr sources: the variable to draw (default the non-coordinate numeric variable with the most dimensions) */
46
+ variable?: string;
47
+ /** NetCDF / Zarr sources: read only frames [start, end) of the time dimension */
48
+ frames?: [number, number];
49
+ /** NetCDF / Zarr sources: the height range [h0, h1] in meters (default from the vertical coordinate; pressure levels use the standard atmosphere) */
50
+ heights?: [number, number];
42
51
  }
43
52
  export interface VoxelPluginOptions {
44
53
  /** "auto" (default): WebGPU when the map draws with WebGPU (shared device and depth texture), else WebGL2. "webgpu": WebGPU (own device under a WebGL2 map) or fail. "webgl2": always WebGL2 */
45
54
  backend?: VoxelBackend | "auto";
55
+ /** GPU memory for the voxel textures of this map, in MB (default 512, 128 on touch devices). Only the frame shown is uploaded; a grid bigger than half of it also gets
56
+ * coarser levels, drawn far away when the layers in view would not fit. Infinity turns the budget off */
57
+ budget?: number;
46
58
  }
59
+ /** 既定の GPU の予算(MB)=タッチ端末は小さく(packages/ortho-core/fallback-ladder.md の 160 MB の中) */
60
+ export declare const defaultBudgetMB: () => number;
47
61
  /** handle.pick の 1 件=統一 picking の答え(layer=手綱の id・index=セルの番号・cell=[i, j, k]・value=セルの値) */
48
62
  export interface VoxelHit extends OverlayPick {
49
63
  id: number;
@@ -86,6 +100,10 @@ export interface VoxelHandle {
86
100
  times: number[] | null;
87
101
  /** the frame shown (0 without a time dimension) */
88
102
  frame: number;
103
+ /** number of coarser levels made for the GPU budget (0 = the grid fits) */
104
+ levels: number;
105
+ /** what a NetCDF / Zarr source was read as (format, variable, units, long name, dimensions, attributes); null for VoxelData */
106
+ source: SourceMeta | null;
89
107
  ready: Promise<VoxelHandle>;
90
108
  setData(data: VoxelSource): Promise<VoxelHandle>;
91
109
  setProps(p: Partial<VoxelOptions>): Promise<VoxelHandle>;
@@ -108,7 +126,7 @@ export declare const OVERLAY_LINGER_MS = 250;
108
126
  /** 格子を上げ直さずに変えられる opts(setProps はこれだけなら "style" を送る) */
109
127
  export declare const STYLE_KEYS: readonly (keyof VoxelOptions)[];
110
128
  /** map.addData(data, { kind: "voxel", … }) の実体 */
111
- export declare function createVoxel(map: VoxelMap, data: VoxelSource, opts?: VoxelOptions, { backend }?: VoxelPluginOptions): VoxelHandle;
129
+ export declare function createVoxel(map: VoxelMap, data: VoxelSource, opts?: VoxelOptions, { backend, budget }?: VoxelPluginOptions): VoxelHandle;
112
130
  /** map.use(voxelPlugin()) → map.addData(data, { kind: "voxel", render?, colormap?, range?, threshold?, alphaRamp?, step?, opacity?, altitude?, depthTest?, above?, properties? }) */
113
- export declare function voxelPlugin({ backend }?: VoxelPluginOptions): VoxelPlugin;
131
+ export declare function voxelPlugin({ backend, budget }?: VoxelPluginOptions): VoxelPlugin;
114
132
  export default voxelPlugin;
package/dist/index.js CHANGED
@@ -1,8 +1,18 @@
1
- import { prepareGrid, transferables, checkVoxelData, checkClip, buildColormap, thrOf, frameAt } from "./grid.js";
2
- export { prepare, checkVoxelData, checkClip, thrOf, buildColormap, colorStops, parseColor, quantize, toRgba8, valueRange, placeBox, placeRegion, unitAt, hprMat, enuBasis, ecefToLngLat, ecefEnuRows, inv3, mul3, rayBox, marchBlocks, COLORMAPS, COLORMAP_NAMES, SHELL_N, prepareGrid, maxOf, needsBricks, brickSpans, extract, parentIndex, frameValues, frameAt, BRICK, BRICK_LIMIT } from "./grid.js";
1
+ import { transferables, levelBuffers, checkVoxelData, checkClip, buildColormap, thrOf, frameAt } from "./grid.js";
2
+ import { readAndPrepare } from "./voxel-reader.js";
3
+ export { prepare, checkVoxelData, checkClip, thrOf, buildColormap, colorStops, parseColor, quantize, toRgba8, valueRange, placeBox, placeRegion, unitAt, hprMat, enuBasis, ecefToLngLat, ecefEnuRows, inv3, mul3, rayBox, marchBlocks, COLORMAPS, COLORMAP_NAMES, SHELL_N, prepareGrid, maxOf, needsBricks, brickSpans, extract, parentIndex, frameValues, frameAt, BRICK, BRICK_LIMIT, LOD_MAX, levelDims, levelCount, downsample, pyramid, makeLevel, levelBuffers } from "./grid.js";
4
+ export { readSource, parseNetCDF, sniffNetCDF, openZarr, zarrArray, readZarr, toVoxelData, pickVariable, axisOf, cfTimes, heightsOf, pressureToHeight, edges, unpack, isZarrUrl } from "./sources.js";
3
5
  // 描き手(worker)の URL=入口(voxel-draw.js が兄弟を import)。字面は .js のまま=npm の dist では実体・repo の中では Vite が .ts に読み替える
4
6
  const VOXEL_URL = new URL("./voxel-draw.js", import.meta.url).href;
5
- export const VERSION = "0.4.0";
7
+ export const VERSION = "0.6.0";
8
+ /** 既定の GPU の予算(MB)=タッチ端末は小さく(packages/ortho-core/fallback-ladder.md の 160 MB の中) */
9
+ export const defaultBudgetMB = () => { try {
10
+ return typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches ? 128 : 512;
11
+ }
12
+ catch {
13
+ return 512;
14
+ } };
15
+ const MB = 1 << 20;
6
16
  /** 楕円体表示の b/a(region の殻を本体と同じ形で作る) */
7
17
  const R_AX_ELL = 1 - 1 / 298.257223563;
8
18
  const rAxOf = (map) => { try {
@@ -14,7 +24,7 @@ catch {
14
24
  const shared = new WeakMap();
15
25
  /** 手綱が全部外れてからオーバーレイを片付けるまでの猶予(ms)=場面の切り替え(remove → すぐ addData)で同じ名のオーバーレイを壊して作り直さない(render worker の片付けと新しい登録が競う) */
16
26
  export const OVERLAY_LINGER_MS = 250;
17
- function acquire(map, above, id, backend) {
27
+ function acquire(map, above, id, backend, budget) {
18
28
  let m = shared.get(map);
19
29
  if (!m)
20
30
  shared.set(map, (m = { ids: new Set() }));
@@ -40,6 +50,7 @@ function acquire(map, above, id, backend) {
40
50
  slot.errors.get(e.q)?.(e.error || "error");
41
51
  }
42
52
  };
53
+ ov.post({ type: "budget", bytes: Number.isFinite(budget) ? budget * MB : 0 }); // 0=予算なし
43
54
  m[k] = slot;
44
55
  }
45
56
  m[k].refs++;
@@ -99,15 +110,14 @@ const readerFree = () => { if (--liveHandles > 0 || !reader)
99
110
  return; readerIdle = setTimeout(() => { readerIdle = null; if (liveHandles || !reader)
100
111
  return; reader.w.terminate(); reader = null; }, 5000); };
101
112
  const isFileSource = (src) => typeof src === "string" || src instanceof URL || src instanceof ArrayBuffer || src instanceof Uint8Array;
102
- async function load(src, o, rAx) {
103
- if (isFileSource(src))
104
- throw new Error("voxel: file sources (.nc / .zarr / .vox / .npy) are not read yet (stage 5); pass VoxelData");
105
- const d = checkVoxelData(src);
106
- const popts = { colormap: o.colormap, range: o.range ?? null, alphaRamp: o.alphaRamp ?? null, threshold: o.threshold ?? null, rAx };
113
+ async function load(src, o, rAx, budget) {
114
+ const file = isFileSource(src), d = file ? null : checkVoxelData(src);
115
+ const source = file ? { variable: o.variable, frames: o.frames ?? null, heights: o.heights ?? null } : undefined, fsrc = file ? (src instanceof URL ? src.href : src) : undefined;
116
+ const popts = { colormap: o.colormap, range: o.range ?? null, alphaRamp: o.alphaRamp ?? null, threshold: o.threshold ?? null, rAx, lodBytes: Number.isFinite(budget) ? budget * MB / 2 : null }; // 予算の半分を越える格子は粗い段も
107
117
  const r = spawnReader();
108
118
  if (r) {
109
119
  try {
110
- const seq = ++r.seq, req = { seq, data: d, opts: popts };
120
+ const seq = ++r.seq, req = file ? { seq, src: fsrc, source, opts: popts } : { seq, data: d, opts: popts };
111
121
  const p = await new Promise((res, rej) => { r.pending.set(seq, { res, rej }); r.w.postMessage(req); }); // data は写し=渡した人の配列は手放さない
112
122
  return { ...p, reader: "worker" };
113
123
  }
@@ -116,7 +126,7 @@ async function load(src, o, rAx) {
116
126
  throw e;
117
127
  }
118
128
  }
119
- return { ...prepareGrid(d, popts), reader: "main" };
129
+ return { ...(await readAndPrepare(file ? { src: fsrc, source, opts: popts } : { data: d, opts: popts })), reader: "main" };
120
130
  }
121
131
  const isVoxelHit = (id) => (t) => t.layer === id;
122
132
  const checkEnum = (v, allowed, name) => { if (!allowed.includes(v))
@@ -138,15 +148,21 @@ function checkOpts(opts) {
138
148
  checkClip(opts.clip);
139
149
  if (opts.time != null && opts.time !== "clock" && !(typeof opts.time === "number" && Number.isFinite(opts.time)))
140
150
  throw new TypeError("voxel: time must be \"clock\" or a time in ms");
151
+ if (opts.variable != null && typeof opts.variable !== "string")
152
+ throw new TypeError("voxel: variable must be a string");
153
+ if (opts.frames != null && !(Array.isArray(opts.frames) && opts.frames.length === 2 && opts.frames.every(Number.isInteger) && opts.frames[0] >= 0 && opts.frames[1] > opts.frames[0]))
154
+ throw new RangeError("voxel: frames must be [start, end) frame numbers");
155
+ if (opts.heights != null && !(Array.isArray(opts.heights) && opts.heights.length === 2 && opts.heights.every(Number.isFinite) && opts.heights[0] < opts.heights[1]))
156
+ throw new RangeError("voxel: heights must be [h0, h1] meters with h0 < h1");
141
157
  }
142
158
  let anonSeq = 0;
143
159
  /** map.addData(data, { kind: "voxel", … }) の実体 */
144
- export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
160
+ export function createVoxel(map, data, opts = {}, { backend = "auto", budget = defaultBudgetMB() } = {}) {
145
161
  checkOpts(opts);
146
162
  if (!isFileSource(data))
147
163
  checkVoxelData(data); // 形の間違いは今・同期に投げる(worker へ送ってからでは遅い)
148
164
  const id = opts.id ?? `voxel${++anonSeq}`;
149
- const { ov, slot, release } = acquire(map, !!opts.above, id, backend);
165
+ const { ov, slot, release } = acquire(map, !!opts.above, id, backend, budget);
150
166
  try {
151
167
  ov.addLayerId?.(id);
152
168
  }
@@ -172,7 +188,7 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
172
188
  map.clock.off("tick", onClock).off("change", onClock); };
173
189
  const run = async (src, o) => {
174
190
  const my = ++seq;
175
- const p = await load(src, o, rAxOf(map));
191
+ const p = await load(src, o, rAxOf(map), budget);
176
192
  if (removed || my !== seq)
177
193
  return h;
178
194
  h.dims = p.dims;
@@ -183,11 +199,13 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
183
199
  h.error = null;
184
200
  h.shape = p.shape;
185
201
  h.bricks = p.bricks?.length ?? 0;
202
+ h.levels = (p.lods ?? p.bricks?.[0]?.lods)?.length ?? 0;
186
203
  times = h.times = p.times;
187
204
  h.frame = frameNow();
188
205
  follow(!!times && (o.time ?? "clock") === "clock");
189
206
  cur = src;
190
- values = src.values;
207
+ values = p.values ?? src.values;
208
+ h.source = p.meta ?? null;
191
209
  comps = p.comps;
192
210
  baked = true;
193
211
  const style = { mode: MODE[o.render ?? "volume"], thr: p.thr, step: o.step ?? 0.5, opacity: o.opacity ?? 1, clip: checkClip(o.clip), depthTest: o.depthTest !== false, properties: o.properties ?? null, range: p.range, on: visible, frame: h.frame, alt: o.altitude ?? (p.placed ? "absolute" : "relative") };
@@ -201,9 +219,9 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
201
219
  for (const q of qs)
202
220
  slot.errors.set(q, e => { h.error = e; }); // 描き手の build の失敗(3D テクスチャの上限など)は非同期に届く=warn と手綱の error(ready は解決する)
203
221
  if (!p.bricks)
204
- ov.post({ type: "layer", q: id, shape: p.shape, dims: p.dims, comps: p.comps, tex: p.tex, frames: p.frames, cmap: p.cmap, anchor: p.anchor, A: p.A, radius: p.radius, geo: p.geo, vmax: p.vmax, ...style }, transferables(p));
222
+ ov.post({ type: "layer", q: id, shape: p.shape, dims: p.dims, comps: p.comps, tex: p.tex, frames: p.frames, lods: p.lods, cmap: p.cmap, anchor: p.anchor, A: p.A, radius: p.radius, geo: p.geo, vmax: p.vmax, ...style }, transferables(p));
205
223
  else
206
- p.bricks.forEach((b, i) => ov.post({ type: "layer", q: qs[i], layer: id, shape: p.shape, dims: b.dims, comps: p.comps, tex: b.tex, frames: b.frames, cmap: i ? p.cmap.slice() : p.cmap, anchor: b.anchor, A: b.A, radius: b.radius, geo: b.geo, vmax: b.vmax, off: b.off, own: b.own, full: p.dims, ...style }, [b.tex.buffer, b.A.buffer, ...(b.geo ? [b.geo.shell.buffer] : []), ...(b.frames ?? []).map(f => f.buffer)].filter((x, k, a) => a.indexOf(x) === k)));
224
+ p.bricks.forEach((b, i) => ov.post({ type: "layer", q: qs[i], layer: id, shape: p.shape, dims: b.dims, comps: p.comps, tex: b.tex, frames: b.frames, lods: b.lods, cmap: i ? p.cmap.slice() : p.cmap, anchor: b.anchor, A: b.A, radius: b.radius, geo: b.geo, vmax: b.vmax, off: b.off, own: b.own, full: p.dims, ...style }, [...levelBuffers(b), ...(b.lods ?? []).flatMap(levelBuffers)].filter((x, k, a) => a.indexOf(x) === k)));
207
225
  return h;
208
226
  };
209
227
  // 見た目だけの変更=格子を上げ直さない(render・opacity・step・threshold・clip・depthTest・properties・colormap・alphaRamp)
@@ -243,7 +261,7 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
243
261
  t.properties = { ...t.properties, value: v };
244
262
  } return t; };
245
263
  const h = {
246
- id, kind: "voxel", overlay: ov, dims: null, n: 0, anchor: null, range: null, reader: null, error: null, shape: null, bricks: 0, times: null, frame: 0,
264
+ id, kind: "voxel", overlay: ov, dims: null, n: 0, anchor: null, range: null, reader: null, error: null, shape: null, bricks: 0, levels: 0, source: null, times: null, frame: 0,
247
265
  get backend() { return slot.backend; },
248
266
  ready: undefined,
249
267
  setData: (d) => { if (!isFileSource(d))
@@ -271,14 +289,16 @@ export function createVoxel(map, data, opts = {}, { backend = "auto" } = {}) {
271
289
  return h;
272
290
  }
273
291
  /** map.use(voxelPlugin()) → map.addData(data, { kind: "voxel", render?, colormap?, range?, threshold?, alphaRamp?, step?, opacity?, altitude?, depthTest?, above?, properties? }) */
274
- export function voxelPlugin({ backend = "auto" } = {}) {
292
+ export function voxelPlugin({ backend = "auto", budget = defaultBudgetMB() } = {}) {
275
293
  checkEnum(backend, ["auto", "webgpu", "webgl2"], "backend");
294
+ if (!(typeof budget === "number" && budget > 0))
295
+ throw new RangeError("voxel: budget must be a positive number of MB (Infinity = no budget)");
276
296
  return {
277
297
  name: "voxel",
278
298
  version: VERSION,
279
299
  provides: ["data.voxel"],
280
300
  install(map) {
281
- map.registry.dataKinds.add("voxel", { create: (m, data, o) => createVoxel(m, data, o, { backend }) });
301
+ map.registry.dataKinds.add("voxel", { create: (m, data, o) => createVoxel(m, data, o, { backend, budget }) });
282
302
  return () => { try {
283
303
  map.registry.dataKinds.remove("voxel");
284
304
  }
@@ -0,0 +1,91 @@
1
+ import type { VoxelData } from "@ortho-earth/globe";
2
+ export interface SourceOptions {
3
+ /** 描く変数の名前(省略=座標でない、次元の一番多い数値の変数) */
4
+ variable?: string;
5
+ /** 時間の次元のこまの範囲 [始め, 終わり)(番号・省略=全部) */
6
+ frames?: [number, number] | null;
7
+ /** 高さの範囲 [h0, h1](m)を上書き(高さの座標が無い・気圧面の近似を避けたい時) */
8
+ heights?: [number, number] | null;
9
+ /** fetch の代わり(検定・認証つきの取り方) */
10
+ fetch?: (url: string) => Promise<Response>;
11
+ }
12
+ /** 読んだ変数の説明(手綱の source に載る) */
13
+ export interface SourceMeta {
14
+ format: "netcdf" | "zarr";
15
+ variable: string;
16
+ units: string | null;
17
+ longName: string | null;
18
+ dims: string[];
19
+ attrs: Record<string, unknown>;
20
+ }
21
+ export type SourceData = VoxelData & {
22
+ shape: "region";
23
+ region: [number, number, number, number, number, number];
24
+ values: Float32Array;
25
+ meta: SourceMeta;
26
+ };
27
+ type Attrs = Record<string, unknown>;
28
+ interface VarInfo {
29
+ name: string;
30
+ dims: string[];
31
+ shape: number[];
32
+ attrs: Attrs;
33
+ numeric: boolean;
34
+ }
35
+ interface Reader {
36
+ format: "netcdf" | "zarr";
37
+ vars: VarInfo[];
38
+ /** 変数の値(全体か、先頭の軸を [t0, t1) に絞った物)→ Float64 ではなく Float32(scale/offset・fill 済み) */
39
+ read(name: string, first?: [number, number] | null): Promise<Float32Array>;
40
+ /** 座標の 1 次元の値(そのまま) */
41
+ coord(name: string): Promise<Float64Array>;
42
+ }
43
+ type Axis = "x" | "y" | "z" | "t";
44
+ /** 座標の変数が何の軸か(CF:axis・units・standard_name・名前) */
45
+ export declare function axisOf(name: string, attrs: Attrs): Axis | null;
46
+ /** CF の時刻("hours since 1900-01-01 00:00:0.0" など)→ ms。読めなければ null */
47
+ export declare function cfTimes(values: ArrayLike<number>, units: string | null): number[] | null;
48
+ /** 気圧(Pa)→ 高さ(m・標準大気の対流圏の式) */
49
+ export declare const pressureToHeight: (pa: number) => number;
50
+ /** 高さの座標 → m(units と positive で) */
51
+ export declare function heightsOf(values: ArrayLike<number>, attrs: Attrs, name?: string): number[];
52
+ /** セルの中心の並び → 範囲(端のセルの半分だけ外へ)と昇順か。一つだけなら step で幅 */
53
+ export declare function edges(c: ArrayLike<number>, step1?: number): {
54
+ lo: number;
55
+ hi: number;
56
+ asc: boolean;
57
+ };
58
+ /** 選んだ変数(座標でない・数値・次元 2〜4・x と y がある)を探す */
59
+ export declare function pickVariable(r: Pick<Reader, "vars">, want?: string): VarInfo;
60
+ /** 読み手 → VoxelData(region・times) */
61
+ export declare function toVoxelData(r: Reader, o?: SourceOptions): Promise<SourceData>;
62
+ /** 値の整え(scale_factor/add_offset・fill・valid range → NaN) */
63
+ export declare function unpack(src: ArrayLike<number> & {
64
+ length: number;
65
+ }, attrs: Attrs, fill?: number | null): Float32Array;
66
+ /** NetCDF の形式("classic"=読める・"hdf5"=NetCDF-4・null=NetCDF でない) */
67
+ export declare function sniffNetCDF(b: Uint8Array): "classic" | "hdf5" | null;
68
+ export declare function parseNetCDF(buf: ArrayBuffer | Uint8Array): Reader;
69
+ interface ZArray {
70
+ shape: number[];
71
+ chunks: number[];
72
+ dtype: string;
73
+ fill: number | null;
74
+ codec: "none" | "gzip" | "zlib";
75
+ sep: string;
76
+ v3: boolean;
77
+ key: (c: number[]) => string;
78
+ attrs: Attrs;
79
+ dims: string[];
80
+ }
81
+ /** Zarr のメタ(v2 の .zarray+.zattrs、v3 の zarr.json)→ 読み方 */
82
+ export declare function zarrArray(meta: Attrs, attrs?: Attrs, v3?: boolean): ZArray;
83
+ /** Zarr の配列を読む(先頭の軸を [a, b) に絞れる・無いチャンクは fill) */
84
+ export declare function readZarr(get: (path: string) => Promise<Uint8Array | null>, path: string, z: ZArray, first?: [number, number] | null): Promise<Float64Array>;
85
+ /** Zarr の店(URL の根)→ 読み手。consolidated(.zmetadata/zarr.json の consolidated_metadata)があれば一覧、無ければ variable と座標を名前で取る */
86
+ export declare function openZarr(root: string, o?: SourceOptions): Promise<Reader>;
87
+ /** URL の種類(.zarr=Zarr の店・それ以外の .nc 等=一つのファイル) */
88
+ export declare const isZarrUrl: (u: string) => boolean;
89
+ /** VoxelSource(URL/bytes)→ VoxelData(region・times・meta) */
90
+ export declare function readSource(src: string | URL | ArrayBuffer | Uint8Array, o?: SourceOptions): Promise<SourceData>;
91
+ export {};