splatFiles.ts

src/worker/splatFiles.ts · raw · all examples · site home

Exports: WorkerChoiceSplatSourceworkerTargetLoadSplatsOptionsloadSplatsloadSplatFilewriteSplats

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Loading and writing splat files, on a worker or this thread. `worker`:
//   left out  the shared pool where a page can make workers, else this
//             thread (Node, code already in a worker)
//   true      the shared pool (an error where workers are unavailable)
//   false     this thread
//   a pool    one call on it
//   a lease   that worker; the caller keeps the lease
// Both paths run the same core functions, so they give the same results.
import { ArrayData } from "../core/ArrayData";
import type { CoordinateSystem } from "../core/axes";
import { SplatFiles, type SplatFormat, type SplatRequestInit, type SplatWriteOptions } from "../core/files";
import type { Progress } from "../core/progress";
import { ShPalette, type ShTarget } from "../core/sh";
import type { LoadedSplats, SplatData, SplatLayout, SplatTarget } from "../core/splats";
import { WorkerLease, WorkerPool } from "./WorkerPool";

export type WorkerChoice = boolean | WorkerPool | WorkerLease;

// A URL, a Blob / File or the file's bytes
export type SplatSource = string | URL | Blob | Uint8Array<ArrayBuffer> | ArrayBuffer;

// Where a call with this `worker` choice runs: a pool or lease, or this
// thread (undefined)
export function workerTarget(worker: WorkerChoice | undefined): WorkerPool | WorkerLease | undefined {
  if (worker === false || (worker === undefined && !WorkerPool.available)) {
    return undefined;
  }
  if (worker === true || worker === undefined) {
    if (!WorkerPool.available) {
      throw new Error("Workers are unavailable here");
    }
    return WorkerPool.default;
  }
  return worker;
}

export type LoadSplatsOptions<L extends SplatLayout = "extSplats"> = SplatTarget<L> & {
  // For telling formats apart by extension (default: the URL or file name)
  name?: string;
  worker?: WorkerChoice;
  onProgress?: Progress;
  sh?: ShTarget;
  // SPZ: the axes to convert to (default: as stored, meta.coordinateSystem)
  coordinateSystem?: CoordinateSystem;
  // fetch options for a URL source: headers, credentials, mode, cache...
  request?: SplatRequestInit;
  // Aborts the download and decode: the promise rejects with signal.reason
  signal?: AbortSignal;
};

// A splat file as one table: splats in bundles[layout] (default
// extSplats), plus lodTree when the file has one, and its spherical
// harmonics in an SH bundle (`sh`: "ext" (default) or "packed"; false skips
// them; RDX keeps its stored codec unless `sh` is given). RAD's clustered SH
// is expanded to per-splat SH (loadSplatFile keeps its palette). Bytes
// given to a worker are transferred: the caller's buffer is detached.
export async function loadSplats<L extends SplatLayout = "extSplats">(source: SplatSource, options: LoadSplatsOptions<L> = {}): Promise<SplatData<L>> {
  return (await load(source, options, false)).splats;
}

// What a splat file holds: { splats, shPalette? }, the splats as
// loadSplats gives them except RAD's clustered SH, kept as bundle shLabel in
// the splats and shPalette, an ShPalette in the `sh` codec
export async function loadSplatFile<L extends SplatLayout = "extSplats">(source: SplatSource, options: LoadSplatsOptions<L> = {}): Promise<LoadedSplats<L>> {
  return load(source, options, true);
}

async function load<L extends SplatLayout>(source: SplatSource, {
  name, worker, onProgress, layout = "extSplats" as L, encoding, sh, coordinateSystem, request, signal,
}: LoadSplatsOptions<L>, palette: boolean): Promise<LoadedSplats<L>> {
  signal?.throwIfAborted();
  const target = workerTarget(worker);
  let url: string | undefined;
  let bytes: Uint8Array<ArrayBuffer> | undefined;
  if (typeof source === "string" || source instanceof URL) {
    url = typeof location !== "undefined" ? new URL(source, location.href).href : String(source);
  } else if (source instanceof Blob) {
    bytes = new Uint8Array(await source.arrayBuffer());
    name ??= (source as File).name;
  } else {
    bytes = source instanceof ArrayBuffer ? new Uint8Array(source) : source;
  }

  if (!target) {
    // Decoders report progress often: an abort stops them at the next report
    const progress: Progress | undefined = signal ? (fraction, stage) => {
      signal.throwIfAborted();
      onProgress?.(fraction, stage);
    } : onProgress;
    const into = { layout, encoding, sh, coordinateSystem, progress };
    const loaded = bytes ? await SplatFiles.decode(bytes, { ...into, name }) : await SplatFiles.fetch(url!, { ...into, init: { ...request, signal } });
    signal?.throwIfAborted();
    return palette || !loaded.shPalette ? loaded : { splats: loaded.shPalette.expanded(loaded.splats as unknown as ArrayData) as unknown as SplatData<L> };
  }
  // A Headers object cannot be cloned to a worker
  const sent = request?.headers ? { ...request, headers: Object.fromEntries(new Headers(request.headers)) } : request;
  const result = await target.call("loadSplats", { url, bytes, name, layout, encoding, sh, coordinateSystem, palette, request: sent }, { onProgress, signal });
  return {
    splats: ArrayData.fromTransferable(result.splats) as unknown as SplatData<L>,
    shPalette: result.shPalette && new ShPalette(ArrayData.fromTransferable(result.shPalette)),
  };
}

// On a worker, data's arrays (and shPalette's) travel there and back: they
// must not be used until the promise settles, and are whole again after
// (resolved or not). No signal: an abort would leave them on the worker.
export async function writeSplats(data: ArrayData, format: SplatFormat, { worker, onProgress, shPalette, ...options }: SplatWriteOptions & {
  worker?: WorkerChoice;
  onProgress?: Progress;
} = {}): Promise<Uint8Array<ArrayBuffer>> {
  const target = workerTarget(worker);
  if (!target) {
    return SplatFiles.encode(data, format, { ...options, shPalette, progress: onProgress });
  }
  const result = await target.call("writeSplats", { data: data.toTransferable(), shPalette: shPalette?.data.toTransferable(), format, options }, { onProgress });
  data.setFromTransferable(result.data);
  if (shPalette && result.shPalette) {
    shPalette.data.setFromTransferable(result.shPalette);
  }
  if (result.error !== undefined) {
    throw new Error(result.error);
  }
  return result.bytes!;
}