UNPKG

@playcanvas/splat-transform

Version:

Library and CLI tool for 3D Gaussian splat format conversion and transformation

1,151 lines (1,141 loc) 4.21 MB
import { Vec3, Quat, Mat3, BindGroupFormat, BindUniformBufferFormat, BindStorageBufferFormat, SHADERSTAGE_COMPUTE, Shader, UniformBufferFormat, SHADERLANGUAGE_WGSL, UniformFormat, UNIFORMTYPE_UINT, UNIFORMTYPE_FLOAT, StorageBuffer, BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC, Compute, Mat4, BoundingBox, FloatPacking, ComputeRadixSort } from 'playcanvas'; const toBase64 = (bytes) => { // Node.js environment if (typeof Buffer !== 'undefined') { return Buffer.from(bytes).toString('base64'); } // Browser environment - chunk to avoid call stack limits let binary = ''; const chunkSize = 0x8000; // 32KB chunks for (let i = 0; i < bytes.length; i += chunkSize) { const chunk = bytes.subarray(i, i + chunkSize); binary += String.fromCharCode(...chunk); } return btoa(binary); }; /** * Format a duration in milliseconds as a human-readable string. * * - Sub-minute durations render in seconds (e.g. `1.234s`). * - Sub-hour durations render as `MmS.SSSs`. * - Otherwise as `HhMmS.SSSs`. * * @param ms - The duration in milliseconds. * @returns The formatted string. */ const fmtTime = (ms) => { if (!Number.isFinite(ms) || ms < 0) return `${ms}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(3)}s`; const h = Math.floor(ms / 3_600_000); const m = Math.floor((ms % 3_600_000) / 60_000); const s = ((ms % 60_000) / 1000).toFixed(3); return h > 0 ? `${h}h${m}m${s}s` : `${m}m${s}s`; }; /** * Format a byte count using binary (1024-based) units. * * @param n - The number of bytes. * @returns The formatted string (e.g. `1.5MB`). */ const fmtBytes = (n) => { if (!Number.isFinite(n) || n < 0) return `${n}B`; if (n < 1024) return `${n}B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`; if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}MB`; return `${(n / (1024 * 1024 * 1024)).toFixed(2)}GB`; }; /** * Format a distance in metres as a human-readable string, picking the most * appropriate unit (mm/cm/m/km). * * @param m - The distance in metres. * @returns The formatted string. */ const fmtDistance = (m) => { if (!Number.isFinite(m)) return `${m}m`; const abs = Math.abs(m); if (abs === 0) return '0m'; if (abs < 0.01) return `${+(m * 1000).toPrecision(3)}mm`; if (abs < 1) return `${+(m * 100).toPrecision(3)}cm`; if (abs < 1000) return `${+m.toPrecision(3)}m`; return `${+(m / 1000).toPrecision(3)}km`; }; /** * Format a count using SI suffixes (K/M/B/T) above 1000. * * @param n - The count to format. * @returns The formatted string. */ const fmtCount = (n) => { if (!Number.isFinite(n)) return `${n}`; const abs = Math.abs(n); if (abs < 1000) return `${n}`; if (abs < 1e6) return `${+(n / 1e3).toPrecision(3)}K`; if (abs < 1e9) return `${+(n / 1e6).toPrecision(3)}M`; if (abs < 1e12) return `${+(n / 1e9).toPrecision(3)}B`; return `${+(n / 1e12).toPrecision(3)}T`; }; const now = () => { if (typeof performance !== 'undefined' && typeof performance.now === 'function') { return performance.now(); } return Date.now(); }; const fmtArgs = (args) => { return args.map((a) => { if (a instanceof Error) return a.stack ?? a.message; if (typeof a === 'string') return a; if (typeof a === 'number' || typeof a === 'boolean' || a == null) return String(a); try { return JSON.stringify(a); } catch { return String(a); } }).join(' '); }; /** * Default no-op renderer. Used when no other renderer is installed (e.g. in * library/embedded contexts where the host wants to consume `LogEvent`s * directly via {@link Logger.setRenderer} but hasn't done so yet). Drops * every event silently. */ class NullRenderer { handle(_event) { } } const verbosityRank = { quiet: 0, normal: 1, verbose: 2 }; const messageMinVerbosity = { error: 'quiet', warn: 'quiet', info: 'normal', debug: 'verbose' }; /** * Active-scope manager and message router. The single shared instance lives * inside this module; the public `logger` surface is a thin façade over it. */ class LoggerCore { /** Stack of currently-open scopes (innermost last). */ stack = []; renderer = new NullRenderer(); verbosity = 'normal'; setRenderer(r) { this.renderer = r; } setVerbosity(v) { this.verbosity = v; } getVerbosity() { return this.verbosity; } /** * Whether a message at `level` would be emitted at the current * verbosity. Primary use: the `logger` façade calls this before * formatting arguments so filtered `info`/`warn`/`debug` calls don't * allocate the joined string that {@link emit} would only throw away. * * @param level - The message level to test. * @returns `true` if a message at `level` would reach the renderer. */ isLevelVisible(level) { return verbosityRank[this.verbosity] >= verbosityRank[messageMinVerbosity[level]]; } /** * Hand the event to the renderer. Lifecycle events (`scopeStart`, * `scopeEnd`, `barStart`, `barTick`, `barEnd`) and `output` are always * forwarded; presentation policy (e.g. hiding successful `scopeEnd` * footers at non-`verbose` verbosity) lives in the renderer so * embedders consuming the event stream see a complete, faithful * record of scope and bar lifecycles. `message` is assumed already * gated at the façade via {@link LoggerCore.isLevelVisible} (so * callers can skip formatting args for filtered levels); anything * that reaches here is passed through. * * @param event - The event to deliver. */ emit(event) { this.renderer.handle(event); } /** * Pop the scope at the top of the stack (no-op if empty) and emit the * matching `*End` event. * * @param failed - When true, mark the closed scope as having failed. */ popScope(failed = false) { if (this.stack.length === 0) return; const top = this.stack[this.stack.length - 1]; this.stack.pop(); const durationMs = now() - top.start; if (top.kind === 'bar') { this.emit({ kind: 'barEnd', depth: top.depth, name: top.name, durationMs, current: top.current, total: top.total, failed }); return; } const numbering = top.index !== undefined && top.total !== undefined ? { index: top.index, total: top.total } : {}; this.emit({ kind: 'scopeEnd', depth: top.depth, name: top.name, durationMs, failed, ...numbering }); } /** * Open a named, timed group at the current depth. Pass * `{ index, total }` to render the group as part of a numbered series * (e.g. `[2/5] name`); both must be present together. * * @param name - The group name. * @param options - Optional configuration. * @param options.index - 1-based position in the numbered series. * @param options.total - Total length of the numbered series. * @returns A handle for closing the group and writing nested log entries. */ pushGroup(name, options = {}) { const { index, total } = options; if ((index === undefined) !== (total === undefined)) { throw new Error('logger.group: { index, total } must be passed together'); } const depth = this.stack.length; const numbering = index !== undefined && total !== undefined ? { index, total } : undefined; const scope = numbering ? { kind: 'group', name, depth, start: now(), index: numbering.index, total: numbering.total } : { kind: 'group', name, depth, start: now() }; this.stack.push(scope); this.emit({ kind: 'scopeStart', depth, name, ...(numbering ?? {}) }); return this.makeGroup(scope); } /** * Open a labelled progress bar at the current stack depth (i.e. nested * directly under whatever scope is currently on top of the stack). This * is a pure-push operation: it does not pop or auto-close anything. * Callers control nesting purely by the order in which they open and * close scopes. * * @param name - The bar's label, displayed alongside the progress indicator. * @param total - Total number of ticks the bar will report before completing. * A `total` of 0 is allowed (e.g. processing an empty payload); both * `LoggerCore` and `TextRenderer` already handle non-positive totals. * @returns A handle for advancing and closing the bar. */ pushBar(name, total) { const scope = { kind: 'bar', name, depth: this.stack.length, start: now(), total: Math.max(0, total), current: 0 }; this.stack.push(scope); this.emit({ kind: 'barStart', depth: scope.depth, name: scope.name, total: scope.total }); return this.makeBar(scope); } makeBar(scope) { let closed = false; // Bars are strictly LIFO from a renderer's perspective: a `TextRenderer` // (or any other line-based renderer) only tracks one active bar line, // so ticking a bar that isn't currently on top of the stack would // corrupt whatever inner bar is. We still update `scope.current` // internally so the recap line at `barEnd` is accurate, but we // suppress `barTick` emission unless this bar is actually on top. const isTopOfStack = () => this.stack[this.stack.length - 1] === scope; const handle = { tick: (n = 1) => { if (closed) return; if (this.stack.indexOf(scope) === -1) { // scope was popped from underneath us (e.g. a sibling // bar opened inside a function call). Silently retire // the handle so further ticks are no-ops. closed = true; return; } const next = Math.min(scope.total, scope.current + Math.max(0, n)); if (next === scope.current) return; scope.current = next; if (!isTopOfStack()) return; this.emit({ kind: 'barTick', depth: scope.depth, name: scope.name, current: scope.current, total: scope.total }); }, update: (current) => { if (closed) return; if (this.stack.indexOf(scope) === -1) { closed = true; return; } const next = Math.min(scope.total, Math.max(0, current)); if (next === scope.current) return; scope.current = next; if (!isTopOfStack()) return; this.emit({ kind: 'barTick', depth: scope.depth, name: scope.name, current: scope.current, total: scope.total }); }, end: () => { if (closed) return; closed = true; const idx = this.stack.indexOf(scope); if (idx === -1) return; while (this.stack.length > idx + 1) this.popScope(true); this.popScope(); }, [Symbol.dispose]: () => handle.end() }; return handle; } makeGroup(scope) { let closed = false; const handle = { end: () => { if (closed) return; closed = true; const idx = this.stack.indexOf(scope); if (idx === -1) return; while (this.stack.length > idx + 1) this.popScope(true); this.popScope(); }, [Symbol.dispose]: () => handle.end() }; return handle; } message(level, text) { this.emit({ kind: 'message', depth: this.stack.length, level, text }); if (level === 'error') this.unwindAll(true); } /** * Pop every open scope, emitting end-events with optional `failed` flag. * Called automatically on `logger.error(...)` so that aborted work renders * a clean trail of `(failed)` markers without callers needing try/finally. * * @param failed - When true, mark every closed scope as having failed. */ unwindAll(failed = false) { while (this.stack.length > 0) this.popScope(failed); } output(text) { this.emit({ kind: 'output', text }); } } const core = new LoggerCore(); /** * Public logger surface. * * Open named, timed scopes with {@link Logger.group}. Pass `{ index, total }` * to render the group as part of a numbered series. Indeterminate progress is * reported with {@link Logger.bar}. Free-form messages route through `info` / * `warn` / `error` / `debug`, indented under whatever is on top of the * active-scope stack. * * Both `group` and `bar` are pure-push operations: opening a new scope simply * places it on top of the stack without auto-closing siblings, so call order * directly determines nesting. Close scopes with `handle.end()` after the * body. Callers that route failures through {@link Logger.error} get scope * cleanup for free; embedders that swallow exceptions should call * {@link Logger.unwindAll} from their catch to close every still-open scope. */ const logger = { /** * Open a named, timed scope. Returns a {@link Group} handle. Call `end()` * to close it. Group children indent automatically based on call depth. * * Pass `{ index, total }` to render the group as part of a numbered * series (e.g. `[2/5] name`). Both fields must be supplied together. * * @param name - The group name. * @param options - Optional configuration. * @param options.index - 1-based position in the numbered series. * @param options.total - Total length of the numbered series. * @returns A handle for closing the group and writing nested log entries. */ group(name, options) { return core.pushGroup(name, options); }, /** * Open a labelled progress bar nested directly under whatever scope is * currently on top of the active-scope stack. Renders as a single line * at child indent. * * Like {@link Logger.group}, this is a pure-push operation: it does not * close any sibling already on the stack. Close with `bar.end()`, or let * an enclosing group's `end()` / {@link Logger.unwindAll} pop it. * * @param name - The bar's label. * @param total - Expected number of ticks (or absolute total when using * {@link Bar.update}). * @returns A handle for advancing and closing the bar. */ bar(name, total) { return core.pushBar(name, total); }, /** * Emit an info message indented under the innermost active scope. * @param args - Message parts (joined with a space). */ info(...args) { if (!core.isLevelVisible('info')) return; core.message('info', fmtArgs(args)); }, /** * Emit a warning indented under the innermost active scope. * @param args - Message parts. */ warn(...args) { if (!core.isLevelVisible('warn')) return; core.message('warn', fmtArgs(args)); }, /** * Emit an error message. Always shown, regardless of verbosity. Triggers * an automatic unwind of all open scopes, marking each as failed. * @param args - Message parts. */ error(...args) { core.message('error', fmtArgs(args)); }, /** * Emit a debug message. Shown only at `verbose` verbosity. * @param args - Message parts. */ debug(...args) { if (!core.isLevelVisible('debug')) return; core.message('debug', fmtArgs(args)); }, /** * Emit a logical unit of pipeable output (typically one line, or a * multi-line block treated as a single unit). The renderer terminates * each unit with a newline, so callers should not include a trailing * `\n`. Always shown, regardless of verbosity. * @param text - The text to emit (without a trailing newline). */ output(text) { core.output(text); }, /** * Replace the active renderer. Embedders install their own renderer here * to consume `LogEvent`s; the default renderer is a no-op. Renderers * receive every scope/bar lifecycle event regardless of verbosity, so * progress UIs can rely on `scopeStart`/`scopeEnd` and `barStart`/`barEnd` * to manage their state. * @param r - The renderer to install. */ setRenderer(r) { core.setRenderer(r); }, /** * Set verbosity: `quiet` (errors and warnings), `normal` (default), * `verbose` (includes debug). * @param v - The verbosity level. */ setVerbosity(v) { core.setVerbosity(v); }, /** * Close every open scope and bar, optionally marking them as failed. * Use this from an embedder's catch when an exception is being swallowed * (rather than rethrown into a `logger.error()` call), to prevent * dangling scopes from corrupting subsequent output. * @param failed - When true, mark every closed scope as having failed. */ unwindAll(failed = false) { core.unwindAll(failed); }, /** * Get the current verbosity level. * @returns The active verbosity level. */ getVerbosity() { return core.getVerbosity(); } }; const sigmoid$1 = (v) => 1 / (1 + Math.exp(-v)); const _tv = new Vec3(); const _sv = new Vec3(); /** * A source-to-engine coordinate transform comprising translation, rotation * and uniform scale. Lives alongside a DataTable to describe how raw * column data maps to PlayCanvas engine coordinates. * * @example * ```ts * const t = new Transform().fromEulers(0, 0, 180); * console.log(t.isIdentity()); // false * * const inv = t.clone().invert(); * console.log(t.mul(inv).isIdentity()); // true * ``` */ class Transform { translation; rotation; scale; constructor(translation, rotation, scale) { this.translation = translation ? translation.clone() : new Vec3(); this.rotation = rotation ? rotation.clone() : new Quat(); this.scale = scale ?? 1; } /** * Sets this transform to a rotation-only transform from Euler angles in degrees. * * @param x - Rotation around X axis in degrees. * @param y - Rotation around Y axis in degrees. * @param z - Rotation around Z axis in degrees. * @returns This transform (for chaining). */ fromEulers(x, y, z) { this.translation.set(0, 0, 0); this.rotation.setFromEulerAngles(x, y, z); this.scale = 1; return this; } /** * Creates a deep copy of this transform. * * @returns A new Transform with the same values. */ clone() { return new Transform(this.translation, this.rotation, this.scale); } /** * Tests whether this transform equals another within the given tolerance. * Quaternion comparison accounts for double-cover (q and -q represent * the same rotation). * * @param other - The transform to compare against. * @param epsilon - Floating-point tolerance. Defaults to 1e-6. * @returns True if the transforms are equal within the tolerance. */ equals(other, epsilon = 1e-6) { const ta = this.translation; const tb = other.translation; if (Math.abs(ta.x - tb.x) > epsilon || Math.abs(ta.y - tb.y) > epsilon || Math.abs(ta.z - tb.z) > epsilon) { return false; } const ra = this.rotation; const rb = other.rotation; const dot = ra.x * rb.x + ra.y * rb.y + ra.z * rb.z + ra.w * rb.w; if (Math.abs(dot) < 1 - epsilon) { return false; } if (Math.abs(this.scale - other.scale) > epsilon) { return false; } return true; } /** * Tests whether this transform is effectively identity within the given tolerance. * * @param epsilon - Floating-point tolerance. Defaults to 1e-6. * @returns True if identity within the tolerance. */ isIdentity(epsilon = 1e-6) { return this.equals(Transform.IDENTITY, epsilon); } /** * Inverts this transform in-place. * * @returns This transform (for chaining). */ invert() { if (this.scale === 0) { throw new Error('Cannot invert a Transform with scale 0'); } this.scale = 1 / this.scale; this.rotation.invert(); this.translation.mulScalar(-this.scale); this.rotation.transformVector(this.translation, this.translation); return this; } /** * Sets this transform to the composition of a * b. Handles aliasing * (either a or b may be this). * * @param a - The first (left) transform. * @param b - The second (right) transform. * @returns This transform (for chaining). */ mul2(a, b) { // Translation must be computed first using original a.rotation a.rotation.transformVector(b.translation, _tv); _tv.mulScalar(a.scale).add(a.translation); this.rotation.mul2(a.rotation, b.rotation); this.scale = a.scale * b.scale; this.translation.copy(_tv); return this; } /** * Sets this transform to this * other. * * @param other - The transform to multiply with. * @returns This transform (for chaining). */ mul(other) { return this.mul2(this, other); } /** * Transforms a point by this TRS transform: result = translation + rotation * (scale * point). * * @param point - The input point. * @param result - The Vec3 to write the result into (may alias point). * @returns The transformed point. */ transformPoint(point, result) { result.copy(point).mulScalar(this.scale); this.rotation.transformVector(result, result); result.add(this.translation); return result; } /** * Fills the provided Mat4 with the TRS matrix for this transform. * * @param result - The Mat4 to fill. * @returns The filled Mat4. */ getMatrix(result) { _sv.set(this.scale, this.scale, this.scale); return result.setTRS(this.translation, this.rotation, _sv); } static freeze(t) { Object.freeze(t.translation); Object.freeze(t.rotation); return Object.freeze(t); } static IDENTITY = Transform.freeze(new Transform()); /** * PLY coordinate convention: 180-degree rotation around Z. * Used by formats that store Gaussian data in PLY-style coordinates: * PLY, splat, KSplat, SPZ, and SOG. */ static PLY = Transform.freeze(new Transform().fromEulers(0, 0, 180)); } /** * Partition indices around the k-th smallest element using quickselect * (median-of-three pivot selection). * * After this call, `idx[k]` holds the index of the k-th smallest value * in `data`, and all indices before k map to smaller-or-equal values. * * @param data - The data array to use for comparison values. * @param idx - The index array to partition (mutated in place). * @param k - The target partition index. * @returns The index value at position k after partitioning. */ const quickselect = (data, idx, k) => { const valAt = (p) => data[idx[p]]; const swap = (i, j) => { const t = idx[i]; idx[i] = idx[j]; idx[j] = t; }; const n = idx.length; let l = 0; let r = n - 1; while (true) { if (r <= l + 1) { if (r === l + 1 && valAt(r) < valAt(l)) swap(l, r); return idx[k]; } const mid = (l + r) >>> 1; swap(mid, l + 1); if (valAt(l) > valAt(r)) swap(l, r); if (valAt(l + 1) > valAt(r)) swap(l + 1, r); if (valAt(l) > valAt(l + 1)) swap(l, l + 1); let i = l + 1; let j = r; const pivotIdxVal = valAt(l + 1); const pivotIdx = idx[l + 1]; while (true) { do { i++; } while (i <= r && valAt(i) < pivotIdxVal); do { j--; } while (j >= l && valAt(j) > pivotIdxVal); if (j < i) break; swap(i, j); } idx[l + 1] = idx[j]; idx[j] = pivotIdx; if (j >= k) r = j - 1; if (j <= k) l = i; } }; /* eslint-disable indent */ const kSqrt03_02 = Math.sqrt(3.0 / 2.0); const kSqrt01_03 = Math.sqrt(1.0 / 3.0); const kSqrt02_03 = Math.sqrt(2.0 / 3.0); const kSqrt04_03 = Math.sqrt(4.0 / 3.0); const kSqrt01_04 = Math.sqrt(1.0 / 4.0); const kSqrt03_04 = Math.sqrt(3.0 / 4.0); const kSqrt01_05 = Math.sqrt(1.0 / 5.0); const kSqrt03_05 = Math.sqrt(3.0 / 5.0); const kSqrt06_05 = Math.sqrt(6.0 / 5.0); const kSqrt08_05 = Math.sqrt(8.0 / 5.0); const kSqrt09_05 = Math.sqrt(9.0 / 5.0); const kSqrt01_06 = Math.sqrt(1.0 / 6.0); const kSqrt05_06 = Math.sqrt(5.0 / 6.0); const kSqrt03_08 = Math.sqrt(3.0 / 8.0); const kSqrt05_08 = Math.sqrt(5.0 / 8.0); const kSqrt09_08 = Math.sqrt(9.0 / 8.0); const kSqrt05_09 = Math.sqrt(5.0 / 9.0); const kSqrt08_09 = Math.sqrt(8.0 / 9.0); const kSqrt01_10 = Math.sqrt(1.0 / 10.0); const kSqrt03_10 = Math.sqrt(3.0 / 10.0); const kSqrt01_12 = Math.sqrt(1.0 / 12.0); const kSqrt04_15 = Math.sqrt(4.0 / 15.0); const kSqrt01_16 = Math.sqrt(1.0 / 16.0); const kSqrt15_16 = Math.sqrt(15.0 / 16.0); const kSqrt01_18 = Math.sqrt(1.0 / 18.0); const kSqrt01_60 = Math.sqrt(1.0 / 60.0); const dp = (n, start, a, b) => { let sum = 0; for (let i = 0; i < n; i++) { sum += a[start + i] * b[i]; } return sum; }; const coeffsIn = new Float32Array(15); // Build a sparse representation of the SH rotation matrices. For axis-aligned // rotations the matrices are highly sparse, so iterating only over non-zero // entries is significantly faster than the full dot-product approach. const buildSparse = (sh1, sh2, sh3) => { const counts = []; const indices = []; const values = []; const addBand = (matrix, size, base) => { for (let i = 0; i < size; i++) { let count = 0; for (let j = 0; j < size; j++) { if (Math.abs(matrix[i][j]) > 1e-10) { indices.push(base + j); values.push(matrix[i][j]); count++; } } counts.push(count); } }; addBand(sh1, 3, 0); addBand(sh2, 5, 3); addBand(sh3, 7, 8); return { counts, indices, values }; }; // Returns true if the rotation matrix is a signed permutation (every entry is 0 or ±1), // i.e. the rotation maps each axis to ±another axis (multiples of 90°). const isAxisAligned = (rot) => { for (let i = 0; i < 9; i++) { const a = Math.abs(rot[i]); if (a > 0.01 && Math.abs(a - 1) > 0.01) return false; } return true; }; // Rotate spherical harmonics up to band 3 based on https://github.com/andrewwillmott/sh-lib // // This implementation calculates the rotation factors during construction which can then // be used to rotate multiple spherical harmonics cheaply. class RotateSH { apply; constructor(mat) { const rot = mat.data; // band 1 const sh1 = [ [rot[4], -rot[7], rot[1]], [-rot[5], rot[8], -rot[2]], [rot[3], -rot[6], rot[0]] ]; // band 2 const sh2 = [[ kSqrt01_04 * ((sh1[2][2] * sh1[0][0] + sh1[2][0] * sh1[0][2]) + (sh1[0][2] * sh1[2][0] + sh1[0][0] * sh1[2][2])), (sh1[2][1] * sh1[0][0] + sh1[0][1] * sh1[2][0]), kSqrt03_04 * (sh1[2][1] * sh1[0][1] + sh1[0][1] * sh1[2][1]), (sh1[2][1] * sh1[0][2] + sh1[0][1] * sh1[2][2]), kSqrt01_04 * ((sh1[2][2] * sh1[0][2] - sh1[2][0] * sh1[0][0]) + (sh1[0][2] * sh1[2][2] - sh1[0][0] * sh1[2][0])) ], [ kSqrt01_04 * ((sh1[1][2] * sh1[0][0] + sh1[1][0] * sh1[0][2]) + (sh1[0][2] * sh1[1][0] + sh1[0][0] * sh1[1][2])), sh1[1][1] * sh1[0][0] + sh1[0][1] * sh1[1][0], kSqrt03_04 * (sh1[1][1] * sh1[0][1] + sh1[0][1] * sh1[1][1]), sh1[1][1] * sh1[0][2] + sh1[0][1] * sh1[1][2], kSqrt01_04 * ((sh1[1][2] * sh1[0][2] - sh1[1][0] * sh1[0][0]) + (sh1[0][2] * sh1[1][2] - sh1[0][0] * sh1[1][0])) ], [ kSqrt01_03 * (sh1[1][2] * sh1[1][0] + sh1[1][0] * sh1[1][2]) - kSqrt01_12 * ((sh1[2][2] * sh1[2][0] + sh1[2][0] * sh1[2][2]) + (sh1[0][2] * sh1[0][0] + sh1[0][0] * sh1[0][2])), kSqrt04_03 * sh1[1][1] * sh1[1][0] - kSqrt01_03 * (sh1[2][1] * sh1[2][0] + sh1[0][1] * sh1[0][0]), sh1[1][1] * sh1[1][1] - kSqrt01_04 * (sh1[2][1] * sh1[2][1] + sh1[0][1] * sh1[0][1]), kSqrt04_03 * sh1[1][1] * sh1[1][2] - kSqrt01_03 * (sh1[2][1] * sh1[2][2] + sh1[0][1] * sh1[0][2]), kSqrt01_03 * (sh1[1][2] * sh1[1][2] - sh1[1][0] * sh1[1][0]) - kSqrt01_12 * ((sh1[2][2] * sh1[2][2] - sh1[2][0] * sh1[2][0]) + (sh1[0][2] * sh1[0][2] - sh1[0][0] * sh1[0][0])) ], [ kSqrt01_04 * ((sh1[1][2] * sh1[2][0] + sh1[1][0] * sh1[2][2]) + (sh1[2][2] * sh1[1][0] + sh1[2][0] * sh1[1][2])), sh1[1][1] * sh1[2][0] + sh1[2][1] * sh1[1][0], kSqrt03_04 * (sh1[1][1] * sh1[2][1] + sh1[2][1] * sh1[1][1]), sh1[1][1] * sh1[2][2] + sh1[2][1] * sh1[1][2], kSqrt01_04 * ((sh1[1][2] * sh1[2][2] - sh1[1][0] * sh1[2][0]) + (sh1[2][2] * sh1[1][2] - sh1[2][0] * sh1[1][0])) ], [ kSqrt01_04 * ((sh1[2][2] * sh1[2][0] + sh1[2][0] * sh1[2][2]) - (sh1[0][2] * sh1[0][0] + sh1[0][0] * sh1[0][2])), (sh1[2][1] * sh1[2][0] - sh1[0][1] * sh1[0][0]), kSqrt03_04 * (sh1[2][1] * sh1[2][1] - sh1[0][1] * sh1[0][1]), (sh1[2][1] * sh1[2][2] - sh1[0][1] * sh1[0][2]), kSqrt01_04 * ((sh1[2][2] * sh1[2][2] - sh1[2][0] * sh1[2][0]) - (sh1[0][2] * sh1[0][2] - sh1[0][0] * sh1[0][0])) ]]; // band 3 const sh3 = [[ kSqrt01_04 * ((sh1[2][2] * sh2[0][0] + sh1[2][0] * sh2[0][4]) + (sh1[0][2] * sh2[4][0] + sh1[0][0] * sh2[4][4])), kSqrt03_02 * (sh1[2][1] * sh2[0][0] + sh1[0][1] * sh2[4][0]), kSqrt15_16 * (sh1[2][1] * sh2[0][1] + sh1[0][1] * sh2[4][1]), kSqrt05_06 * (sh1[2][1] * sh2[0][2] + sh1[0][1] * sh2[4][2]), kSqrt15_16 * (sh1[2][1] * sh2[0][3] + sh1[0][1] * sh2[4][3]), kSqrt03_02 * (sh1[2][1] * sh2[0][4] + sh1[0][1] * sh2[4][4]), kSqrt01_04 * ((sh1[2][2] * sh2[0][4] - sh1[2][0] * sh2[0][0]) + (sh1[0][2] * sh2[4][4] - sh1[0][0] * sh2[4][0])) ], [ kSqrt01_06 * (sh1[1][2] * sh2[0][0] + sh1[1][0] * sh2[0][4]) + kSqrt01_06 * ((sh1[2][2] * sh2[1][0] + sh1[2][0] * sh2[1][4]) + (sh1[0][2] * sh2[3][0] + sh1[0][0] * sh2[3][4])), sh1[1][1] * sh2[0][0] + (sh1[2][1] * sh2[1][0] + sh1[0][1] * sh2[3][0]), kSqrt05_08 * sh1[1][1] * sh2[0][1] + kSqrt05_08 * (sh1[2][1] * sh2[1][1] + sh1[0][1] * sh2[3][1]), kSqrt05_09 * sh1[1][1] * sh2[0][2] + kSqrt05_09 * (sh1[2][1] * sh2[1][2] + sh1[0][1] * sh2[3][2]), kSqrt05_08 * sh1[1][1] * sh2[0][3] + kSqrt05_08 * (sh1[2][1] * sh2[1][3] + sh1[0][1] * sh2[3][3]), sh1[1][1] * sh2[0][4] + (sh1[2][1] * sh2[1][4] + sh1[0][1] * sh2[3][4]), kSqrt01_06 * (sh1[1][2] * sh2[0][4] - sh1[1][0] * sh2[0][0]) + kSqrt01_06 * ((sh1[2][2] * sh2[1][4] - sh1[2][0] * sh2[1][0]) + (sh1[0][2] * sh2[3][4] - sh1[0][0] * sh2[3][0])) ], [ kSqrt04_15 * (sh1[1][2] * sh2[1][0] + sh1[1][0] * sh2[1][4]) + kSqrt01_05 * (sh1[0][2] * sh2[2][0] + sh1[0][0] * sh2[2][4]) - kSqrt01_60 * ((sh1[2][2] * sh2[0][0] + sh1[2][0] * sh2[0][4]) - (sh1[0][2] * sh2[4][0] + sh1[0][0] * sh2[4][4])), kSqrt08_05 * sh1[1][1] * sh2[1][0] + kSqrt06_05 * sh1[0][1] * sh2[2][0] - kSqrt01_10 * (sh1[2][1] * sh2[0][0] - sh1[0][1] * sh2[4][0]), sh1[1][1] * sh2[1][1] + kSqrt03_04 * sh1[0][1] * sh2[2][1] - kSqrt01_16 * (sh1[2][1] * sh2[0][1] - sh1[0][1] * sh2[4][1]), kSqrt08_09 * sh1[1][1] * sh2[1][2] + kSqrt02_03 * sh1[0][1] * sh2[2][2] - kSqrt01_18 * (sh1[2][1] * sh2[0][2] - sh1[0][1] * sh2[4][2]), sh1[1][1] * sh2[1][3] + kSqrt03_04 * sh1[0][1] * sh2[2][3] - kSqrt01_16 * (sh1[2][1] * sh2[0][3] - sh1[0][1] * sh2[4][3]), kSqrt08_05 * sh1[1][1] * sh2[1][4] + kSqrt06_05 * sh1[0][1] * sh2[2][4] - kSqrt01_10 * (sh1[2][1] * sh2[0][4] - sh1[0][1] * sh2[4][4]), kSqrt04_15 * (sh1[1][2] * sh2[1][4] - sh1[1][0] * sh2[1][0]) + kSqrt01_05 * (sh1[0][2] * sh2[2][4] - sh1[0][0] * sh2[2][0]) - kSqrt01_60 * ((sh1[2][2] * sh2[0][4] - sh1[2][0] * sh2[0][0]) - (sh1[0][2] * sh2[4][4] - sh1[0][0] * sh2[4][0])) ], [ kSqrt03_10 * (sh1[1][2] * sh2[2][0] + sh1[1][0] * sh2[2][4]) - kSqrt01_10 * ((sh1[2][2] * sh2[3][0] + sh1[2][0] * sh2[3][4]) + (sh1[0][2] * sh2[1][0] + sh1[0][0] * sh2[1][4])), kSqrt09_05 * sh1[1][1] * sh2[2][0] - kSqrt03_05 * (sh1[2][1] * sh2[3][0] + sh1[0][1] * sh2[1][0]), kSqrt09_08 * sh1[1][1] * sh2[2][1] - kSqrt03_08 * (sh1[2][1] * sh2[3][1] + sh1[0][1] * sh2[1][1]), sh1[1][1] * sh2[2][2] - kSqrt01_03 * (sh1[2][1] * sh2[3][2] + sh1[0][1] * sh2[1][2]), kSqrt09_08 * sh1[1][1] * sh2[2][3] - kSqrt03_08 * (sh1[2][1] * sh2[3][3] + sh1[0][1] * sh2[1][3]), kSqrt09_05 * sh1[1][1] * sh2[2][4] - kSqrt03_05 * (sh1[2][1] * sh2[3][4] + sh1[0][1] * sh2[1][4]), kSqrt03_10 * (sh1[1][2] * sh2[2][4] - sh1[1][0] * sh2[2][0]) - kSqrt01_10 * ((sh1[2][2] * sh2[3][4] - sh1[2][0] * sh2[3][0]) + (sh1[0][2] * sh2[1][4] - sh1[0][0] * sh2[1][0])) ], [ kSqrt04_15 * (sh1[1][2] * sh2[3][0] + sh1[1][0] * sh2[3][4]) + kSqrt01_05 * (sh1[2][2] * sh2[2][0] + sh1[2][0] * sh2[2][4]) - kSqrt01_60 * ((sh1[2][2] * sh2[4][0] + sh1[2][0] * sh2[4][4]) + (sh1[0][2] * sh2[0][0] + sh1[0][0] * sh2[0][4])), kSqrt08_05 * sh1[1][1] * sh2[3][0] + kSqrt06_05 * sh1[2][1] * sh2[2][0] - kSqrt01_10 * (sh1[2][1] * sh2[4][0] + sh1[0][1] * sh2[0][0]), sh1[1][1] * sh2[3][1] + kSqrt03_04 * sh1[2][1] * sh2[2][1] - kSqrt01_16 * (sh1[2][1] * sh2[4][1] + sh1[0][1] * sh2[0][1]), kSqrt08_09 * sh1[1][1] * sh2[3][2] + kSqrt02_03 * sh1[2][1] * sh2[2][2] - kSqrt01_18 * (sh1[2][1] * sh2[4][2] + sh1[0][1] * sh2[0][2]), sh1[1][1] * sh2[3][3] + kSqrt03_04 * sh1[2][1] * sh2[2][3] - kSqrt01_16 * (sh1[2][1] * sh2[4][3] + sh1[0][1] * sh2[0][3]), kSqrt08_05 * sh1[1][1] * sh2[3][4] + kSqrt06_05 * sh1[2][1] * sh2[2][4] - kSqrt01_10 * (sh1[2][1] * sh2[4][4] + sh1[0][1] * sh2[0][4]), kSqrt04_15 * (sh1[1][2] * sh2[3][4] - sh1[1][0] * sh2[3][0]) + kSqrt01_05 * (sh1[2][2] * sh2[2][4] - sh1[2][0] * sh2[2][0]) - kSqrt01_60 * ((sh1[2][2] * sh2[4][4] - sh1[2][0] * sh2[4][0]) + (sh1[0][2] * sh2[0][4] - sh1[0][0] * sh2[0][0])) ], [ kSqrt01_06 * (sh1[1][2] * sh2[4][0] + sh1[1][0] * sh2[4][4]) + kSqrt01_06 * ((sh1[2][2] * sh2[3][0] + sh1[2][0] * sh2[3][4]) - (sh1[0][2] * sh2[1][0] + sh1[0][0] * sh2[1][4])), sh1[1][1] * sh2[4][0] + (sh1[2][1] * sh2[3][0] - sh1[0][1] * sh2[1][0]), kSqrt05_08 * sh1[1][1] * sh2[4][1] + kSqrt05_08 * (sh1[2][1] * sh2[3][1] - sh1[0][1] * sh2[1][1]), kSqrt05_09 * sh1[1][1] * sh2[4][2] + kSqrt05_09 * (sh1[2][1] * sh2[3][2] - sh1[0][1] * sh2[1][2]), kSqrt05_08 * sh1[1][1] * sh2[4][3] + kSqrt05_08 * (sh1[2][1] * sh2[3][3] - sh1[0][1] * sh2[1][3]), sh1[1][1] * sh2[4][4] + (sh1[2][1] * sh2[3][4] - sh1[0][1] * sh2[1][4]), kSqrt01_06 * (sh1[1][2] * sh2[4][4] - sh1[1][0] * sh2[4][0]) + kSqrt01_06 * ((sh1[2][2] * sh2[3][4] - sh1[2][0] * sh2[3][0]) - (sh1[0][2] * sh2[1][4] - sh1[0][0] * sh2[1][0])) ], [ kSqrt01_04 * ((sh1[2][2] * sh2[4][0] + sh1[2][0] * sh2[4][4]) - (sh1[0][2] * sh2[0][0] + sh1[0][0] * sh2[0][4])), kSqrt03_02 * (sh1[2][1] * sh2[4][0] - sh1[0][1] * sh2[0][0]), kSqrt15_16 * (sh1[2][1] * sh2[4][1] - sh1[0][1] * sh2[0][1]), kSqrt05_06 * (sh1[2][1] * sh2[4][2] - sh1[0][1] * sh2[0][2]), kSqrt15_16 * (sh1[2][1] * sh2[4][3] - sh1[0][1] * sh2[0][3]), kSqrt03_02 * (sh1[2][1] * sh2[4][4] - sh1[0][1] * sh2[0][4]), kSqrt01_04 * ((sh1[2][2] * sh2[4][4] - sh1[2][0] * sh2[4][0]) - (sh1[0][2] * sh2[0][4] - sh1[0][0] * sh2[0][0])) ]]; if (isAxisAligned(rot)) { const { counts, indices, values } = buildSparse(sh1, sh2, sh3); this.apply = (result, src) => { if (!src || src === result) { coeffsIn.set(result); src = coeffsIn; } let vp = 0; if (result.length < 3) return; for (let i = 0; i < 3; i++) { let sum = 0; for (let k = 0; k < counts[i]; k++) { sum += values[vp] * src[indices[vp]]; vp++; } result[i] = sum; } if (result.length < 8) return; for (let i = 0; i < 5; i++) { let sum = 0; for (let k = 0; k < counts[3 + i]; k++) { sum += values[vp] * src[indices[vp]]; vp++; } result[3 + i] = sum; } if (result.length < 15) return; for (let i = 0; i < 7; i++) { let sum = 0; for (let k = 0; k < counts[8 + i]; k++) { sum += values[vp] * src[indices[vp]]; vp++; } result[8 + i] = sum; } }; } else { this.apply = (result, src) => { if (!src || src === result) { coeffsIn.set(result); src = coeffsIn; } // band 1 if (result.length < 3) { return; } result[0] = dp(3, 0, src, sh1[0]); result[1] = dp(3, 0, src, sh1[1]); result[2] = dp(3, 0, src, sh1[2]); // band 2 if (result.length < 8) { return; } result[3] = dp(5, 3, src, sh2[0]); result[4] = dp(5, 3, src, sh2[1]); result[5] = dp(5, 3, src, sh2[2]); result[6] = dp(5, 3, src, sh2[3]); result[7] = dp(5, 3, src, sh2[4]); // band 3 if (result.length < 15) { return; } result[8] = dp(7, 8, src, sh3[0]); result[9] = dp(7, 8, src, sh3[1]); result[10] = dp(7, 8, src, sh3[2]); result[11] = dp(7, 8, src, sh3[3]); result[12] = dp(7, 8, src, sh3[4]); result[13] = dp(7, 8, src, sh3[5]); result[14] = dp(7, 8, src, sh3[6]); }; } } } const indent = (depth) => ' '.repeat(Math.max(0, depth)); const BAR_WIDTH = 20; /** * Default human-readable text renderer. Emits one event per line - no * carriage-return rewriting, no TTY detection, no buffering. Bars render * as `[#### ...... ] duration`, with `#` appended incrementally on each * `barTick` and the remainder padded with `.` on `barEnd`. `output` * events are treated as line-oriented: their text is written to the * pipeable sink with a trailing `\n` appended (callers should not include * one themselves). * * Verbosity is consulted directly from the shared {@link logger} on each * event, so this renderer alone decides what to display - the core * delivers every scope/bar lifecycle event so embedders consuming the * event stream see a faithful record. The display rules are: * * - `quiet` - suppresses every scope/bar lifecycle line (start, tick, * end - including failed ends). Errors, warnings and `output` still * show. * - `normal` (default) - shows scope/bar headers and bar progress; * shows failed `scopeEnd` / `barEnd` footers (the "failed in ..." * cascade from `logger.error` / `unwindAll(true)`); hides successful * `scopeEnd` footers. * - `verbose` - shows everything, including successful `scopeEnd` * footers ("done in ..."). * * Sinks are injected (no `process` reference here) so the renderer works in * both Node CLI and browser/bundle contexts: the CLI passes * `process.stderr.write` for status and `process.stdout.write` for raw * output; library/browser consumers can pass a `console.log` line buffer. */ class TextRenderer { write; output; getPeakMemory; getLiveMemory; /** * When true, scope-end and bar-end lines gain a `[peak X]` suffix * (or `[peak X | live Y]` when {@link TextRendererOptions.getLiveMemory} * is also supplied) sourced from * {@link TextRendererOptions.getPeakMemory}. No effect when the probe * is omitted. Defaults to `true` when `getPeakMemory` is provided so * embedders that supply a probe see the overlay automatically. Mutable * so the host can toggle the overlay without re-installing the renderer. */ mem; /** True while a bar header has been written without its closing `\n`. */ lineDirty = false; /** * Hash count already written for the active bar. Bars are strictly LIFO * (the active-scope stack guarantees it), so a single counter suffices. */ barFilled = 0; constructor(options) { this.write = options.write; this.output = options.output ?? options.write; this.getPeakMemory = options.getPeakMemory; this.getLiveMemory = options.getLiveMemory; this.mem = options.getPeakMemory !== undefined; } rank() { return verbosityRank[logger.getVerbosity()]; } handle(event) { switch (event.kind) { case 'scopeStart': { if (this.rank() < verbosityRank.normal) return; this.commitDirty(); const numbered = event.index !== undefined && event.total !== undefined ? `[${event.index}/${event.total}] ` : ''; this.write(`${indent(event.depth)}\u25b8 ${numbered}${event.name}\n`); return; } case 'scopeEnd': { const rank = this.rank(); if (event.failed) { if (rank < verbosityRank.normal) return; } else if (rank < verbosityRank.verbose) { return; } this.commitDirty(); const verb = event.failed ? 'failed in' : 'done in'; this.write(`${indent(event.depth + 1)}${verb} ${fmtTime(event.durationMs)}${this.memSuffix()}\n`); return; } case 'barStart': { if (this.rank() < verbosityRank.normal) return; this.commitDirty(); this.write(`${indent(event.depth)}\u25b8 ${event.name} [`); this.lineDirty = true; this.barFilled = 0; return; } case 'barTick': { if (!this.lineDirty) return; if (this.rank() < verbosityRank.normal) return; const target = event.total <= 0 ? 0 : Math.min(BAR_WIDTH, Math.floor((event.current / event.total) * BAR_WIDTH)); if (target > this.barFilled) { this.write('#'.repeat(target - this.barFilled)); this.barFilled = target; } return; } case 'barEnd': { if (this.rank() < verbosityRank.normal) return; const suffix = event.failed ? `] (failed) ${fmtTime(event.durationMs)}` : `] ${fmtTime(event.durationMs)}`; if (this.lineDirty) { const remaining = Math.max(0, BAR_WIDTH - this.barFilled); this.write(`${'.'.repeat(remaining)}${suffix}${this.memSuffix()}\n`); this.lineDirty = false; this.barFilled = 0; } else { // bar's inline line was committed early by a nested // event (e.g. a child group/message). Emit a recap // line whose fill reflects actual progress, so bars // that ended early or failed don't read as complete. const filled = event.total <= 0 ? 0 : Math.min(BAR_WIDTH, Math.floor((event.current / event.total) * BAR_WIDTH)); const bar = `${'#'.repeat(filled)}${'.'.repeat(BAR_WIDTH - filled)}`; this.write(`${indent(event.depth)}\u25b8 ${event.name} [${bar}${suffix}${this.memSuffix()}\n`); } return; } case 'message': { this.commitDirty(); // info/debug get a `\u00b7` glyph only when nested under a // scope - at depth 0 they're framing lines (banners, // summaries) and read cleaner without decoration. warn/error // always carry their severity glyph regardless of depth. let prefix = ''; if (event.level === 'error') prefix = '\u2717 '; else if (event.level === 'warn') prefix = '! '; else if (event.depth > 0) prefix = '\u00b7 '; this.write(`${indent(event.depth)}${prefix}${event.text}\n`); return; } case 'output': { this.commitDirty(); this.output(`${event.text}\n`); } } } /** * Terminate any in-progress bar line so subsequent output starts on a * fresh line. The bar's `#` count is preserved on its own line; the * eventual `barEnd` will produce its own footer line if it fires later. */ commitDirty() { if (this.lineDirty) { this.write('\n'); this.lineDirty = false; } } memSuffix() { if (!this.mem || !this.getPeakMemory) return ''; const peak = fmtBytes(this.getPeakMemory()); if (!this.getLiveMemory) return ` [peak ${peak}]`; return ` [peak ${peak} | live ${fmtBytes(this.getLiveMemory())}]`; } } var Module = (() => { return ( async function(moduleArg = {}) { var moduleRtn; var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject;});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof WorkerGlobalScope!="undefined";var ENVIRONMENT_IS_NODE=typeof process=="object"&&process.versions?.node&&process.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import('module');var require=createRequire(import.meta.url);}var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");if(_scriptName.startsWith("file:")){scriptDirectory=nodePath.dirname(require("url").fileURLToPath(_scriptName))+"/";}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){process.argv[1].replace(/\\/g,"/");}process.argv.slice(2);}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href;}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)};}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.