igasset files and igpack bundles.
- TypeScript 47.8%
- C++ 46.3%
- CMake 3.1%
- WGSL 2.7%
Co-authored-by: Kamaron Peterson <kamaron.peterson@gmail.com> Reviewed-on: #47 |
||
|---|---|---|
| .forgejo/workflows | ||
| demo | ||
| packages/viewer-react | ||
| raw_assets | ||
| wasm | ||
| .clangd | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc.json | ||
| CMakeLists.txt | ||
| CMakePresets.json | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
igasset-viewer-react
React components for inspecting igasset / .igpack files via a WebAssembly + WebGPU runtime. Built to power asset visualization on indigocode.dev.
The library wraps a C++ WASM module (compiled via Emscripten) that owns a WebGPU device and an observable filestore. React components handle canvas lifecycle, pan/zoom/orbit interaction, and filestore state — you just mount a provider and render inspectors.
Installation
npm install @indigocode/igasset-viewer-react react react-dom
The published package includes binary artifacts that must be served at runtime:
.wasmfiles — Emscripten builds (single-threaded + multi-threaded). Modern bundlers handle these automatically vianew URL(..., import.meta.url)..igpackfiles — WGSL shaders and sample assets. You must servedist/igpack/at the/igpack/path on your host. See Bundler config below.
The package is published to the Forgejo NPM registry at git.indigocode.dev. Point the @indigocode scope there in your .npmrc if you haven't already.
Basic usage
Mount <IgviewerProvider>, then render an inspector inside it:
import { IgviewerProvider, TextureInspector } from "@indigocode/igasset-viewer-react";
export function App() {
return (
<IgviewerProvider fallback={<p>Loading WebGPU…</p>}>
<TextureInspector />
</IgviewerProvider>
);
}
<IgviewerProvider> loads the WASM module, acquires a WebGPU device, creates the runtime + filestore, and auto-loads required shaders. <TextureInspector> is an all-in-one component with a pan/zoom canvas, texture/format/sampler selectors, sprite list, and readouts.
For geometry inspection, swap in <GeometryInspector> (orbit-camera canvas + mesh selector):
import { IgviewerProvider, GeometryInspector } from "@indigocode/igasset-viewer-react";
export function App() {
return (
<IgviewerProvider fallback={<p>Loading WebGPU…</p>}>
<GeometryInspector />
</IgviewerProvider>
);
}
A bare canvas (no inspector UI) is also available:
import { IgviewerProvider, IgviewerCanvas } from "@indigocode/igasset-viewer-react";
export function App() {
return (
<IgviewerProvider>
<IgviewerCanvas style={{ width: "100%", height: 480 }} />
</IgviewerProvider>
);
}
Modest customization
Each inspector accepts custom children to compose your own layout. The provider wrapper is still supplied, so child components can consume context:
import {
TextureInspector,
TextureInspectorCanvas,
TextureInspectorImageSelector,
TextureInspectorFormatSelector,
TextureInspectorSizeReadout,
} from "@indigocode/igasset-viewer-react";
<TextureInspector>
<TextureInspectorCanvas />
<div style={{ display: "flex", gap: 12, padding: 8 }}>
<TextureInspectorImageSelector />
<TextureInspectorFormatSelector />
<TextureInspectorSizeReadout />
</div>
</TextureInspector>
Available texture sub-components: TextureInspectorCanvas, TextureInspectorImageSelector, TextureInspectorFormatSelector, TextureInspectorSamplerSelector, TextureInspectorSpriteList, TextureInspectorSizeReadout, TextureInspectorVRAMReadout, TextureInspectorDiskReadout, TextureInspectorPanZoomReadout, TextureInspectorStatusReadout.
Available geometry sub-components: GeometryInspectorCanvas, GeometryInspectorMeshSelector, GeometryInspectorVertexReadout, GeometryInspectorTriangleReadout, GeometryInspectorIndexFormatReadout, GeometryInspectorAttributesReadout, GeometryInspectorOrbitReadout, GeometryInspectorStatusReadout.
Advanced customization
Use the context hooks directly to build fully custom UIs:
import {
IgviewerProvider,
TextureInspectorProvider,
useTextureInspector,
} from "@indigocode/igasset-viewer-react";
function CustomTexturePicker() {
const { textureSources, selectedTextureIdx, setSelectedTextureIdx } =
useTextureInspector();
return (
<select
value={selectedTextureIdx}
onChange={(e) => setSelectedTextureIdx(+e.target.value)}
>
<option value={-1}>(none)</option>
{textureSources.map((src, i) => (
<option key={i} value={i}>
{src.sourceType === "igasset" ? src.igassetPath : src.igassetName}
</option>
))}
</select>
);
}
export function App() {
return (
<IgviewerProvider fallback={<p>Loading…</p>}>
<TextureInspectorProvider>
<CustomTexturePicker />
</TextureInspectorProvider>
</IgviewerProvider>
);
}
useTextureInspector() exposes: textureSources, selectedTextureIdx/setSelectedTextureIdx, format/setFormat, filterMode/setFilterMode, display (pan/zoom state), sizeText/vramText/diskText, sprites, status, and more. useGeometryInspector() exposes the analogous set for geometry.
For lower-level access, useIgviewer() returns { module, runtime, filestore, threading, capabilities }. Use useFilestoreState() for reactive filestore state and the filter functions (getTextureMetadata, getSpritesheetMetadata, getDracoMetadata, getWgslSourceMetadata) to extract typed metadata.
Loading files
import { useIgviewer } from "@indigocode/igasset-viewer-react";
function AssetLoader() {
const { filestore } = useIgviewer();
const handleFile = async (file: File) => {
const data = new Uint8Array(await file.arrayBuffer());
if (file.name.endsWith(".igasset")) filestore.load_igasset(file.name, data);
else if (file.name.endsWith(".igpack")) filestore.load_igpack(file.name, data);
};
return <input type="file" onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])} />;
}
Provider props
| Prop | Type | Default | Description |
|---|---|---|---|
threading |
"auto" | "single" | "multi" |
"auto" |
Auto picks multi-threaded if crossOriginIsolated, else single. |
wasmUrl |
string |
— | Override .wasm URL (bypasses import.meta.url resolution). |
device |
GPUDevice |
— | Inject a pre-existing WebGPU device (not destroyed on unmount). |
fallback |
ReactNode |
null |
Rendered during WASM + GPU initialization. |
errorFallback |
(error: Error) => ReactNode |
— | Rendered if initialization fails. |
Threading
The library ships two WASM builds. <IgviewerProvider> auto-selects based on crossOriginIsolated (requires COOP + COEP headers). Force one with threading="single" or threading="multi".
Building + running the demo locally
Prerequisites: Emscripten SDK, CMake 3.24+, Node 20+, pnpm 9+, a WebGPU-capable browser.
pnpm install
pnpm build:wasm # C++ → WASM via Emscripten (CMakePresets.json, reads $EMSDK)
pnpm dev # Vite demo at http://localhost:5173
For iterative C++ development:
pnpm dev:debug # debug WASM (-O0 -g3) + lib build + dev server
Other scripts: pnpm build:lib (library bundle), pnpm build:demo (demo production build), pnpm build (all three), pnpm format / pnpm format:check (Prettier).
CMake presets: emscripten (release, build/) and emscripten-debug (debug, build-debug/). Both read $EMSDK — no emcmake wrapper needed. See CMakePresets.json and .clangd for IDE integration.
Bundler config & static assets
Vite
The demo's vite.config.ts shows the full setup. The key pieces:
import { viteStaticCopy } from "vite-plugin-static-copy";
const coiHeaders = {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
};
export default defineConfig({
plugins: [
viteStaticCopy({
targets: [
{ src: "node_modules/@indigocode/igasset-viewer-react/dist/igpack/*", dest: "igpack" },
],
}),
],
server: { headers: coiHeaders }, // required for multi-threaded build
preview: { headers: coiHeaders },
});
The COOP/COEP headers enable crossOriginIsolated, which unlocks the multi-threaded WASM build. Without them, the provider falls back to single-threaded automatically.
Other bundlers
The .wasm files use new URL("igviewer.wasm", import.meta.url) — supported natively by Webpack 5+, RSPack, and Turbopack. The multi-threaded build also spawns a worker via new Worker(new URL("igviewer-mt.js", import.meta.url), { type: "module" }), which these bundlers also recognize.
For hosts where you can't set COOP/COEP headers (GitHub Pages, plain S3), use coi-serviceworker or force threading="single".
If your bundler can't handle the new URL pattern, copy the .wasm to your static dir and pass wasmUrl to <IgviewerProvider>.