@playcanvas/splat-transform
Version:
Library and CLI tool for 3D Gaussian splat format conversion and transformation
1 lines • 5.42 MB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/lib/utils/base64.ts","../src/lib/utils/fmt.ts","../src/lib/utils/logger.ts","../src/lib/utils/math.ts","../src/lib/utils/quickselect.ts","../src/lib/utils/rotate-sh.ts","../src/lib/utils/text-renderer.ts","../lib/webp.mjs","../src/lib/utils/webp-codec.ts","../src/lib/data-table/data-table.ts","../src/lib/data-table/transform.ts","../src/lib/data-table/combine.ts","../src/lib/gpu/gpu-edge-cost.ts","../src/lib/spatial/kd-tree.ts","../src/lib/gpu/gpu-knn.ts","../src/lib/spatial/radix-sort.ts","../src/lib/data-table/decimate.ts","../src/lib/render/config.ts","../src/lib/data-table/gaussian-aabb.ts","../src/lib/data-table/gaussian-eval.ts","../src/lib/data-table/morton-order.ts","../src/lib/data-table/sh-bands.ts","../src/lib/data-table/summary.ts","../src/lib/io/read/file-system.ts","../src/lib/io/read/buffered-read-stream.ts","../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs","../src/lib/io/read/memory-file-system.ts","../src/lib/io/read/url-file-system.ts","../src/lib/io/read/zip-file-system.ts","../src/lib/readers/read-ksplat.ts","../src/lib/readers/read-lcc.ts","../src/lib/readers/read-mjs.ts","../src/lib/readers/decompress-ply.ts","../src/lib/readers/read-ply.ts","../src/lib/readers/read-sog-v1.ts","../src/lib/readers/read-sog.ts","../src/lib/readers/read-splat.ts","../src/lib/spz-module.ts","../src/lib/readers/read-spz.ts","../src/lib/read.ts","../src/lib/writers/utils.ts","../src/lib/writers/compressed-chunk.ts","../src/lib/version.ts","../src/lib/writers/write-compressed-ply.ts","../src/lib/writers/write-csv.ts","../src/lib/writers/write-glb.ts","../node_modules/@playcanvas/supersplat-viewer/dist/index.js","../src/lib/io/write/write-helpers.ts","../src/lib/io/write/memory-file-system.ts","../src/lib/io/write/crc.ts","../src/lib/io/write/zip-file-system.ts","../src/lib/spatial/b-tree.ts","../src/lib/spatial/gaussian-bvh.ts","../src/lib/gpu/gpu-clustering.ts","../src/lib/gpu/shaders/dilation.ts","../src/lib/gpu/gpu-dilation.ts","../src/lib/gpu/shaders/chunks/constants.ts","../src/lib/gpu/shaders/chunks/covariance-3d.ts","../src/lib/gpu/shaders/chunks/jacobian-equirect.ts","../src/lib/gpu/shaders/chunks/jacobian-pinhole.ts","../src/lib/gpu/shaders/chunks/projection-equirect.ts","../src/lib/gpu/shaders/chunks/projection-pinhole.ts","../src/lib/gpu/shaders/chunks/quat-rotation.ts","../src/lib/gpu/shaders/chunks/sh-band-1.ts","../src/lib/gpu/shaders/chunks/sh-band-2.ts","../src/lib/gpu/shaders/chunks/sh-band-3.ts","../src/lib/gpu/shaders/chunks/tile-aabb-equirect.ts","../src/lib/gpu/shaders/chunks/tile-aabb-pinhole.ts","../src/lib/gpu/shaders/chunks/tile-walk-equirect.ts","../src/lib/gpu/shaders/chunks/tile-walk-pinhole.ts","../src/lib/gpu/shaders/finalize.ts","../src/lib/gpu/shaders/find-boundaries.ts","../src/lib/gpu/shaders/init-tile-offsets.ts","../src/lib/gpu/shaders/prefix-sum.ts","../src/lib/gpu/shaders/prepare-indirect.ts","../src/lib/gpu/shaders/project.ts","../src/lib/gpu/shaders/rasterize-binned.ts","../src/lib/gpu/shaders/tile-bin-emit-pairs.ts","../src/lib/gpu/shaders/uniforms.ts","../src/lib/gpu/gpu-splat-rasterizer.ts","../src/lib/gpu/gpu-voxelization.ts","../src/lib/spatial/k-means.ts","../src/lib/spatial/quantize-1d.ts","../src/lib/writers/write-sog.ts","../src/lib/writers/write-html.ts","../src/lib/render/camera.ts","../src/lib/render/preprocess.ts","../src/lib/render/raster-pass.ts","../src/lib/writers/write-image.ts","../src/lib/writers/write-lod.ts","../src/lib/writers/write-ply.ts","../src/lib/writers/write-spz.ts","../src/lib/voxel/morton.ts","../src/lib/voxel/block-mask-buffer.ts","../src/lib/voxel/block-mask-map.ts","../src/lib/voxel/sparse-voxel-grid.ts","../src/lib/mesh/marching-cubes.ts","../src/lib/mesh/coplanar-merge.ts","../src/lib/mesh/voxel-faces.ts","../src/lib/writers/collision-glb.ts","../src/lib/writers/sparse-octree.ts","../src/lib/voxel/block-cleanup.ts","../src/lib/voxel/voxelize.ts","../src/lib/voxel/filter-pipeline.ts","../src/lib/voxel/flood-fill.ts","../src/lib/voxel/voxel-query.ts","../src/lib/voxel/filter-cluster.ts","../src/lib/voxel/filter-floaters.ts","../src/lib/voxel/dilation.ts","../src/lib/voxel/grid-ops.ts","../src/lib/voxel/carve.ts","../src/lib/voxel/fill-exterior.ts","../src/lib/voxel/fill-floor.ts","../src/lib/writers/write-voxel.ts","../src/lib/write.ts","../src/lib/process.ts"],"sourcesContent":["const toBase64 = (bytes: Uint8Array): string => {\n // Node.js environment\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(bytes).toString('base64');\n }\n\n // Browser environment - chunk to avoid call stack limits\n let binary = '';\n const chunkSize = 0x8000; // 32KB chunks\n for (let i = 0; i < bytes.length; i += chunkSize) {\n const chunk = bytes.subarray(i, i + chunkSize);\n binary += String.fromCharCode(...chunk);\n }\n return btoa(binary);\n};\n\nexport { toBase64 };\n","/**\n * Format a duration in milliseconds as a human-readable string.\n *\n * - Sub-minute durations render in seconds (e.g. `1.234s`).\n * - Sub-hour durations render as `MmS.SSSs`.\n * - Otherwise as `HhMmS.SSSs`.\n *\n * @param ms - The duration in milliseconds.\n * @returns The formatted string.\n */\nconst fmtTime = (ms: number): string => {\n if (!Number.isFinite(ms) || ms < 0) return `${ms}ms`;\n if (ms < 60_000) return `${(ms / 1000).toFixed(3)}s`;\n\n const h = Math.floor(ms / 3_600_000);\n const m = Math.floor((ms % 3_600_000) / 60_000);\n const s = ((ms % 60_000) / 1000).toFixed(3);\n\n return h > 0 ? `${h}h${m}m${s}s` : `${m}m${s}s`;\n};\n\n/**\n * Format a byte count using binary (1024-based) units.\n *\n * @param n - The number of bytes.\n * @returns The formatted string (e.g. `1.5MB`).\n */\nconst fmtBytes = (n: number): string => {\n if (!Number.isFinite(n) || n < 0) return `${n}B`;\n if (n < 1024) return `${n}B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;\n if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}MB`;\n return `${(n / (1024 * 1024 * 1024)).toFixed(2)}GB`;\n};\n\n/**\n * Format a distance in metres as a human-readable string, picking the most\n * appropriate unit (mm/cm/m/km).\n *\n * @param m - The distance in metres.\n * @returns The formatted string.\n */\nconst fmtDistance = (m: number): string => {\n if (!Number.isFinite(m)) return `${m}m`;\n const abs = Math.abs(m);\n if (abs === 0) return '0m';\n if (abs < 0.01) return `${+(m * 1000).toPrecision(3)}mm`;\n if (abs < 1) return `${+(m * 100).toPrecision(3)}cm`;\n if (abs < 1000) return `${+m.toPrecision(3)}m`;\n return `${+(m / 1000).toPrecision(3)}km`;\n};\n\n/**\n * Format a count using SI suffixes (K/M/B/T) above 1000.\n *\n * @param n - The count to format.\n * @returns The formatted string.\n */\nconst fmtCount = (n: number): string => {\n if (!Number.isFinite(n)) return `${n}`;\n const abs = Math.abs(n);\n if (abs < 1000) return `${n}`;\n if (abs < 1e6) return `${+(n / 1e3).toPrecision(3)}K`;\n if (abs < 1e9) return `${+(n / 1e6).toPrecision(3)}M`;\n if (abs < 1e12) return `${+(n / 1e9).toPrecision(3)}B`;\n return `${+(n / 1e12).toPrecision(3)}T`;\n};\n\nexport { fmtBytes, fmtCount, fmtDistance, fmtTime };\n","/**\n * Verbosity level controlling which messages reach the renderer.\n *\n * - `quiet` - errors and warnings only.\n * - `normal` - tasks, bars, info, warn, error (default).\n * - `verbose` - normal + debug messages.\n */\ntype Verbosity = 'quiet' | 'normal' | 'verbose';\n\n/** Severity tag for free-form messages (ordered descending by severity). */\ntype MessageKind = 'error' | 'warn' | 'info' | 'debug';\n\n/**\n * Semantic event delivered to a {@link Renderer}. Renderers can filter, format\n * and display these as they wish.\n *\n * `scopeStart` / `scopeEnd` represent the open/close of a {@link Group}.\n * They carry optional `index` / `total` fields when the scope is part of a\n * numbered series, which renderers can use to switch to a `[N/T] name` style.\n *\n * `barStart` / `barTick` / `barEnd` represent a determinate progress bar.\n * The bar's `name` is repeated on every event so the renderer can keep its\n * label stable across in-place updates while tracking progress via `current`\n * and `total`.\n *\n * `output` is the pipeable channel: each event represents a single logical\n * unit of output (typically one line - or a multi-line block treated as a\n * unit) that the renderer is expected to terminate with a newline. Callers\n * should not include a trailing `\\n` themselves.\n */\ntype LogEvent =\n | { kind: 'scopeStart'; depth: number; name: string; index?: number; total?: number }\n | { kind: 'scopeEnd'; depth: number; name: string; durationMs: number; failed?: boolean; index?: number; total?: number }\n | { kind: 'barStart'; depth: number; name: string; total: number }\n | { kind: 'barTick'; depth: number; name: string; current: number; total: number }\n | { kind: 'barEnd'; depth: number; name: string; durationMs: number; current: number; total: number; failed?: boolean }\n | { kind: 'message'; depth: number; level: MessageKind; text: string }\n | { kind: 'output'; text: string };\n\n/**\n * Renderer interface. Receives the full stream of semantic lifecycle\n * events ({@link LogEvent}) and decides how to display them. The core\n * does not filter scope/bar events by verbosity, so renderers see a\n * faithful record of every scope open/close and bar progress update -\n * embedders consuming the event stream can rely on this for progress\n * UIs that must close themselves on completion. Visibility decisions\n * (e.g. hiding successful `scopeEnd` footers at non-`verbose`\n * verbosity) are the renderer's responsibility; {@link logger.getVerbosity}\n * is available to consult.\n *\n * `message` events for `info`, `warn` and `debug` are gated by verbosity\n * at the façade (see {@link LoggerCore.isLevelVisible}) before reaching\n * the renderer; `error` is always delivered.\n */\ninterface Renderer {\n /**\n * Handle a log event.\n * @param event - The event to render.\n */\n handle(event: LogEvent): void;\n}\n\n/**\n * Determinate progress bar handle. Closed explicitly via `end()`, or\n * implicitly when an enclosing {@link Group}'s `end()` (or a\n * {@link Logger.unwindAll}) pops it as part of cleanup.\n *\n * Carries a `[Symbol.dispose]` slot directly (rather than extending the\n * built-in `Disposable` lib type) so the published `.d.ts` stays free of\n * any reference to the `Disposable` interface. `Symbol.dispose` itself is\n * still a TS 5.2+ / `esnext.disposable` (or `es2024.disposable`) lib\n * symbol, so consumers compiling against these declarations need that\n * lib enabled (or `skipLibCheck: true`). Callers on TS 5.2+ / Node 20+\n * can adopt `using bar = logger.bar(...)` because `using` only requires\n * the `[Symbol.dispose]` shape structurally.\n */\ninterface Bar {\n /**\n * Advance the bar by `n` ticks.\n * @param n - Number of ticks to advance (default 1).\n */\n tick(n?: number): void;\n /**\n * Set the bar's absolute progress. Clamped to `[0, total]`. Suppresses\n * a `barTick` event when the value is unchanged.\n * @param current - Absolute progress value.\n */\n update(current: number): void;\n /**\n * Close the bar and emit final timing.\n */\n end(): void;\n /** Dispose hook so `using` syntax closes the bar on scope exit. */\n [Symbol.dispose](): void;\n}\n\n/**\n * Named, timed scope returned from {@link Logger.group}. Manages the scope's\n * lifecycle only - free-form messages, nested groups and bars are emitted via\n * the global `logger` (they auto-indent under whatever is on top of the\n * active-scope stack).\n *\n * Open scopes with `logger.group(name)` and close them with `sub.end()` after\n * the body. Embedders that catch their own exceptions (rather than letting\n * them propagate to a `logger.error()` call) should call\n * {@link Logger.unwindAll} from their catch to close any scopes/bars left\n * dangling on the stack.\n *\n * Carries a `[Symbol.dispose]` slot directly (rather than extending the\n * built-in `Disposable` lib type) so the published `.d.ts` stays free of\n * any reference to the `Disposable` interface. `Symbol.dispose` itself is\n * still a TS 5.2+ / `esnext.disposable` (or `es2024.disposable`) lib\n * symbol, so consumers compiling against these declarations need that\n * lib enabled (or `skipLibCheck: true`). Callers on TS 5.2+ / Node 20+\n * can adopt `using g = logger.group(...)` because `using` only requires\n * the `[Symbol.dispose]` shape structurally.\n */\ninterface Group {\n /**\n * Close the group, popping anything still open above it on the stack\n * (defensively handles forgotten inner scopes) and emit the timing event.\n */\n end(): void;\n /** Dispose hook so `using` syntax closes the group on scope exit. */\n [Symbol.dispose](): void;\n}\n\n/**\n * Internal node tracked on the active-scope stack. Both kinds carry their own\n * depth and start time; `group` may additionally carry `index` / `total` so a\n * numbered series can re-emit the same numbering on close.\n */\ntype Scope =\n | { kind: 'group'; name: string; depth: number; start: number; index?: number; total?: number }\n | { kind: 'bar'; name: string; depth: number; start: number; total: number; current: number };\n\nconst now = (): number => {\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return performance.now();\n }\n return Date.now();\n};\n\nconst fmtArgs = (args: any[]): string => {\n return args.map((a) => {\n if (a instanceof Error) return a.stack ?? a.message;\n if (typeof a === 'string') return a;\n if (typeof a === 'number' || typeof a === 'boolean' || a == null) return String(a);\n try {\n return JSON.stringify(a);\n } catch {\n return String(a);\n }\n }).join(' ');\n};\n\n/**\n * Default no-op renderer. Used when no other renderer is installed (e.g. in\n * library/embedded contexts where the host wants to consume `LogEvent`s\n * directly via {@link Logger.setRenderer} but hasn't done so yet). Drops\n * every event silently.\n */\nclass NullRenderer implements Renderer {\n handle(_event: LogEvent): void { /* no-op */ }\n}\n\nconst verbosityRank: Record<Verbosity, number> = {\n quiet: 0,\n normal: 1,\n verbose: 2\n};\n\nconst messageMinVerbosity: Record<MessageKind, Verbosity> = {\n error: 'quiet',\n warn: 'quiet',\n info: 'normal',\n debug: 'verbose'\n};\n\n/**\n * Active-scope manager and message router. The single shared instance lives\n * inside this module; the public `logger` surface is a thin façade over it.\n */\nclass LoggerCore {\n /** Stack of currently-open scopes (innermost last). */\n readonly stack: Scope[] = [];\n\n private renderer: Renderer = new NullRenderer();\n\n private verbosity: Verbosity = 'normal';\n\n setRenderer(r: Renderer): void {\n this.renderer = r;\n }\n\n setVerbosity(v: Verbosity): void {\n this.verbosity = v;\n }\n\n getVerbosity(): Verbosity {\n return this.verbosity;\n }\n\n /**\n * Whether a message at `level` would be emitted at the current\n * verbosity. Primary use: the `logger` façade calls this before\n * formatting arguments so filtered `info`/`warn`/`debug` calls don't\n * allocate the joined string that {@link emit} would only throw away.\n *\n * @param level - The message level to test.\n * @returns `true` if a message at `level` would reach the renderer.\n */\n isLevelVisible(level: MessageKind): boolean {\n return verbosityRank[this.verbosity] >= verbosityRank[messageMinVerbosity[level]];\n }\n\n /**\n * Hand the event to the renderer. Lifecycle events (`scopeStart`,\n * `scopeEnd`, `barStart`, `barTick`, `barEnd`) and `output` are always\n * forwarded; presentation policy (e.g. hiding successful `scopeEnd`\n * footers at non-`verbose` verbosity) lives in the renderer so\n * embedders consuming the event stream see a complete, faithful\n * record of scope and bar lifecycles. `message` is assumed already\n * gated at the façade via {@link LoggerCore.isLevelVisible} (so\n * callers can skip formatting args for filtered levels); anything\n * that reaches here is passed through.\n *\n * @param event - The event to deliver.\n */\n emit(event: LogEvent): void {\n this.renderer.handle(event);\n }\n\n /**\n * Pop the scope at the top of the stack (no-op if empty) and emit the\n * matching `*End` event.\n *\n * @param failed - When true, mark the closed scope as having failed.\n */\n private popScope(failed = false): void {\n if (this.stack.length === 0) return;\n const top = this.stack[this.stack.length - 1];\n this.stack.pop();\n const durationMs = now() - top.start;\n if (top.kind === 'bar') {\n this.emit({\n kind: 'barEnd',\n depth: top.depth,\n name: top.name,\n durationMs,\n current: top.current,\n total: top.total,\n failed\n });\n return;\n }\n const numbering = top.index !== undefined && top.total !== undefined ?\n { index: top.index, total: top.total } :\n {};\n this.emit({ kind: 'scopeEnd', depth: top.depth, name: top.name, durationMs, failed, ...numbering });\n }\n\n /**\n * Open a named, timed group at the current depth. Pass\n * `{ index, total }` to render the group as part of a numbered series\n * (e.g. `[2/5] name`); both must be present together.\n *\n * @param name - The group name.\n * @param options - Optional configuration.\n * @param options.index - 1-based position in the numbered series.\n * @param options.total - Total length of the numbered series.\n * @returns A handle for closing the group and writing nested log entries.\n */\n pushGroup(name: string, options: { index?: number; total?: number } = {}): Group {\n const { index, total } = options;\n if ((index === undefined) !== (total === undefined)) {\n throw new Error('logger.group: { index, total } must be passed together');\n }\n const depth = this.stack.length;\n const numbering = index !== undefined && total !== undefined ? { index, total } : undefined;\n const scope: Scope = numbering ?\n { kind: 'group', name, depth, start: now(), index: numbering.index, total: numbering.total } :\n { kind: 'group', name, depth, start: now() };\n this.stack.push(scope);\n this.emit({ kind: 'scopeStart', depth, name, ...(numbering ?? {}) });\n return this.makeGroup(scope);\n }\n\n /**\n * Open a labelled progress bar at the current stack depth (i.e. nested\n * directly under whatever scope is currently on top of the stack). This\n * is a pure-push operation: it does not pop or auto-close anything.\n * Callers control nesting purely by the order in which they open and\n * close scopes.\n *\n * @param name - The bar's label, displayed alongside the progress indicator.\n * @param total - Total number of ticks the bar will report before completing.\n * A `total` of 0 is allowed (e.g. processing an empty payload); both\n * `LoggerCore` and `TextRenderer` already handle non-positive totals.\n * @returns A handle for advancing and closing the bar.\n */\n pushBar(name: string, total: number): Bar {\n const scope: Scope = {\n kind: 'bar',\n name,\n depth: this.stack.length,\n start: now(),\n total: Math.max(0, total),\n current: 0\n };\n this.stack.push(scope);\n this.emit({ kind: 'barStart', depth: scope.depth, name: scope.name, total: scope.total });\n return this.makeBar(scope);\n }\n\n private makeBar(scope: Scope & { kind: 'bar' }): Bar {\n let closed = false;\n // Bars are strictly LIFO from a renderer's perspective: a `TextRenderer`\n // (or any other line-based renderer) only tracks one active bar line,\n // so ticking a bar that isn't currently on top of the stack would\n // corrupt whatever inner bar is. We still update `scope.current`\n // internally so the recap line at `barEnd` is accurate, but we\n // suppress `barTick` emission unless this bar is actually on top.\n const isTopOfStack = () => this.stack[this.stack.length - 1] === scope;\n const handle: Bar = {\n tick: (n = 1) => {\n if (closed) return;\n if (this.stack.indexOf(scope) === -1) {\n // scope was popped from underneath us (e.g. a sibling\n // bar opened inside a function call). Silently retire\n // the handle so further ticks are no-ops.\n closed = true;\n return;\n }\n const next = Math.min(scope.total, scope.current + Math.max(0, n));\n if (next === scope.current) return;\n scope.current = next;\n if (!isTopOfStack()) return;\n this.emit({ kind: 'barTick', depth: scope.depth, name: scope.name, current: scope.current, total: scope.total });\n },\n update: (current: number) => {\n if (closed) return;\n if (this.stack.indexOf(scope) === -1) {\n closed = true;\n return;\n }\n const next = Math.min(scope.total, Math.max(0, current));\n if (next === scope.current) return;\n scope.current = next;\n if (!isTopOfStack()) return;\n this.emit({ kind: 'barTick', depth: scope.depth, name: scope.name, current: scope.current, total: scope.total });\n },\n end: () => {\n if (closed) return;\n closed = true;\n const idx = this.stack.indexOf(scope);\n if (idx === -1) return;\n while (this.stack.length > idx + 1) this.popScope(true);\n this.popScope();\n },\n [Symbol.dispose]: () => handle.end()\n };\n return handle;\n }\n\n private makeGroup(scope: Scope & { kind: 'group' }): Group {\n let closed = false;\n const handle: Group = {\n end: () => {\n if (closed) return;\n closed = true;\n const idx = this.stack.indexOf(scope);\n if (idx === -1) return;\n while (this.stack.length > idx + 1) this.popScope(true);\n this.popScope();\n },\n [Symbol.dispose]: () => handle.end()\n };\n return handle;\n }\n\n message(level: MessageKind, text: string): void {\n this.emit({ kind: 'message', depth: this.stack.length, level, text });\n if (level === 'error') this.unwindAll(true);\n }\n\n /**\n * Pop every open scope, emitting end-events with optional `failed` flag.\n * Called automatically on `logger.error(...)` so that aborted work renders\n * a clean trail of `(failed)` markers without callers needing try/finally.\n *\n * @param failed - When true, mark every closed scope as having failed.\n */\n unwindAll(failed = false): void {\n while (this.stack.length > 0) this.popScope(failed);\n }\n\n output(text: string): void {\n this.emit({ kind: 'output', text });\n }\n}\n\nconst core = new LoggerCore();\n\n/**\n * Public logger surface.\n *\n * Open named, timed scopes with {@link Logger.group}. Pass `{ index, total }`\n * to render the group as part of a numbered series. Indeterminate progress is\n * reported with {@link Logger.bar}. Free-form messages route through `info` /\n * `warn` / `error` / `debug`, indented under whatever is on top of the\n * active-scope stack.\n *\n * Both `group` and `bar` are pure-push operations: opening a new scope simply\n * places it on top of the stack without auto-closing siblings, so call order\n * directly determines nesting. Close scopes with `handle.end()` after the\n * body. Callers that route failures through {@link Logger.error} get scope\n * cleanup for free; embedders that swallow exceptions should call\n * {@link Logger.unwindAll} from their catch to close every still-open scope.\n */\nconst logger = {\n /**\n * Open a named, timed scope. Returns a {@link Group} handle. Call `end()`\n * to close it. Group children indent automatically based on call depth.\n *\n * Pass `{ index, total }` to render the group as part of a numbered\n * series (e.g. `[2/5] name`). Both fields must be supplied together.\n *\n * @param name - The group name.\n * @param options - Optional configuration.\n * @param options.index - 1-based position in the numbered series.\n * @param options.total - Total length of the numbered series.\n * @returns A handle for closing the group and writing nested log entries.\n */\n group(name: string, options?: { index?: number; total?: number }): Group {\n return core.pushGroup(name, options);\n },\n\n /**\n * Open a labelled progress bar nested directly under whatever scope is\n * currently on top of the active-scope stack. Renders as a single line\n * at child indent.\n *\n * Like {@link Logger.group}, this is a pure-push operation: it does not\n * close any sibling already on the stack. Close with `bar.end()`, or let\n * an enclosing group's `end()` / {@link Logger.unwindAll} pop it.\n *\n * @param name - The bar's label.\n * @param total - Expected number of ticks (or absolute total when using\n * {@link Bar.update}).\n * @returns A handle for advancing and closing the bar.\n */\n bar(name: string, total: number): Bar {\n return core.pushBar(name, total);\n },\n\n /**\n * Emit an info message indented under the innermost active scope.\n * @param args - Message parts (joined with a space).\n */\n info(...args: any[]): void {\n if (!core.isLevelVisible('info')) return;\n core.message('info', fmtArgs(args));\n },\n\n /**\n * Emit a warning indented under the innermost active scope.\n * @param args - Message parts.\n */\n warn(...args: any[]): void {\n if (!core.isLevelVisible('warn')) return;\n core.message('warn', fmtArgs(args));\n },\n\n /**\n * Emit an error message. Always shown, regardless of verbosity. Triggers\n * an automatic unwind of all open scopes, marking each as failed.\n * @param args - Message parts.\n */\n error(...args: any[]): void {\n core.message('error', fmtArgs(args));\n },\n\n /**\n * Emit a debug message. Shown only at `verbose` verbosity.\n * @param args - Message parts.\n */\n debug(...args: any[]): void {\n if (!core.isLevelVisible('debug')) return;\n core.message('debug', fmtArgs(args));\n },\n\n /**\n * Emit a logical unit of pipeable output (typically one line, or a\n * multi-line block treated as a single unit). The renderer terminates\n * each unit with a newline, so callers should not include a trailing\n * `\\n`. Always shown, regardless of verbosity.\n * @param text - The text to emit (without a trailing newline).\n */\n output(text: string): void {\n core.output(text);\n },\n\n /**\n * Replace the active renderer. Embedders install their own renderer here\n * to consume `LogEvent`s; the default renderer is a no-op. Renderers\n * receive every scope/bar lifecycle event regardless of verbosity, so\n * progress UIs can rely on `scopeStart`/`scopeEnd` and `barStart`/`barEnd`\n * to manage their state.\n * @param r - The renderer to install.\n */\n setRenderer(r: Renderer): void {\n core.setRenderer(r);\n },\n\n /**\n * Set verbosity: `quiet` (errors and warnings), `normal` (default),\n * `verbose` (includes debug).\n * @param v - The verbosity level.\n */\n setVerbosity(v: Verbosity): void {\n core.setVerbosity(v);\n },\n\n /**\n * Close every open scope and bar, optionally marking them as failed.\n * Use this from an embedder's catch when an exception is being swallowed\n * (rather than rethrown into a `logger.error()` call), to prevent\n * dangling scopes from corrupting subsequent output.\n * @param failed - When true, mark every closed scope as having failed.\n */\n unwindAll(failed = false): void {\n core.unwindAll(failed);\n },\n\n /**\n * Get the current verbosity level.\n * @returns The active verbosity level.\n */\n getVerbosity(): Verbosity {\n return core.getVerbosity();\n }\n};\n\n/**\n * Public type alias for the logger object. Embedders can type-hint against\n * this to inject a configured logger.\n */\ntype Logger = typeof logger;\n\nexport { logger, verbosityRank };\nexport type { Bar, Group, LogEvent, Logger, MessageKind, Renderer, Verbosity };\n","import { Mat4, Quat, Vec3 } from 'playcanvas';\n\nconst sigmoid = (v: number) => 1 / (1 + Math.exp(-v));\n\nconst _tv = new Vec3();\nconst _sv = new Vec3();\n\n/**\n * A source-to-engine coordinate transform comprising translation, rotation\n * and uniform scale. Lives alongside a DataTable to describe how raw\n * column data maps to PlayCanvas engine coordinates.\n *\n * @example\n * ```ts\n * const t = new Transform().fromEulers(0, 0, 180);\n * console.log(t.isIdentity()); // false\n *\n * const inv = t.clone().invert();\n * console.log(t.mul(inv).isIdentity()); // true\n * ```\n */\nclass Transform {\n translation: Vec3;\n rotation: Quat;\n scale: number;\n\n constructor(translation?: Vec3, rotation?: Quat, scale?: number) {\n this.translation = translation ? translation.clone() : new Vec3();\n this.rotation = rotation ? rotation.clone() : new Quat();\n this.scale = scale ?? 1;\n }\n\n /**\n * Sets this transform to a rotation-only transform from Euler angles in degrees.\n *\n * @param x - Rotation around X axis in degrees.\n * @param y - Rotation around Y axis in degrees.\n * @param z - Rotation around Z axis in degrees.\n * @returns This transform (for chaining).\n */\n fromEulers(x: number, y: number, z: number): Transform {\n this.translation.set(0, 0, 0);\n this.rotation.setFromEulerAngles(x, y, z);\n this.scale = 1;\n return this;\n }\n\n /**\n * Creates a deep copy of this transform.\n *\n * @returns A new Transform with the same values.\n */\n clone(): Transform {\n return new Transform(this.translation, this.rotation, this.scale);\n }\n\n /**\n * Tests whether this transform equals another within the given tolerance.\n * Quaternion comparison accounts for double-cover (q and -q represent\n * the same rotation).\n *\n * @param other - The transform to compare against.\n * @param epsilon - Floating-point tolerance. Defaults to 1e-6.\n * @returns True if the transforms are equal within the tolerance.\n */\n equals(other: Transform, epsilon = 1e-6): boolean {\n const ta = this.translation;\n const tb = other.translation;\n if (Math.abs(ta.x - tb.x) > epsilon || Math.abs(ta.y - tb.y) > epsilon || Math.abs(ta.z - tb.z) > epsilon) {\n return false;\n }\n const ra = this.rotation;\n const rb = other.rotation;\n const dot = ra.x * rb.x + ra.y * rb.y + ra.z * rb.z + ra.w * rb.w;\n if (Math.abs(dot) < 1 - epsilon) {\n return false;\n }\n if (Math.abs(this.scale - other.scale) > epsilon) {\n return false;\n }\n return true;\n }\n\n /**\n * Tests whether this transform is effectively identity within the given tolerance.\n *\n * @param epsilon - Floating-point tolerance. Defaults to 1e-6.\n * @returns True if identity within the tolerance.\n */\n isIdentity(epsilon = 1e-6): boolean {\n return this.equals(Transform.IDENTITY, epsilon);\n }\n\n /**\n * Inverts this transform in-place.\n *\n * @returns This transform (for chaining).\n */\n invert(): Transform {\n if (this.scale === 0) {\n throw new Error('Cannot invert a Transform with scale 0');\n }\n this.scale = 1 / this.scale;\n this.rotation.invert();\n this.translation.mulScalar(-this.scale);\n this.rotation.transformVector(this.translation, this.translation);\n return this;\n }\n\n /**\n * Sets this transform to the composition of a * b. Handles aliasing\n * (either a or b may be this).\n *\n * @param a - The first (left) transform.\n * @param b - The second (right) transform.\n * @returns This transform (for chaining).\n */\n mul2(a: Transform, b: Transform): Transform {\n // Translation must be computed first using original a.rotation\n a.rotation.transformVector(b.translation, _tv);\n _tv.mulScalar(a.scale).add(a.translation);\n\n this.rotation.mul2(a.rotation, b.rotation);\n this.scale = a.scale * b.scale;\n this.translation.copy(_tv);\n return this;\n }\n\n /**\n * Sets this transform to this * other.\n *\n * @param other - The transform to multiply with.\n * @returns This transform (for chaining).\n */\n mul(other: Transform): Transform {\n return this.mul2(this, other);\n }\n\n /**\n * Transforms a point by this TRS transform: result = translation + rotation * (scale * point).\n *\n * @param point - The input point.\n * @param result - The Vec3 to write the result into (may alias point).\n * @returns The transformed point.\n */\n transformPoint(point: Vec3, result: Vec3): Vec3 {\n result.copy(point).mulScalar(this.scale);\n this.rotation.transformVector(result, result);\n result.add(this.translation);\n return result;\n }\n\n /**\n * Fills the provided Mat4 with the TRS matrix for this transform.\n *\n * @param result - The Mat4 to fill.\n * @returns The filled Mat4.\n */\n getMatrix(result: Mat4): Mat4 {\n _sv.set(this.scale, this.scale, this.scale);\n return result.setTRS(this.translation, this.rotation, _sv);\n }\n\n static freeze(t: Transform): Readonly<Transform> {\n Object.freeze(t.translation);\n Object.freeze(t.rotation);\n return Object.freeze(t);\n }\n\n static IDENTITY = Transform.freeze(new Transform());\n\n /**\n * PLY coordinate convention: 180-degree rotation around Z.\n * Used by formats that store Gaussian data in PLY-style coordinates:\n * PLY, splat, KSplat, SPZ, and SOG.\n */\n static PLY = Transform.freeze(new Transform().fromEulers(0, 0, 180));\n}\n\nexport { sigmoid, Transform };\n","import { TypedArray } from '../data-table';\n\n/**\n * Partition indices around the k-th smallest element using quickselect\n * (median-of-three pivot selection).\n *\n * After this call, `idx[k]` holds the index of the k-th smallest value\n * in `data`, and all indices before k map to smaller-or-equal values.\n *\n * @param data - The data array to use for comparison values.\n * @param idx - The index array to partition (mutated in place).\n * @param k - The target partition index.\n * @returns The index value at position k after partitioning.\n */\nconst quickselect = (data: TypedArray, idx: Uint32Array, k: number): number => {\n const valAt = (p: number) => data[idx[p]];\n const swap = (i: number, j: number) => {\n const t = idx[i];\n idx[i] = idx[j];\n idx[j] = t;\n };\n\n const n = idx.length;\n let l = 0;\n let r = n - 1;\n\n while (true) {\n if (r <= l + 1) {\n if (r === l + 1 && valAt(r) < valAt(l)) swap(l, r);\n return idx[k];\n }\n\n const mid = (l + r) >>> 1;\n swap(mid, l + 1);\n if (valAt(l) > valAt(r)) swap(l, r);\n if (valAt(l + 1) > valAt(r)) swap(l + 1, r);\n if (valAt(l) > valAt(l + 1)) swap(l, l + 1);\n\n let i = l + 1;\n let j = r;\n const pivotIdxVal = valAt(l + 1);\n const pivotIdx = idx[l + 1];\n\n while (true) {\n do {\n i++;\n } while (i <= r && valAt(i) < pivotIdxVal);\n do {\n j--;\n } while (j >= l && valAt(j) > pivotIdxVal);\n if (j < i) break;\n swap(i, j);\n }\n\n idx[l + 1] = idx[j];\n idx[j] = pivotIdx;\n\n if (j >= k) r = j - 1;\n if (j <= k) l = i;\n }\n};\n\nexport { quickselect };\n","import { Mat3 } from 'playcanvas';\n\n/* eslint-disable indent */\n\nconst kSqrt03_02 = Math.sqrt(3.0 / 2.0);\nconst kSqrt01_03 = Math.sqrt(1.0 / 3.0);\nconst kSqrt02_03 = Math.sqrt(2.0 / 3.0);\nconst kSqrt04_03 = Math.sqrt(4.0 / 3.0);\nconst kSqrt01_04 = Math.sqrt(1.0 / 4.0);\nconst kSqrt03_04 = Math.sqrt(3.0 / 4.0);\nconst kSqrt01_05 = Math.sqrt(1.0 / 5.0);\nconst kSqrt03_05 = Math.sqrt(3.0 / 5.0);\nconst kSqrt06_05 = Math.sqrt(6.0 / 5.0);\nconst kSqrt08_05 = Math.sqrt(8.0 / 5.0);\nconst kSqrt09_05 = Math.sqrt(9.0 / 5.0);\nconst kSqrt01_06 = Math.sqrt(1.0 / 6.0);\nconst kSqrt05_06 = Math.sqrt(5.0 / 6.0);\nconst kSqrt03_08 = Math.sqrt(3.0 / 8.0);\nconst kSqrt05_08 = Math.sqrt(5.0 / 8.0);\nconst kSqrt09_08 = Math.sqrt(9.0 / 8.0);\nconst kSqrt05_09 = Math.sqrt(5.0 / 9.0);\nconst kSqrt08_09 = Math.sqrt(8.0 / 9.0);\nconst kSqrt01_10 = Math.sqrt(1.0 / 10.0);\nconst kSqrt03_10 = Math.sqrt(3.0 / 10.0);\nconst kSqrt01_12 = Math.sqrt(1.0 / 12.0);\nconst kSqrt04_15 = Math.sqrt(4.0 / 15.0);\nconst kSqrt01_16 = Math.sqrt(1.0 / 16.0);\nconst kSqrt15_16 = Math.sqrt(15.0 / 16.0);\nconst kSqrt01_18 = Math.sqrt(1.0 / 18.0);\nconst kSqrt01_60 = Math.sqrt(1.0 / 60.0);\n\nconst dp = (n: number, start: number, a: number[] | Float32Array, b: number[] | Float32Array) => {\n let sum = 0;\n for (let i = 0; i < n; i++) {\n sum += a[start + i] * b[i];\n }\n return sum;\n};\n\nconst coeffsIn = new Float32Array(15);\n\n// Build a sparse representation of the SH rotation matrices. For axis-aligned\n// rotations the matrices are highly sparse, so iterating only over non-zero\n// entries is significantly faster than the full dot-product approach.\nconst buildSparse = (sh1: number[][], sh2: number[][], sh3: number[][]) => {\n const counts: number[] = [];\n const indices: number[] = [];\n const values: number[] = [];\n\n const addBand = (matrix: number[][], size: number, base: number) => {\n for (let i = 0; i < size; i++) {\n let count = 0;\n for (let j = 0; j < size; j++) {\n if (Math.abs(matrix[i][j]) > 1e-10) {\n indices.push(base + j);\n values.push(matrix[i][j]);\n count++;\n }\n }\n counts.push(count);\n }\n };\n\n addBand(sh1, 3, 0);\n addBand(sh2, 5, 3);\n addBand(sh3, 7, 8);\n\n return { counts, indices, values };\n};\n\n// Returns true if the rotation matrix is a signed permutation (every entry is 0 or ±1),\n// i.e. the rotation maps each axis to ±another axis (multiples of 90°).\nconst isAxisAligned = (rot: Float32Array) => {\n for (let i = 0; i < 9; i++) {\n const a = Math.abs(rot[i]);\n if (a > 0.01 && Math.abs(a - 1) > 0.01) return false;\n }\n return true;\n};\n\n// Rotate spherical harmonics up to band 3 based on https://github.com/andrewwillmott/sh-lib\n//\n// This implementation calculates the rotation factors during construction which can then\n// be used to rotate multiple spherical harmonics cheaply.\nclass RotateSH {\n apply: (result: Float32Array | number[], src?: Float32Array | number[]) => void;\n\n constructor(mat: Mat3) {\n const rot = mat.data;\n\n // band 1\n const sh1 = [\n [rot[4], -rot[7], rot[1]],\n [-rot[5], rot[8], -rot[2]],\n [rot[3], -rot[6], rot[0]]\n ];\n\n // band 2\n const sh2 = [[\n 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])),\n (sh1[2][1] * sh1[0][0] + sh1[0][1] * sh1[2][0]),\n kSqrt03_04 * (sh1[2][1] * sh1[0][1] + sh1[0][1] * sh1[2][1]),\n (sh1[2][1] * sh1[0][2] + sh1[0][1] * sh1[2][2]),\n 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]))\n ], [\n 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])),\n sh1[1][1] * sh1[0][0] + sh1[0][1] * sh1[1][0],\n kSqrt03_04 * (sh1[1][1] * sh1[0][1] + sh1[0][1] * sh1[1][1]),\n sh1[1][1] * sh1[0][2] + sh1[0][1] * sh1[1][2],\n 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]))\n ], [\n 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])),\n kSqrt04_03 * sh1[1][1] * sh1[1][0] - kSqrt01_03 * (sh1[2][1] * sh1[2][0] + sh1[0][1] * sh1[0][0]),\n sh1[1][1] * sh1[1][1] - kSqrt01_04 * (sh1[2][1] * sh1[2][1] + sh1[0][1] * sh1[0][1]),\n kSqrt04_03 * sh1[1][1] * sh1[1][2] - kSqrt01_03 * (sh1[2][1] * sh1[2][2] + sh1[0][1] * sh1[0][2]),\n 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]))\n ], [\n 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])),\n sh1[1][1] * sh1[2][0] + sh1[2][1] * sh1[1][0],\n kSqrt03_04 * (sh1[1][1] * sh1[2][1] + sh1[2][1] * sh1[1][1]),\n sh1[1][1] * sh1[2][2] + sh1[2][1] * sh1[1][2],\n 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]))\n ], [\n 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])),\n (sh1[2][1] * sh1[2][0] - sh1[0][1] * sh1[0][0]),\n kSqrt03_04 * (sh1[2][1] * sh1[2][1] - sh1[0][1] * sh1[0][1]),\n (sh1[2][1] * sh1[2][2] - sh1[0][1] * sh1[0][2]),\n 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]))\n ]];\n\n // band 3\n const sh3 = [[\n 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])),\n kSqrt03_02 * (sh1[2][1] * sh2[0][0] + sh1[0][1] * sh2[4][0]),\n kSqrt15_16 * (sh1[2][1] * sh2[0][1] + sh1[0][1] * sh2[4][1]),\n kSqrt05_06 * (sh1[2][1] * sh2[0][2] + sh1[0][1] * sh2[4][2]),\n kSqrt15_16 * (sh1[2][1] * sh2[0][3] + sh1[0][1] * sh2[4][3]),\n kSqrt03_02 * (sh1[2][1] * sh2[0][4] + sh1[0][1] * sh2[4][4]),\n 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]))\n ], [\n 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])),\n sh1[1][1] * sh2[0][0] + (sh1[2][1] * sh2[1][0] + sh1[0][1] * sh2[3][0]),\n kSqrt05_08 * sh1[1][1] * sh2[0][1] + kSqrt05_08 * (sh1[2][1] * sh2[1][1] + sh1[0][1] * sh2[3][1]),\n kSqrt05_09 * sh1[1][1] * sh2[0][2] + kSqrt05_09 * (sh1[2][1] * sh2[1][2] + sh1[0][1] * sh2[3][2]),\n kSqrt05_08 * sh1[1][1] * sh2[0][3] + kSqrt05_08 * (sh1[2][1] * sh2[1][3] + sh1[0][1] * sh2[3][3]),\n sh1[1][1] * sh2[0][4] + (sh1[2][1] * sh2[1][4] + sh1[0][1] * sh2[3][4]),\n 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]))\n ], [\n 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])),\n 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]),\n 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]),\n 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]),\n 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]),\n 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]),\n 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]))\n ], [\n 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])),\n kSqrt09_05 * sh1[1][1] * sh2[2][0] - kSqrt03_05 * (sh1[2][1] * sh2[3][0] + sh1[0][1] * sh2[1][0]),\n kSqrt09_08 * sh1[1][1] * sh2[2][1] - kSqrt03_08 * (sh1[2][1] * sh2[3][1] + sh1[0][1] * sh2[1][1]),\n sh1[1][1] * sh2[2][2] - kSqrt01_03 * (sh1[2][1] * sh2[3][2] + sh1[0][1] * sh2[1][2]),\n kSqrt09_08 * sh1[1][1] * sh2[2][3] - kSqrt03_08 * (sh1[2][1] * sh2[3][3] + sh1[0][1] * sh2[1][3]),\n kSqrt09_05 * sh1[1][1] * sh2[2][4] - kSqrt03_05 * (sh1[2][1] * sh2[3][4] + sh1[0][1] * sh2[1][4]),\n 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]))\n ], [\n 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])),\n 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]),\n 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]),\n 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]),\n 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]),\n 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]),\n 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]))\n ], [\n 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])),\n sh1[1][1] * sh2[4][0] + (sh1[2][1] * sh2[3][0] - sh1[0][1] * sh2[1][0]),\n kSqrt05_08 * sh1[1][1] * sh2[4][1] + kSqrt05_08 * (sh1[2][1] * sh2[3][1] - sh1[0][1] * sh2[1][1]),\n kSqrt05_09 * sh1[1][1] * sh2[4][2] + kSqrt05_09 * (sh1[2][1] * sh2[3][2] - sh1[0][1] * sh2[1][2]),\n kSqrt05_08 * sh1[1][1] * sh2[4][3] + kSqrt05_08 * (sh1[2][1] * sh2[3][3] - sh1[0][1] * sh2[1][3]),\n