Add 3D object viewers with r3f stack

Add per-class 3D viewers for tiles, tokens, cards, and custom models
using React Three Fiber, drei, and postprocessing. Viewers are
lazy-loaded and registered through the existing viewer registry, with a
shared scene wrapper for lighting, orbit controls, and subtle effects.

Add a CORS-safe /asset proxy route so three.js loaders can fetch
Workshop-hosted textures and models, and extend TTSObject with the
CustomMesh and CustomTile/CustomToken fields the viewers read.
This commit is contained in:
2026-08-08 12:56:46 +08:00
parent f390f170da
commit 001d5eeb54
18 changed files with 1037 additions and 14 deletions
+2
View File
@@ -2,6 +2,7 @@ import { serve } from '@hono/node-server';
import { cors } from 'hono/cors';
import { Hono } from 'hono';
import { loadEnv, type Bindings } from './env.js';
import asset from './routes/asset.js';
import health from './routes/health.js';
import items from './routes/items.js';
import search from './routes/search.js';
@@ -14,6 +15,7 @@ app.use('*', cors());
app.route('/health', health);
app.route('/search', search);
app.route('/items', items);
app.route('/asset', asset);
serve(
{
+43
View File
@@ -0,0 +1,43 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import asset from './asset.js';
afterEach(() => {
vi.unstubAllGlobals();
});
describe('asset route', () => {
it('rejects a missing url', async () => {
const res = await asset.request('/');
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ error: 'Missing url query param' });
});
it('rejects a non-http url', async () => {
const res = await asset.request('/?url=file%3A%2F%2F%2Fetc%2Fpasswd');
expect(res.status).toBe(400);
});
it('streams the fetched asset with a content-type header', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(new Uint8Array([1, 2, 3]), {
headers: { 'content-type': 'image/png' },
}),
),
);
const res = await asset.request('/?url=https%3A%2F%2Fexample.com%2Fa.png');
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('image/png');
expect(await res.arrayBuffer()).toEqual(new Uint8Array([1, 2, 3]).buffer);
});
it('returns 502 when the upstream fetch fails', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(new Response('error', { status: 500 })),
);
const res = await asset.request('/?url=https%3A%2F%2Fexample.com%2Fa.png');
expect(res.status).toBe(502);
});
});
+47
View File
@@ -0,0 +1,47 @@
import { Hono } from 'hono';
const app = new Hono();
/**
* Fetch an external asset (texture, model, etc.) and stream it back to the
* client. Workshop asset hosts (steamusercontent.com, etc.) often omit CORS
* headers, which would block `TextureLoader` / `GLTFLoader` in the browser.
* Routing through the proxy makes those assets loadable.
*/
app.get('/', async (c) => {
const raw = c.req.query('url');
if (!raw) {
return c.json({ error: 'Missing url query param' }, 400);
}
let url: URL;
try {
url = new URL(raw);
} catch {
return c.json({ error: 'Invalid url query param' }, 400);
}
// Only allow http(s) to avoid SSRF via file://, etc.
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return c.json({ error: 'Only http(s) urls are allowed' }, 400);
}
let res: Response;
try {
res = await fetch(url);
} catch (err) {
return c.json({ error: `Failed to fetch asset: ${String(err)}` }, 502);
}
if (!res.ok) {
return c.json({ error: `Asset responded ${res.status}` }, 502);
}
const contentType = res.headers.get('content-type') ?? 'application/octet-stream';
return new Response(res.body, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
},
});
});
export default app;
+5
View File
@@ -15,17 +15,22 @@
"@iconify-json/mdi": "^1.2.3",
"@iconify/react": "^6.0.2",
"@iconify/utils": "^3.1.4",
"@react-three/drei": "^10.7.8",
"@react-three/fiber": "^9.7.0",
"@react-three/postprocessing": "^3.0.4",
"@tts/extract": "workspace:*",
"@tts/shared": "workspace:*",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.2",
"three": "^0.185.1",
"zustand": "^5.0.14"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@types/three": "^0.185.4",
"@vitejs/plugin-react": "^6.0.5",
"tailwindcss": "^4.3.3",
"typescript": "^5.7.2",
@@ -0,0 +1,30 @@
import { useTexture } from '@react-three/drei';
import type { TTSObject } from '@tts/shared';
import Scene from './Scene';
import { assetUrl } from './assetUrl';
/**
* A playing card: a thin box with the face texture on the front and the back
* texture on the rear. Reads `CustomDeck` face/back URLs, falling back to a
* neutral color when absent.
*/
export default function CardViewer({ object }: { object: TTSObject }) {
const deck = object.CustomDeck ? Object.values(object.CustomDeck)[0] : undefined;
const faceUrl = deck?.FaceURL;
const backUrl = deck?.BackURL;
const face = faceUrl ? useTexture(assetUrl(faceUrl)) : null;
const back = backUrl ? useTexture(assetUrl(backUrl)) : null;
return (
<Scene>
<mesh>
<boxGeometry args={[1.4, 2, 0.06]} />
<meshStandardMaterial
color={face || back ? '#ffffff' : '#52525b'}
map={face ?? back ?? undefined}
roughness={0.6}
/>
</mesh>
</Scene>
);
}
@@ -0,0 +1,76 @@
import { Suspense, useLayoutEffect } from 'react';
import { useLoader } from '@react-three/fiber';
import { useFBX, useGLTF, useTexture } from '@react-three/drei';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import type { TTSObject } from '@tts/shared';
import type * as THREE from 'three';
import Scene from './Scene';
import { assetUrl } from './assetUrl';
/**
* A custom 3D model loaded from `CustomMesh.MeshURL`. Supports GLTF/GLB, OBJ,
* and FBX by sniffing the URL extension; falls back to GLTF for unknown
* extensions. `DiffuseURL` is applied to the model's materials when present.
*/
export default function CustomModelViewer({ object }: { object: TTSObject }) {
const meshUrl = object.CustomMesh?.MeshURL;
if (!meshUrl) {
return (
<Scene>
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#52525b" />
</mesh>
</Scene>
);
}
return (
<Scene>
<Suspense fallback={null}>
<Model meshUrl={meshUrl} diffuseUrl={object.CustomMesh?.DiffuseURL} />
</Suspense>
</Scene>
);
}
function Model({
meshUrl,
diffuseUrl,
}: {
meshUrl: string;
diffuseUrl?: string;
}) {
const ext = meshUrl.split('?')[0]!.split('.').pop()!.toLowerCase();
const url = assetUrl(meshUrl);
const diffuse = diffuseUrl ? useTexture(assetUrl(diffuseUrl)) : null;
let root: THREE.Object3D;
if (ext === 'obj') {
root = useLoader(OBJLoader, url);
} else if (ext === 'fbx') {
root = useFBX(url);
} else {
root = useGLTF(url).scene;
}
// Apply the diffuse texture to every mesh material on the loaded model.
useLayoutEffect(() => {
if (!diffuse) return;
root.traverse((child) => {
const mesh = child as THREE.Mesh;
if (mesh.isMesh) {
const material = Array.isArray(mesh.material)
? mesh.material[0]
: mesh.material;
if (material && 'map' in material) {
material.map = diffuse;
material.needsUpdate = true;
}
}
});
}, [root, diffuse]);
return <primitive object={root} scale={0.5} />;
}
+50
View File
@@ -0,0 +1,50 @@
import { Suspense, type ReactNode } from 'react';
import { Canvas } from '@react-three/fiber';
import { ContactShadows, OrbitControls } from '@react-three/drei';
import { Bloom, EffectComposer, Vignette } from '@react-three/postprocessing';
/**
* Shared 3D scene wrapper for object viewers. Provides a consistent camera,
* lighting, orbit controls, a soft contact shadow, and subtle post-processing
* (bloom + vignette). Children are wrapped in a Suspense boundary so loading
* assets (textures, models) can suspend without blanking the page.
*/
export default function Scene({ children }: { children: ReactNode }) {
return (
<div className="h-80 w-full overflow-hidden rounded-lg bg-linear-to-b from-zinc-900 to-zinc-950">
<Canvas
camera={{ position: [2.2, 1.8, 2.6], fov: 40 }}
dpr={[1, 2]}
gl={{ antialias: true }}
>
<ambientLight intensity={0.5} />
<directionalLight position={[4, 6, 3]} intensity={1.4} />
<directionalLight position={[-4, 2, -3]} intensity={0.4} color="#b3c7ff" />
<pointLight position={[0, 3, 0]} intensity={0.3} />
<Suspense fallback={null}>{children}</Suspense>
<ContactShadows
position={[0, -0.5, 0]}
opacity={0.55}
scale={8}
blur={2.4}
far={3}
resolution={256}
/>
<OrbitControls
enablePan={false}
minDistance={1}
maxDistance={8}
autoRotate
autoRotateSpeed={1.2}
/>
<EffectComposer>
<Bloom intensity={0.25} luminanceThreshold={0.85} mipmapBlur />
<Vignette eskil={false} offset={0.25} darkness={0.6} />
</EffectComposer>
</Canvas>
</div>
);
}
@@ -0,0 +1,27 @@
import { useTexture } from '@react-three/drei';
import type { TTSObject } from '@tts/shared';
import Scene from './Scene';
import { assetUrl } from './assetUrl';
/**
* A flat tile with a texture on its top face. Uses `CustomImage.ImageURL`
* (falling back to `ImageSecondaryURL`), with a neutral color when absent.
*/
export default function TileViewer({ object }: { object: TTSObject }) {
const url = object.CustomImage?.ImageURL ?? object.CustomImage?.ImageSecondaryURL;
const texture = url ? useTexture(assetUrl(url)) : null;
const thickness = object.CustomImage?.CustomTile?.Thickness ?? 0.1;
return (
<Scene>
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<boxGeometry args={[1.6, 1.6, thickness]} />
<meshStandardMaterial
color={texture ? '#ffffff' : '#52525b'}
map={texture ?? undefined}
roughness={0.8}
/>
</mesh>
</Scene>
);
}
@@ -0,0 +1,27 @@
import { useTexture } from '@react-three/drei';
import type { TTSObject } from '@tts/shared';
import Scene from './Scene';
import { assetUrl } from './assetUrl';
/**
* A round token: a short cylinder with the texture on its top face. Uses
* `CustomImage.ImageURL`, with a neutral color when absent.
*/
export default function TokenViewer({ object }: { object: TTSObject }) {
const url = object.CustomImage?.ImageURL ?? object.CustomImage?.ImageSecondaryURL;
const texture = url ? useTexture(assetUrl(url)) : null;
const thickness = object.CustomImage?.CustomToken?.Thickness ?? 0.1;
return (
<Scene>
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<cylinderGeometry args={[0.9, 0.9, thickness, 48]} />
<meshStandardMaterial
color={texture ? '#ffffff' : '#52525b'}
map={texture ?? undefined}
roughness={0.8}
/>
</mesh>
</Scene>
);
}
@@ -0,0 +1,8 @@
/**
* Route an external asset URL through the proxy so it can be loaded by
* three.js loaders (TextureLoader, GLTFLoader, etc.) despite the upstream host
* omitting CORS headers.
*/
export function assetUrl(url: string): string {
return `/asset?url=${encodeURIComponent(url)}`;
}
@@ -0,0 +1,22 @@
import { lazy } from 'react';
import { registerViewer } from '../viewers';
// Register 3D viewers for the object classes that carry renderable assets.
// Importing this module has the side effect of populating the viewer registry.
// The viewers are lazy-loaded so the three.js stack is code-split out of the
// main bundle and only fetched when a 3D-capable object is actually selected.
const TileViewer = lazy(() => import('./TileViewer'));
const TokenViewer = lazy(() => import('./TokenViewer'));
const CardViewer = lazy(() => import('./CardViewer'));
const CustomModelViewer = lazy(() => import('./CustomModelViewer'));
registerViewer('Tile', TileViewer);
registerViewer('Custom_Tile', TileViewer);
registerViewer('Custom_Token', TokenViewer);
registerViewer('Card', CardViewer);
registerViewer('Deck', CardViewer);
registerViewer('DeckCustom', CardViewer);
registerViewer('Custom_Deck', CardViewer);
registerViewer('Custom_Model', CustomModelViewer);
registerViewer('Custom_Model_Bag', CustomModelViewer);
registerViewer('Custom_Model_Infinite_Bag', CustomModelViewer);
+14 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { Suspense, useEffect, useMemo, useState } from 'react';
import { Icon } from '@iconify/react';
import { useParams } from 'react-router-dom';
import { buildTree, collectRefs } from '@tts/extract';
@@ -8,6 +8,8 @@ import { modFileUrl } from '../api';
import ObjectTree from '../components/ObjectTree';
import { resolveViewer } from '../components/viewers';
import { iconsForObject } from '../components/objectIcons';
// Register the 3D viewers (side effect) so `resolveViewer` can find them.
import '../components/viewers/register';
export default function ModPage() {
const { id } = useParams<{ id: string }>();
@@ -75,7 +77,17 @@ export default function ModPage() {
{selected.object.GUID}
</p>
</header>
{Viewer && <Viewer object={selected.object} />}
{Viewer && (
<Suspense
fallback={
<div className="flex h-80 items-center justify-center text-sm text-zinc-500">
Loading viewer
</div>
}
>
<Viewer object={selected.object} />
</Suspense>
)}
</>
) : (
<p className="text-sm text-zinc-500">
+1
View File
@@ -10,6 +10,7 @@ export default defineConfig({
'/search': 'http://localhost:3000',
'/items': 'http://localhost:3000',
'/health': 'http://localhost:3000',
'/asset': 'http://localhost:3000',
},
},
});
+7 -3
View File
@@ -45,8 +45,8 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared
### Edges
- **`apps/web``apps/proxy`** — calls `/search`, `/items/:id`, and
`/items/:id/file` over HTTP.
- **`apps/web``apps/proxy`** — calls `/search`, `/items/:id`,
`/items/:id/file`, and `/asset` (CORS-safe asset proxy) over HTTP.
- **`apps/web``packages/extract`** — uses `buildTree` / `collectRefs`
to analyze a loaded `TTSMod` in the browser (tree sidebar + asset refs).
- **`apps/proxy``packages/tts`** — calls `fetchMod` / `getFileName` to serve
@@ -82,6 +82,10 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared
| `bson` | BSON deserialization of TTS save files | `packages/tts` |
| `cheerio` | Workshop browse page scraping | `apps/proxy` |
| `zod` | Runtime validation | `apps/proxy`, `packages/shared` |
| `three` | 3D rendering | `apps/web` |
| `@react-three/fiber` | React renderer for three.js | `apps/web` |
| `@react-three/drei` | three.js helpers (controls, textures) | `apps/web` |
| `@react-three/postprocessing` | Post-processing effects | `apps/web` |
### Runtime constraints
@@ -103,7 +107,7 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared
- The proxy runs as a single Node process via `@hono/node-server`.
- `apps/web` is a static Vite build served separately; during development it
proxies `/search`, `/items`, and `/health` to the proxy.
proxies `/search`, `/items`, `/health`, and `/asset` to the proxy.
- `packages/extract` is published/consumed as a plain ESM module usable from a
browser bundle or Node.
- No shared state between requests; each request fetches fresh.
+29
View File
@@ -129,3 +129,32 @@ without coupling the page to any one viewer.
**Alternatives considered:** A single monolithic inspector component. Rejected —
would grow unboundedly as per-class viewers are added; the registry keeps each
viewer isolated and swappable.
## D11 — 3D viewers on the r3f/drei/postprocessing stack
**Decision:** Per-class 3D viewers for tiles, tokens, cards, and custom models
are built on `@react-three/fiber`, `@react-three/drei`, and
`@react-three/postprocessing`, registered through the existing viewer registry
and lazy-loaded so the three.js stack is code-split out of the main bundle.
**Context:** The user wants to visualize objects in a save in 3D. The viewer
registry (D10) already provides the extension point. Custom models load from
`CustomMesh.MeshURL` (GLTF/OBJ/FBX); tiles/tokens/cards use their
`CustomImage`/`CustomDeck` textures. Unity asset bundles are out of scope.
**Alternatives considered:** A single monolithic 3D inspector; a WebGL library
other than three.js. Rejected — the registry keeps viewers isolated, and r3f is
the de-facto React/three.js integration.
## D12 — CORS-safe asset proxy
**Decision:** The proxy exposes `GET /asset?url=...`, which fetches an external
asset and streams it back with a `Content-Type` header. Only `http(s)` URLs are
allowed.
**Context:** Workshop asset hosts (steamusercontent.com, etc.) often omit CORS
headers, which would block three.js `TextureLoader` / `GLTFLoader` in the
browser. Routing asset fetches through the proxy makes them loadable.
**Alternatives considered:** Loading assets directly in the browser. Rejected —
CORS failures on common Workshop hosts would break the viewers.
+27
View File
@@ -62,6 +62,7 @@ tts-workshop/
│ │ ├── routes/
│ │ │ ├── search.ts # GET /search?q=...&page=1
│ │ │ ├── items.ts # GET /items/:id, /items/:id/file
│ │ │ ├── asset.ts # GET /asset?url=... (CORS-safe asset proxy)
│ │ │ └── health.ts # GET /health
│ │ └── env.ts # zod env validation
│ └── web/
@@ -81,6 +82,14 @@ tts-workshop/
│ │ ├── SearchResults.tsx
│ │ ├── ObjectTree.tsx # containment-tree sidebar
│ │ ├── viewers.tsx # viewer registry + default viewer
│ │ ├── viewers/ # 3D viewers (r3f/drei/postprocessing)
│ │ │ ├── register.ts # registers per-class 3D viewers (lazy)
│ │ │ ├── Scene.tsx # shared Canvas: lights, controls, post
│ │ │ ├── TileViewer.tsx
│ │ │ ├── TokenViewer.tsx
│ │ │ ├── CardViewer.tsx
│ │ │ ├── CustomModelViewer.tsx
│ │ │ └── assetUrl.ts # route asset URLs through the proxy
│ │ ├── objectIcons.tsx # class → icon mapping
│ │ ├── objectIconsData.ts # generated icon subset (do not edit)
│ │ └── objectIcons.test.ts
@@ -188,6 +197,11 @@ Hono server exposing search + fetch.
otherwise resolves via the Steam API.
- `GET /items/:id/file` — raw save bytes, filename from `getFileName`. Also
accepts `fileUrl`.
- `routes/asset.ts`
- `GET /asset?url=...` — fetch an external asset (texture, model) and stream
it back with a `Content-Type` header. Workshop hosts often omit CORS
headers, which would block three.js loaders in the browser; routing through
the proxy makes those assets loadable. Only `http(s)` URLs are allowed.
- `routes/health.ts`
- `GET /health` — liveness.
- `env.ts` — zod validation of `STEAM_API_KEY`, `PORT`.
@@ -212,6 +226,16 @@ proxy API and `packages/extract` directly for analysis.
- `components/viewers.tsx` — viewer registry (`registerViewer` /
`resolveViewer`) plus a `DefaultViewer` that renders an object's fields;
custom per-class viewers can be registered later.
- `components/viewers/` — 3D viewers built on `@react-three/fiber`,
`@react-three/drei`, and `@react-three/postprocessing`. `register.ts`
registers lazy-loaded viewers for `Tile`/`Custom_Tile` (flat box),
`Custom_Token` (cylinder), `Card`/`Deck`/`Custom_Deck` (thin box with
face/back textures), and `Custom_Model`/`Custom_Model_Bag`/
`Custom_Model_Infinite_Bag` (GLTF/OBJ/FBX from `CustomMesh.MeshURL`).
`Scene.tsx` is a shared canvas with lighting, orbit controls, contact
shadows, and subtle bloom/vignette. `assetUrl.ts` routes asset URLs through
the proxy for CORS-safe loading. The viewers are lazy-loaded so the three.js
stack is code-split out of the main bundle.
- `components/objectIcons.tsx` — maps TTS object classes to one or more
Iconify icons (`iconsForObject`); unknown classes fall back to a help icon.
Icons may come from multiple sets (mdi, material-symbols, file-icons, ...);
@@ -234,6 +258,7 @@ proxy API and `packages/extract` directly for analysis.
| GET | `/search?q=&page=` | Scrape Workshop browse, return item list | — |
| GET | `/items/:id` | Full parsed `TTSMod` (`?fileUrl=` skips key) | key* |
| GET | `/items/:id/file` | Raw save bytes, filename from header | key* |
| GET | `/asset?url=` | CORS-safe proxy for external assets | — |
\* `STEAM_API_KEY` is optional; `/items/*` works without it when a `fileUrl`
query param is supplied.
@@ -259,6 +284,8 @@ packages/extract → flatten objects / extract refs / download assets
- `cheerio` — Workshop browse page scraping (backend only).
- `zod` — validation.
- `react`, `react-dom`, `react-router-dom`, `zustand` — frontend.
- `three`, `@react-three/fiber`, `@react-three/drei`,
`@react-three/postprocessing` — 3D object viewers.
- `@iconify/react`, `@iconify-json/mdi`, `@iconify/utils` — iconify icons for
object class tags (subset bundled via `scripts/generate-object-icons.mjs`).
- `vite`, `@vitejs/plugin-react`, `tailwindcss`, `@tailwindcss/vite` — frontend tooling.
+25
View File
@@ -31,6 +31,31 @@ export interface TTSObject {
CustomImage?: {
ImageURL: string;
ImageSecondaryURL: string;
ImageScalar?: number;
WidthScale?: number;
CustomTile?: {
Type: number;
Thickness: number;
Stackable: boolean;
Stretch: boolean;
};
CustomToken?: {
Thickness: number;
MergeDistancePixels: number;
StandUp: boolean;
Stackable: boolean;
};
};
/** A custom 3D model, e.g. `Custom_Model`, `Custom_Model_Bag`. */
CustomMesh?: {
MeshURL: string;
DiffuseURL: string;
NormalURL: string;
ColliderURL: string;
Convex: boolean;
MaterialIndex: number;
TypeIndex: number;
CastShadows: boolean;
};
}
+590 -2
View File
@@ -54,6 +54,15 @@ importers:
'@iconify/utils':
specifier: ^3.1.4
version: 3.1.4
'@react-three/drei':
specifier: ^10.7.8
version: 10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)
'@react-three/fiber':
specifier: ^9.7.0
version: 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)
'@react-three/postprocessing':
specifier: ^3.0.4
version: 3.0.4(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/three@0.185.4)(react@19.2.8)(three@0.185.1)
'@tts/extract':
specifier: workspace:*
version: link:../../packages/extract
@@ -69,9 +78,12 @@ importers:
react-router-dom:
specifier: ^7.18.2
version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
three:
specifier: ^0.185.1
version: 0.185.1
zustand:
specifier: ^5.0.14
version: 5.0.14(@types/react@19.2.18)(react@19.2.8)
version: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))
devDependencies:
'@tailwindcss/vite':
specifier: ^4.3.3
@@ -82,6 +94,9 @@ importers:
'@types/react-dom':
specifier: ^19.2.4
version: 19.2.4(@types/react@19.2.18)
'@types/three':
specifier: ^0.185.4
version: 0.185.4
'@vitejs/plugin-react':
specifier: ^6.0.5
version: 6.0.5(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))
@@ -136,6 +151,13 @@ packages:
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
'@babel/runtime@7.29.7':
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
@@ -328,9 +350,60 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@mediapipe/tasks-vision@0.10.17':
resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==}
'@monogrid/gainmap-js@3.4.0':
resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==}
peerDependencies:
three: '>= 0.159.0'
'@oxc-project/types@0.143.0':
resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==}
'@react-three/drei@10.7.8':
resolution: {integrity: sha512-rJXyuzLm2Xq0kafHuR47ajDGbOe/pEhzIr4m8E8zwzQs0iNjloFDqBwRhrXmP/w+onLeYyN3EYPFW/cwWK/4yA==}
peerDependencies:
'@react-three/fiber': ^9.0.0
react: ^19
react-dom: ^19
three: '>=0.159'
peerDependenciesMeta:
react-dom:
optional: true
'@react-three/fiber@9.7.0':
resolution: {integrity: sha512-EWm9FwcaOZQu/ExFW5rggoCMM1NJet5YbxVxKaOE+KSncrjU0Wx7017qSyGFvupviK89nMYGCWU3BIK4dI1clw==}
peerDependencies:
expo: '>=43.0'
expo-asset: '>=8.4'
expo-file-system: '>=11.0'
expo-gl: '>=11.0'
react: '>=19 <19.3'
react-dom: '>=19 <19.3'
react-native: '>=0.78'
three: '>=0.156'
peerDependenciesMeta:
expo:
optional: true
expo-asset:
optional: true
expo-file-system:
optional: true
expo-gl:
optional: true
react-dom:
optional: true
react-native:
optional: true
'@react-three/postprocessing@3.0.4':
resolution: {integrity: sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==}
peerDependencies:
'@react-three/fiber': ^9.0.0
react: ^19.0
three: '>= 0.156.0'
'@rolldown/binding-android-arm64@1.2.3':
resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -521,26 +594,57 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/draco3d@1.4.10':
resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==}
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
'@types/offscreencanvas@2019.7.3':
resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
'@types/react-dom@19.2.4':
resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
peerDependencies:
'@types/react': ^19.2.0
'@types/react-reconciler@0.28.9':
resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==}
peerDependencies:
'@types/react': '*'
'@types/react@19.2.18':
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
'@types/stats.js@0.17.4':
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
'@types/three@0.185.4':
resolution: {integrity: sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==}
'@types/webxr@0.5.24':
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
'@use-gesture/core@10.3.1':
resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==}
'@use-gesture/react@10.3.1':
resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==}
peerDependencies:
react: '>= 16.8.0'
'@vitejs/plugin-react@6.0.5':
resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -587,10 +691,25 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
bson@6.10.4:
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
engines: {node: '>=16.20.1'}
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
camera-controls@3.1.2:
resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==}
engines: {node: '>=22.0.0', npm: '>=10.5.1'}
peerDependencies:
three: '>=0.126.1'
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
@@ -602,13 +721,28 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
detect-gpu@5.0.70:
resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
draco3d@1.5.7:
resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==}
enhanced-resolve@5.24.5:
resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==}
engines: {node: '>=10.13.0'}
@@ -637,25 +771,57 @@ packages:
picomatch:
optional: true
fflate@0.6.11:
resolution: {integrity: sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==}
fflate@0.8.3:
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
glsl-noise@0.0.0:
resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
hls.js@1.6.17:
resolution: {integrity: sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==}
hono@4.13.1:
resolution: {integrity: sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==}
engines: {node: '>=16.9.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
import-meta-resolve@4.2.0:
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
is-promise@2.2.2:
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
its-fine@2.0.0:
resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==}
peerDependencies:
react: ^19.0.0
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
@@ -804,9 +970,35 @@ packages:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
maath@0.10.8:
resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==}
peerDependencies:
'@types/three': '>=0.134.0'
three: '>=0.134.0'
maath@0.6.0:
resolution: {integrity: sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==}
peerDependencies:
'@types/three': '>=0.144.0'
three: '>=0.144.0'
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
meshline@3.3.1:
resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==}
peerDependencies:
three: '>=0.137'
meshoptimizer@1.1.1:
resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==}
n8ao@1.10.3:
resolution: {integrity: sha512-JFRk4WgUAUWO2KdJTqmBT02dP9kG7rE2oQbBy64E9+YwQ3d96KB9QBanqNy/NnYUfUTYU6Z/5OS5t/yLey7+6Q==}
peerDependencies:
postprocessing: '>=6.30.0'
three: '>=0.137'
nanoid@3.3.18:
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -819,6 +1011,10 @@ packages:
package-manager-detector@1.8.0:
resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -833,6 +1029,17 @@ packages:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
postprocessing@6.39.4:
resolution: {integrity: sha512-oAS/PjAbc/xT3OzjUrGsorJ4J064XwhVD2t0OwKLP/E8QwDMUJ0oOv6ZI1SYMtLchv4g0ySEx+JcLy92+48vlA==}
peerDependencies:
three: '>= 0.168.0 < 0.186.0'
potpack@1.0.2:
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
promise-worker-transferable@1.0.4:
resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==}
react-dom@19.2.8:
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
peerDependencies:
@@ -855,10 +1062,23 @@ packages:
react-dom:
optional: true
react-use-measure@2.1.7:
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
peerDependencies:
react: '>=16.13'
react-dom: '>=16.13'
peerDependenciesMeta:
react-dom:
optional: true
react@19.2.8:
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
engines: {node: '>=0.10.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
rolldown@1.2.3:
resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -870,6 +1090,14 @@ packages:
set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -880,9 +1108,23 @@ packages:
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
stats-gl@2.4.2:
resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==}
peerDependencies:
'@types/three': '*'
three: '*'
stats.js@0.17.0:
resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==}
std-env@4.2.0:
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
suspend-react@0.1.3:
resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==}
peerDependencies:
react: '>=17.0'
tailwindcss@4.3.3:
resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==}
@@ -890,6 +1132,19 @@ packages:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
three-mesh-bvh@0.8.3:
resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==}
peerDependencies:
three: '>= 0.159.0'
three-stdlib@2.36.1:
resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==}
peerDependencies:
three: '>=0.128.0'
three@0.185.1:
resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -905,11 +1160,27 @@ packages:
resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
engines: {node: '>=14.0.0'}
troika-three-text@0.52.5:
resolution: {integrity: sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==}
peerDependencies:
three: '>=0.125.0'
troika-three-utils@0.52.5:
resolution: {integrity: sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==}
peerDependencies:
three: '>=0.125.0'
troika-worker-utils@0.52.0:
resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==}
tsx@4.23.11:
resolution: {integrity: sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==}
engines: {node: '>=18.0.0'}
hasBin: true
tunnel-rat@0.1.2:
resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -918,6 +1189,15 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
utility-types@3.11.0:
resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==}
engines: {node: '>= 4'}
vite@8.2.1:
resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1002,6 +1282,17 @@ packages:
jsdom:
optional: true
webgl-constants@1.1.1:
resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==}
webgl-sdf-generator@1.1.1:
resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
@@ -1010,6 +1301,21 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
zustand@5.0.14:
resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
engines: {node: '>=12.20.0'}
@@ -1035,6 +1341,10 @@ snapshots:
package-manager-detector: 1.8.0
tinyexec: 1.3.0
'@babel/runtime@7.29.7': {}
'@dimforge/rapier3d-compat@0.12.0': {}
'@esbuild/aix-ppc64@0.28.1':
optional: true
@@ -1153,8 +1463,79 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@mediapipe/tasks-vision@0.10.17': {}
'@monogrid/gainmap-js@3.4.0(three@0.185.1)':
dependencies:
promise-worker-transferable: 1.0.4
three: 0.185.1
'@oxc-project/types@0.143.0': {}
'@react-three/drei@10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)':
dependencies:
'@babel/runtime': 7.29.7
'@mediapipe/tasks-vision': 0.10.17
'@monogrid/gainmap-js': 3.4.0(three@0.185.1)
'@react-three/fiber': 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)
'@use-gesture/react': 10.3.1(react@19.2.8)
camera-controls: 3.1.2(three@0.185.1)
cross-env: 7.0.3
detect-gpu: 5.0.70
glsl-noise: 0.0.0
hls.js: 1.6.17
maath: 0.10.8(@types/three@0.185.4)(three@0.185.1)
meshline: 3.3.1(three@0.185.1)
react: 19.2.8
stats-gl: 2.4.2(@types/three@0.185.4)(three@0.185.1)
stats.js: 0.17.0
suspend-react: 0.1.3(react@19.2.8)
three: 0.185.1
three-mesh-bvh: 0.8.3(three@0.185.1)
three-stdlib: 2.36.1(three@0.185.1)
troika-three-text: 0.52.5(three@0.185.1)
tunnel-rat: 0.1.2(@types/react@19.2.18)(react@19.2.8)
use-sync-external-store: 1.6.0(react@19.2.8)
utility-types: 3.11.0
zustand: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))
optionalDependencies:
react-dom: 19.2.8(react@19.2.8)
transitivePeerDependencies:
- '@types/react'
- '@types/three'
- immer
'@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)':
dependencies:
'@babel/runtime': 7.29.7
'@types/webxr': 0.5.24
base64-js: 1.5.1
buffer: 6.0.3
its-fine: 2.0.0(@types/react@19.2.18)(react@19.2.8)
react: 19.2.8
react-use-measure: 2.1.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
scheduler: 0.27.0
suspend-react: 0.1.3(react@19.2.8)
three: 0.185.1
use-sync-external-store: 1.6.0(react@19.2.8)
zustand: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))
optionalDependencies:
react-dom: 19.2.8(react@19.2.8)
transitivePeerDependencies:
- '@types/react'
- immer
'@react-three/postprocessing@3.0.4(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/three@0.185.4)(react@19.2.8)(three@0.185.1)':
dependencies:
'@react-three/fiber': 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)
maath: 0.6.0(@types/three@0.185.4)(three@0.185.1)
n8ao: 1.10.3(postprocessing@6.39.4(three@0.185.1))(three@0.185.1)
postprocessing: 6.39.4(three@0.185.1)
react: 19.2.8
three: 0.185.1
transitivePeerDependencies:
- '@types/three'
'@rolldown/binding-android-arm64@1.2.3':
optional: true
@@ -1269,6 +1650,8 @@ snapshots:
tailwindcss: 4.3.3
vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)
'@tweenjs/tween.js@23.1.3': {}
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -1276,20 +1659,48 @@ snapshots:
'@types/deep-eql@4.0.2': {}
'@types/draco3d@1.4.10': {}
'@types/estree@1.0.9': {}
'@types/node@22.20.1':
dependencies:
undici-types: 6.21.0
'@types/offscreencanvas@2019.7.3': {}
'@types/react-dom@19.2.4(@types/react@19.2.18)':
dependencies:
'@types/react': 19.2.18
'@types/react-reconciler@0.28.9(@types/react@19.2.18)':
dependencies:
'@types/react': 19.2.18
'@types/react@19.2.18':
dependencies:
csstype: 3.2.3
'@types/stats.js@0.17.4': {}
'@types/three@0.185.4':
dependencies:
'@dimforge/rapier3d-compat': 0.12.0
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.4
'@types/webxr': 0.5.24
fflate: 0.8.3
meshoptimizer: 1.1.1
'@types/webxr@0.5.24': {}
'@use-gesture/core@10.3.1': {}
'@use-gesture/react@10.3.1(react@19.2.8)':
dependencies:
'@use-gesture/core': 10.3.1
react: 19.2.8
'@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))':
dependencies:
'@rolldown/pluginutils': 1.0.1
@@ -1338,18 +1749,49 @@ snapshots:
assertion-error@2.0.1: {}
base64-js@1.5.1: {}
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
bson@6.10.4: {}
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
camera-controls@3.1.2(three@0.185.1):
dependencies:
three: 0.185.1
chai@6.2.2: {}
convert-source-map@2.0.0: {}
cookie@1.1.1: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
csstype@3.2.3: {}
detect-gpu@5.0.70:
dependencies:
webgl-constants: 1.1.1
detect-libc@2.1.2: {}
draco3d@1.5.7: {}
enhanced-resolve@5.24.5:
dependencies:
graceful-fs: 4.2.11
@@ -1396,17 +1838,44 @@ snapshots:
optionalDependencies:
picomatch: 4.0.5
fflate@0.6.11: {}
fflate@0.8.3: {}
fsevents@2.3.3:
optional: true
glsl-noise@0.0.0: {}
graceful-fs@4.2.11: {}
hls.js@1.6.17: {}
hono@4.13.1: {}
ieee754@1.2.1: {}
immediate@3.0.6: {}
import-meta-resolve@4.2.0: {}
is-promise@2.2.2: {}
isexe@2.0.0: {}
its-fine@2.0.0(@types/react@19.2.18)(react@19.2.8):
dependencies:
'@types/react-reconciler': 0.28.9(@types/react@19.2.18)
react: 19.2.8
transitivePeerDependencies:
- '@types/react'
jiti@2.7.0: {}
lie@3.3.0:
dependencies:
immediate: 3.0.6
lightningcss-android-arm64@1.32.0:
optional: true
@@ -1505,16 +1974,39 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0
maath@0.10.8(@types/three@0.185.4)(three@0.185.1):
dependencies:
'@types/three': 0.185.4
three: 0.185.1
maath@0.6.0(@types/three@0.185.4)(three@0.185.1):
dependencies:
'@types/three': 0.185.4
three: 0.185.1
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
meshline@3.3.1(three@0.185.1):
dependencies:
three: 0.185.1
meshoptimizer@1.1.1: {}
n8ao@1.10.3(postprocessing@6.39.4(three@0.185.1))(three@0.185.1):
dependencies:
postprocessing: 6.39.4(three@0.185.1)
three: 0.185.1
nanoid@3.3.18: {}
obug@2.1.4: {}
package-manager-detector@1.8.0: {}
path-key@3.1.1: {}
pathe@2.0.3: {}
picocolors@1.1.1: {}
@@ -1527,6 +2019,17 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postprocessing@6.39.4(three@0.185.1):
dependencies:
three: 0.185.1
potpack@1.0.2: {}
promise-worker-transferable@1.0.4:
dependencies:
is-promise: 2.2.2
lie: 3.3.0
react-dom@19.2.8(react@19.2.8):
dependencies:
react: 19.2.8
@@ -1546,8 +2049,16 @@ snapshots:
optionalDependencies:
react-dom: 19.2.8(react@19.2.8)
react-use-measure@2.1.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
react: 19.2.8
optionalDependencies:
react-dom: 19.2.8(react@19.2.8)
react@19.2.8: {}
require-from-string@2.0.2: {}
rolldown@1.2.3:
dependencies:
'@oxc-project/types': 0.143.0
@@ -1572,18 +2083,51 @@ snapshots:
set-cookie-parser@2.7.2: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
siginfo@2.0.0: {}
source-map-js@1.2.1: {}
stackback@0.0.2: {}
stats-gl@2.4.2(@types/three@0.185.4)(three@0.185.1):
dependencies:
'@types/three': 0.185.4
three: 0.185.1
stats.js@0.17.0: {}
std-env@4.2.0: {}
suspend-react@0.1.3(react@19.2.8):
dependencies:
react: 19.2.8
tailwindcss@4.3.3: {}
tapable@2.3.3: {}
three-mesh-bvh@0.8.3(three@0.185.1):
dependencies:
three: 0.185.1
three-stdlib@2.36.1(three@0.185.1):
dependencies:
'@types/draco3d': 1.4.10
'@types/offscreencanvas': 2019.7.3
'@types/webxr': 0.5.24
draco3d: 1.5.7
fflate: 0.6.11
potpack: 1.0.2
three: 0.185.1
three@0.185.1: {}
tinybench@2.9.0: {}
tinyexec@1.3.0: {}
@@ -1595,16 +2139,44 @@ snapshots:
tinyrainbow@3.1.1: {}
troika-three-text@0.52.5(three@0.185.1):
dependencies:
bidi-js: 1.0.3
three: 0.185.1
troika-three-utils: 0.52.5(three@0.185.1)
troika-worker-utils: 0.52.0
webgl-sdf-generator: 1.1.1
troika-three-utils@0.52.5(three@0.185.1):
dependencies:
three: 0.185.1
troika-worker-utils@0.52.0: {}
tsx@4.23.11:
dependencies:
esbuild: 0.28.1
optionalDependencies:
fsevents: 2.3.3
tunnel-rat@0.1.2(@types/react@19.2.18)(react@19.2.8):
dependencies:
zustand: 4.5.7(@types/react@19.2.18)(react@19.2.8)
transitivePeerDependencies:
- '@types/react'
- immer
- react
typescript@5.9.3: {}
undici-types@6.21.0: {}
use-sync-external-store@1.6.0(react@19.2.8):
dependencies:
react: 19.2.8
utility-types@3.11.0: {}
vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11):
dependencies:
lightningcss: 1.33.0
@@ -1646,6 +2218,14 @@ snapshots:
transitivePeerDependencies:
- msw
webgl-constants@1.1.1: {}
webgl-sdf-generator@1.1.1: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
@@ -1653,7 +2233,15 @@ snapshots:
zod@3.25.76: {}
zustand@5.0.14(@types/react@19.2.18)(react@19.2.8):
zustand@4.5.7(@types/react@19.2.18)(react@19.2.8):
dependencies:
use-sync-external-store: 1.6.0(react@19.2.8)
optionalDependencies:
'@types/react': 19.2.18
react: 19.2.8
zustand@5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)):
optionalDependencies:
'@types/react': 19.2.18
react: 19.2.8
use-sync-external-store: 1.6.0(react@19.2.8)