diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efd81c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +dist-demo/ +*.tsbuildinfo +.DS_Store diff --git a/packages/animated-shorts/.gitignore b/packages/animated-shorts/.gitignore new file mode 100644 index 0000000..3630e29 --- /dev/null +++ b/packages/animated-shorts/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +var +*.tsbuildinfo diff --git a/packages/animated-shorts/DESIGN.md b/packages/animated-shorts/DESIGN.md new file mode 100644 index 0000000..28de920 --- /dev/null +++ b/packages/animated-shorts/DESIGN.md @@ -0,0 +1,28 @@ +# DESIGN — @shoploop/animated-shorts + +- **Deterministic > real-time.** Every visual — camera transform, variant selection, material + finish, overlay opacity — is a pure function of `(storyboard, frame)`, evaluated against + Remotion's frame counter. No `requestAnimationFrame`, no wall-clock, no drift. The same + storyboard renders the same bytes every time, which is what makes the bandit's scores mean + something. + +- **Storyboard as code.** A short is a tiny typed DSL: a product twin plus an ordered list of + keyframe beats, validated against `storyboard.v1.json`. Hand-authored or generated, diffable, + reviewable. If it starts to look like After Effects, it's wrong. + +- **Kinfolk gating at compose time.** `brand_rules.json` is enforced before the render, not + after. The orchestrator composes the scene for every variant the storyboard visits and scores + it; a storyboard that would emit an off-brand frame is refused with `BrandRulesViolation` and + its reasons. The moat is brand-constraint enforcement, not pixels. + +- **Bandit-ready provenance.** Each render emits a `creative_pack.v1` (identical to the still + pipeline) plus a `RenderProvenance` record onto the same `BANDIT_QUEUE_PATH` the Google Ads + connector writes performance to. Thompson Sampling joins the two streams by + `(twin_handle × preset)` — shorts and stills are scored as the same kind of arm. + +- **Two-mode render, one output.** `local` uses `@remotion/renderer` for $0 dev renders; + `open-imagine` dispatches to the GPU router (`POST /v1/render` → poll `GET /v1/jobs/:id`). + Both paths return the same Creative Pack, so callers — and the bandit — never branch on where + a short was rendered. The Three.js scene comes from the SAME parser + scene builder as + `@shoploop/twin-renderer` (re-exported, not duplicated), so offline shorts and the live web + twin stay pixel-for-pixel in agreement. diff --git a/packages/animated-shorts/README.md b/packages/animated-shorts/README.md new file mode 100644 index 0000000..c0d058f --- /dev/null +++ b/packages/animated-shorts/README.md @@ -0,0 +1,110 @@ +# @shoploop/animated-shorts + +Deterministic vertical ad shorts (9:16, **1080×1920**, 6s/9s/15s) for Varitea, built from +the SAME USD product twins the live web renderer uses. Three.js scenes are embedded inside +[Remotion](https://www.remotion.dev/) compositions and driven entirely off Remotion's frame +counter — **frame-accurate, no real-time animation, no drift.** + +``` +USD twin (.usda) → composed scene → Three.js animated capture (Remotion) → encoded MP4 → Creative Pack +``` + +## Why this exists + +A short is just another creative the bandit can score. It emits the identical +`creative_pack.v1` shape a still does, so the Thompson Sampling loop joins shorts and stills +by `(twin_handle × preset)` without caring which pipeline produced them. + +## Quickstart — 6s Varitea hero short, locally + +From this package directory: + +```bash +pnpm install +pnpm cli render --storyboard storyboards/varitea_jasmine_hero_6s.json +# → var/shorts/varitea_jasmine_hero_6s.mp4 (1080×1920, 6s, H.264 CRF 18) +``` + +Or via the installed bin (after `pnpm build`): + +```bash +shoploop-shorts render --storyboard storyboards/varitea_jasmine_hero_6s.json --out hero.mp4 +``` + +First run downloads a headless Chromium shell for Remotion (~90 MB), then renders at $0 on +your machine. + +## Render the 15s flavors short via open-imagine + +The render can run locally (default) or be dispatched to the offline GPU router. Same +storyboard, same Creative Pack out: + +```bash +export SHOPLOOP_ROUTER_URL=http://localhost:8080 +export SHOPLOOP_ROUTER_TOKEN=... # optional bearer +shoploop-shorts render \ + --storyboard storyboards/varitea_flavors_lifestyle_15s.json \ + --mode open-imagine +``` + +`open-imagine` mode `POST`s `{ stage_uri, storyboard_uri, … }` to `/v1/render`, polls +`GET /v1/jobs/:id` until done, and returns the Creative Pack the router produced. In this +PR the router's `blender_cpu` stub keeps that path at $0. + +## The five primitives + +| # | Primitive | File | +|---|---|---| +| 1 | Storyboard DSL (+ `storyboard.v1.json`) | `src/dsl/storyboard.ts` | +| 2 | Remotion composer | `src/composer/Short.tsx` | +| 3 | Three.js bridge | `src/three/ThreeCanvas.tsx` | +| 4 | Render orchestrator (local + router) | `src/render/orchestrator.ts` | +| 5 | Bandit feedback bridge | `src/feedback/observation.ts` | + +## Storyboard DSL + +A storyboard is a product twin plus an ordered list of **beats** (keyframes). If it starts to +look like After Effects, it's wrong. + +```jsonc +{ + "id": "varitea_jasmine_hero_6s", + "duration_s": 6, + "preset": "hero", + "product_twin": "storyboards/assets/varitea_jasmine_pearl.usda", + "brand_rules": "storyboards/assets/brand_rules.json", + "beats": [ + { "t_s": 0, "kind": "camera", "preset": "orbit", "ease": "ease-in-out" }, + { "t_s": 2, "kind": "overlay", "text": "Jasmine Pearl", "placement": "lower-third", "duration_s": 3 } + ] +} +``` + +Beat kinds: `camera` (`orbit` | `dolly` | `macro` | `still`), `variant` (timeline keyframe, +never a runtime UI), `material` (`matte` | `satin` | `gloss`), `overlay`. Validated against +`schema/storyboard.v1.json`. + +Checked-in Varitea storyboards: +- `storyboards/varitea_jasmine_hero_6s.json` — 6s hero, orbit camera, single lower-third overlay. +- `storyboards/varitea_flavors_lifestyle_15s.json` — 15s lifestyle, dolly-in, flavor flip at 5s & 10s, centered end card. + +## Kinfolk gate + +`brand_rules.json` is applied at **scene-compose time**. Before a single frame renders, the +orchestrator composes the scene for every variant the storyboard visits and scores it +(palette, finish, camera). If a frame would fail the Kinfolk test, the render is **refused** +with a `BrandRulesViolation` listing the reasons — never silently shipped. + +## Env + +``` +SHOPLOOP_ROUTER_URL=http://localhost:8080 +SHOPLOOP_ROUTER_TOKEN= +BANDIT_QUEUE_PATH=./var/bandit/queue.jsonl +``` + +## Test + +```bash +pnpm test # dsl schema · composer 1-frame · local MP4 render · router mock · Kinfolk refusal +``` diff --git a/packages/animated-shorts/bin/cli.js b/packages/animated-shorts/bin/cli.js new file mode 100644 index 0000000..8fb1272 --- /dev/null +++ b/packages/animated-shorts/bin/cli.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import "../dist/cli.js"; diff --git a/packages/animated-shorts/contracts/creative_pack.v1.json b/packages/animated-shorts/contracts/creative_pack.v1.json new file mode 100644 index 0000000..5ce58f8 --- /dev/null +++ b/packages/animated-shorts/contracts/creative_pack.v1.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "creative_pack.v1", + "title": "CreativePack", + "description": "A renderable creative output (still OR short) plus the brand/provenance metadata the Thompson Sampling loop needs to score it. Shorts emit the SAME shape as stills so the bandit treats them identically. EVENTUAL HOME: open-imagine/contracts/creative_pack.v1.json — vendored here until promoted.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "id", "twin_handle", "preset", "format", "assets", "brand_score", "created_at"], + "properties": { + "schema": { "const": "creative_pack.v1" }, + "id": { "type": "string", "minLength": 1 }, + "twin_handle": { + "type": "string", + "description": "Stable product-twin handle (commerce:handle). Bandit arm join key, paired with preset." + }, + "preset": { "type": "string", "enum": ["hero", "lifestyle", "pdp", "ugc"] }, + "format": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "width", "height"], + "properties": { + "kind": { "type": "string", "enum": ["still", "short"] }, + "width": { "type": "integer" }, + "height": { "type": "integer" }, + "duration_s": { "type": "number" }, + "fps": { "type": "integer" } + } + }, + "assets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["role", "uri", "mime"], + "properties": { + "role": { "type": "string", "enum": ["primary", "thumbnail", "preview"] }, + "uri": { "type": "string" }, + "mime": { "type": "string" }, + "bytes": { "type": "integer", "minimum": 0 } + } + } + }, + "brand_score": { "type": "number", "minimum": 0, "maximum": 1 }, + "storyboard_id": { "type": "string" }, + "render_mode": { "type": "string", "enum": ["local", "open-imagine"] }, + "created_at": { "type": "string", "format": "date-time" } + } +} diff --git a/packages/animated-shorts/package.json b/packages/animated-shorts/package.json new file mode 100644 index 0000000..ac4670f --- /dev/null +++ b/packages/animated-shorts/package.json @@ -0,0 +1,57 @@ +{ + "name": "@shoploop/animated-shorts", + "version": "0.1.0", + "description": "Deterministic vertical ad shorts (9:16, 1080×1920) for Varitea. Three.js product twins driven by Remotion's frame counter, encoded to MP4 locally or dispatched to open-imagine. Storyboard-as-code, Kinfolk-gated, bandit-ready.", + "license": "UNLICENSED", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "bin": { + "shoploop-shorts": "./bin/cli.js" + }, + "files": [ + "dist", + "bin", + "schema", + "storyboards" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "cli": "tsx src/cli.ts", + "render": "tsx src/cli.ts render" + }, + "dependencies": { + "@remotion/bundler": "^4.0.482", + "@remotion/renderer": "^4.0.482", + "@remotion/three": "^4.0.482", + "@shoploop/twin-renderer": "workspace:*", + "ajv": "^8.17.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "remotion": "^4.0.482", + "three": "^0.169.0" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/three": "^0.169.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/animated-shorts/schema/storyboard.v1.json b/packages/animated-shorts/schema/storyboard.v1.json new file mode 100644 index 0000000..1e82396 --- /dev/null +++ b/packages/animated-shorts/schema/storyboard.v1.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "storyboard.v1", + "title": "Storyboard", + "description": "A deterministic vertical-short timeline: a product twin plus ordered keyframe beats. Consumed by the Remotion composer, the Three.js bridge, the Kinfolk gate, and the render orchestrator.", + "type": "object", + "additionalProperties": false, + "required": ["id", "duration_s", "beats", "product_twin", "brand_rules", "preset"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "duration_s": { "type": "integer", "enum": [6, 9, 15] }, + "preset": { "type": "string", "enum": ["hero", "lifestyle", "pdp", "ugc"] }, + "product_twin": { + "type": "string", + "minLength": 1, + "description": "Path or s3:// uri to the product_twin .usda." + }, + "brand_rules": { + "type": "string", + "minLength": 1, + "description": "Path to brand_rules.json applied at scene-compose time." + }, + "audio": { + "type": "object", + "additionalProperties": false, + "required": ["uri"], + "properties": { + "uri": { "type": "string", "minLength": 1 }, + "gain_db": { "type": "number" } + } + }, + "captions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["t_s", "text", "duration_s"], + "properties": { + "t_s": { "type": "number", "minimum": 0 }, + "text": { "type": "string" }, + "duration_s": { "type": "number", "exclusiveMinimum": 0 }, + "placement": { "type": "string", "enum": ["lower-third", "corner", "centered"] } + } + } + }, + "beats": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["t_s", "kind"], + "properties": { + "t_s": { "type": "number", "minimum": 0 }, + "kind": { "type": "string", "enum": ["camera", "variant", "material", "overlay"] } + }, + "allOf": [ + { + "if": { "properties": { "kind": { "const": "camera" } } }, + "then": { + "additionalProperties": false, + "required": ["t_s", "kind", "preset"], + "properties": { + "t_s": { "type": "number", "minimum": 0 }, + "kind": { "const": "camera" }, + "preset": { "type": "string", "enum": ["orbit", "dolly", "macro", "still"] }, + "ease": { "type": "string", "enum": ["linear", "ease-in-out"] } + } + } + }, + { + "if": { "properties": { "kind": { "const": "variant" } } }, + "then": { + "additionalProperties": false, + "required": ["t_s", "kind", "set", "option"], + "properties": { + "t_s": { "type": "number", "minimum": 0 }, + "kind": { "const": "variant" }, + "set": { "type": "string", "minLength": 1 }, + "option": { "type": "string", "minLength": 1 } + } + } + }, + { + "if": { "properties": { "kind": { "const": "material" } } }, + "then": { + "additionalProperties": false, + "required": ["t_s", "kind", "mesh", "finish"], + "properties": { + "t_s": { "type": "number", "minimum": 0 }, + "kind": { "const": "material" }, + "mesh": { "type": "string", "minLength": 1 }, + "finish": { "type": "string", "enum": ["matte", "satin", "gloss"] } + } + } + }, + { + "if": { "properties": { "kind": { "const": "overlay" } } }, + "then": { + "additionalProperties": false, + "required": ["t_s", "kind", "text", "placement", "duration_s"], + "properties": { + "t_s": { "type": "number", "minimum": 0 }, + "kind": { "const": "overlay" }, + "text": { "type": "string" }, + "placement": { "type": "string", "enum": ["lower-third", "corner", "centered"] }, + "duration_s": { "type": "number", "exclusiveMinimum": 0 } + } + } + } + ] + } + } + } +} diff --git a/packages/animated-shorts/src/brand/kinfolk.ts b/packages/animated-shorts/src/brand/kinfolk.ts new file mode 100644 index 0000000..deadfda --- /dev/null +++ b/packages/animated-shorts/src/brand/kinfolk.ts @@ -0,0 +1,186 @@ +import { readFile } from "node:fs/promises"; +import * as THREE from "three"; +import { parser, scene as sceneBuilder } from "@shoploop/twin-renderer"; +import type { Storyboard } from "../dsl/storyboard.js"; +import { beatsOfKind } from "../dsl/storyboard.js"; + +// The Kinfolk test, as code. soul.md: "if a frame wouldn't fit a Kinfolk spread, it's +// a regression." We enforce it at scene-compose time — BEFORE a single frame is rendered — +// by composing the same Three.js scene the renderer will use, for every variant the +// storyboard visits, and scoring it against brand_rules.json. A render that would emit an +// off-brand frame is refused, not shipped. + +export interface BrandRules { + /** Editorial palette: hex colors the composed materials must stay near. */ + palette?: string[]; + /** Max allowed distance (0..1, RGB) from the nearest palette color before a material counts as off-palette. */ + palette_tolerance?: number; + /** Material finishes that are off-brand (Kinfolk skews matte/satin; high gloss reads as ad-y). */ + forbidden_finishes?: ("matte" | "satin" | "gloss")[]; + /** Camera moves allowed for this brand (e.g. ban frenetic moves). */ + allowed_camera?: ("orbit" | "dolly" | "macro" | "still")[]; + /** Minimum composite brand score (0..1) to pass the gate. */ + min_score?: number; +} + +export interface BrandViolation { + rule: string; + detail: string; + /** The variant selection under which this violation surfaced, if scene-derived. */ + at?: string; +} + +export interface BrandReport { + passed: boolean; + /** 0..1 composite. The bandit stores this next to performance so shorts and stills score alike. */ + score: number; + violations: BrandViolation[]; +} + +export class BrandRulesViolation extends Error { + constructor( + message: string, + readonly report: BrandReport, + ) { + super(message); + this.name = "BrandRulesViolation"; + } + get reasons(): string[] { + return this.report.violations.map((v) => `${v.rule}: ${v.detail}${v.at ? ` (${v.at})` : ""}`); + } +} + +export async function loadBrandRules(path: string): Promise { + return JSON.parse(await readFile(path, "utf8")) as BrandRules; +} + +function hexToRgb(hex: string): [number, number, number] { + const c = new THREE.Color(hex); + return [c.r, c.g, c.b]; +} + +function rgbDistance(a: [number, number, number], b: [number, number, number]): number { + // Euclidean in linear RGB, normalized so max distance ≈ 1. + const d = Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); + return d / Math.sqrt(3); +} + +/** Walk a composed group and collect every material base color. */ +function collectColors(group: THREE.Object3D): [number, number, number][] { + const out: [number, number, number][] = []; + group.traverse((obj) => { + const mesh = obj as THREE.Mesh; + if (!mesh.isMesh) return; + const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material]; + for (const m of mats) { + const col = (m as THREE.MeshStandardMaterial).color; + if (col) out.push([col.r, col.g, col.b]); + } + }); + return out; +} + +/** Every variant selection the storyboard visits (default + each variant beat). */ +function visitedSelections(storyboard: Storyboard): Record[] { + const base: Record = {}; + const selections: Record[] = [{ ...base }]; + let current = { ...base }; + for (const beat of beatsOfKind(storyboard, "variant")) { + current = { ...current, [beat.set]: beat.option }; + selections.push({ ...current }); + } + return selections; +} + +/** + * Compose the storyboard's scene for every variant it visits and score it against + * the brand rules. Pure + synchronous-ish (no network): uses the procedural placeholder + * path so the gate is deterministic and runs in CI without asset fetches. + */ +export function evaluateBrand( + storyboard: Storyboard, + stage: parser.ProductTwinStage, + rules: BrandRules, +): BrandReport { + const violations: BrandViolation[] = []; + const palette = (rules.palette ?? []).map(hexToRgb); + const tol = rules.palette_tolerance ?? 0.25; + const minScore = rules.min_score ?? 0.7; + + // 1. Palette adherence across every visited variant. + let colorSamples = 0; + let offPalette = 0; + if (palette.length > 0) { + for (const selection of visitedSelections(storyboard)) { + const built = sceneBuilder.buildScene(stage, { selection }); + for (const color of collectColors(built.group)) { + colorSamples++; + const nearest = Math.min(...palette.map((p) => rgbDistance(color, p))); + if (nearest > tol) { + offPalette++; + violations.push({ + rule: "palette", + detail: `material color #${new THREE.Color(...color).getHexString()} is ${nearest.toFixed(2)} from nearest palette color (tol ${tol})`, + at: JSON.stringify(selection), + }); + } + } + } + } + + // 2. Forbidden material finishes (hard rule). + const forbidden = new Set(rules.forbidden_finishes ?? []); + for (const beat of beatsOfKind(storyboard, "material")) { + if (forbidden.has(beat.finish)) { + violations.push({ + rule: "finish", + detail: `material beat sets ${beat.mesh} to '${beat.finish}', which is off-brand`, + at: `t=${beat.t_s}s`, + }); + } + } + + // 3. Allowed camera moves (hard rule). + if (rules.allowed_camera) { + const allowed = new Set(rules.allowed_camera); + for (const beat of beatsOfKind(storyboard, "camera")) { + if (!allowed.has(beat.preset)) { + violations.push({ + rule: "camera", + detail: `camera move '${beat.preset}' is not in allowed set [${[...allowed].join(", ")}]`, + at: `t=${beat.t_s}s`, + }); + } + } + } + + // Composite score: palette adherence fraction, then hard penalties. + const paletteScore = colorSamples === 0 ? 1 : 1 - offPalette / colorSamples; + const hardPenalty = violations.filter((v) => v.rule !== "palette").length * 0.25; + const score = Math.max(0, Math.min(1, paletteScore - hardPenalty)); + + return { passed: violations.length === 0 && score >= minScore, score, violations }; +} + +/** + * Gate a storyboard. Loads the twin + brand rules, composes, scores. Throws + * BrandRulesViolation (with reasons) if a frame would fail Kinfolk. Returns the + * passing report otherwise so callers can stamp brand_score onto provenance. + */ +export async function gateStoryboard( + storyboard: Storyboard, + usdaSource: string, +): Promise { + const stage = parser.parseUsda(usdaSource); + const rules = await loadBrandRules(storyboard.brand_rules); + const report = evaluateBrand(storyboard, stage, rules); + if (!report.passed) { + throw new BrandRulesViolation( + `Kinfolk gate refused render of '${storyboard.id}' (score ${report.score.toFixed(2)}):\n - ${report.violations + .map((v) => `${v.rule}: ${v.detail}`) + .join("\n - ")}`, + report, + ); + } + return report; +} diff --git a/packages/animated-shorts/src/cli.ts b/packages/animated-shorts/src/cli.ts new file mode 100644 index 0000000..5d6e395 --- /dev/null +++ b/packages/animated-shorts/src/cli.ts @@ -0,0 +1,75 @@ +import { resolve } from "node:path"; +import { loadStoryboard } from "./dsl/validate.js"; +import { renderShort } from "./render/orchestrator.js"; +import { BrandRulesViolation } from "./brand/kinfolk.js"; +import type { RenderMode } from "./render/pack.js"; + +// shoploop-shorts render --storyboard [--mode local|open-imagine] [--out ] + +interface Args { + command: string; + storyboard?: string; + mode?: RenderMode; + out?: string; +} + +function parseArgs(argv: string[]): Args { + const args: Args = { command: argv[0] ?? "" }; + for (let i = 1; i < argv.length; i++) { + const a = argv[i]; + if (a === "--storyboard") args.storyboard = argv[++i]; + else if (a === "--mode") args.mode = argv[++i] as RenderMode; + else if (a === "--out") args.out = argv[++i]; + } + return args; +} + +const USAGE = `shoploop-shorts — Three.js + Remotion animated shorts + +Usage: + shoploop-shorts render --storyboard [--mode local|open-imagine] [--out ] + +Options: + --storyboard Path to a storyboard JSON (validated against storyboard.v1.json). + --mode 'local' (default, Remotion → MP4) or 'open-imagine' (router dispatch). + --out Output MP4 path. Defaults to ./var/shorts/.mp4. +`; + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (args.command !== "render" || !args.storyboard) { + process.stdout.write(USAGE); + process.exit(args.command === "render" ? 1 : 0); + return; + } + + const mode: RenderMode = args.mode ?? "local"; + const storyboardPath = resolve(process.cwd(), args.storyboard); + + try { + const storyboard = await loadStoryboard(storyboardPath); + process.stderr.write(`rendering '${storyboard.id}' (${storyboard.duration_s}s, ${mode})…\n`); + const pack = await renderShort({ + storyboard, + mode, + out: args.out, + storyboardUri: storyboardPath, + }); + const primary = pack.assets.find((a) => a.role === "primary"); + process.stderr.write( + `done: ${primary?.uri} (brand_score ${pack.brand_score.toFixed(2)}, ${primary?.bytes ?? "?"} bytes)\n`, + ); + process.stdout.write(JSON.stringify(pack, null, 2) + "\n"); + } catch (err) { + if (err instanceof BrandRulesViolation) { + process.stderr.write(`BrandRulesViolation: render refused.\n`); + for (const r of err.reasons) process.stderr.write(` - ${r}\n`); + process.exit(2); + } + process.stderr.write(`error: ${(err as Error).message}\n`); + process.exit(1); + } +} + +main(); diff --git a/packages/animated-shorts/src/composer/Overlays.tsx b/packages/animated-shorts/src/composer/Overlays.tsx new file mode 100644 index 0000000..cd98efd --- /dev/null +++ b/packages/animated-shorts/src/composer/Overlays.tsx @@ -0,0 +1,45 @@ +import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion"; +import type { Storyboard } from "../dsl/storyboard.js"; +import { evaluateFrame, type OverlayState } from "./timeline.js"; + +// Captions / overlays live in a separate Remotion layer ON TOP of the Three.js canvas — +// text is never baked into the 3D scene. Each overlay's visibility + fade is frame-derived. + +const PLACEMENT_STYLE: Record = { + "lower-third": { justifyContent: "flex-end", alignItems: "flex-start", padding: "0 72px 220px" }, + corner: { justifyContent: "flex-start", alignItems: "flex-start", padding: "120px 0 0 72px" }, + centered: { justifyContent: "center", alignItems: "center", padding: "0 96px" }, +}; + +const TEXT_STYLE: Record = { + "lower-third": { fontSize: 64, textAlign: "left", maxWidth: 760 }, + corner: { fontSize: 40, textAlign: "left", letterSpacing: 2, textTransform: "uppercase" }, + centered: { fontSize: 84, textAlign: "center", maxWidth: 840, lineHeight: 1.1 }, +}; + +export function Overlays({ storyboard }: { storyboard: Storyboard }) { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const { overlays } = evaluateFrame(storyboard, frame, fps); + + return ( + <> + {overlays.map((o, i) => ( + +
+ {o.text} +
+
+ ))} + + ); +} diff --git a/packages/animated-shorts/src/composer/Root.tsx b/packages/animated-shorts/src/composer/Root.tsx new file mode 100644 index 0000000..aeede06 --- /dev/null +++ b/packages/animated-shorts/src/composer/Root.tsx @@ -0,0 +1,46 @@ +import { Composition } from "remotion"; +import { FPS, SHORT_HEIGHT, SHORT_WIDTH, type Storyboard } from "../dsl/storyboard.js"; +import { Short, SHORT_COMPOSITION_ID, type ShortProps } from "./Short.js"; + +// A minimal valid default so the composition mounts standalone (Remotion Studio / bundle +// probe). The orchestrator always overrides via inputProps for real renders. +const DEFAULT_USDA = `#usda 1.0 +( + defaultPrim = "Product" + upAxis = "Y" +) + +def Xform "Product" (kind = "component") +{ + def Mesh "Placeholder" { custom bool commerce:placeholder = true } +} +`; + +const DEFAULT_STORYBOARD: Storyboard = { + id: "default", + duration_s: 6, + preset: "hero", + product_twin: "(inline)", + brand_rules: "(inline)", + beats: [{ t_s: 0, kind: "camera", preset: "orbit", ease: "ease-in-out" }], +}; + +export function RemotionRoot() { + return ( + ({ + durationInFrames: props.storyboard.duration_s * FPS, + fps: FPS, + width: SHORT_WIDTH, + height: SHORT_HEIGHT, + })} + /> + ); +} diff --git a/packages/animated-shorts/src/composer/Short.tsx b/packages/animated-shorts/src/composer/Short.tsx new file mode 100644 index 0000000..bf790c5 --- /dev/null +++ b/packages/animated-shorts/src/composer/Short.tsx @@ -0,0 +1,34 @@ +import { AbsoluteFill, Audio } from "remotion"; +import { parser } from "@shoploop/twin-renderer"; +import type { Storyboard } from "../dsl/storyboard.js"; +import { ThreeCanvas } from "../three/ThreeCanvas.js"; +import { Overlays } from "./Overlays.js"; + +// The Remotion composition. Props are JSON-serializable (storyboard + raw USDA source) so +// the orchestrator can pass them via inputProps for headless capture. The USDA is parsed +// here with the SAME parser the live twin uses — no duplicate scene logic. +// Type alias (not interface) so it satisfies Remotion's `Record` props bound. +export type ShortProps = { + storyboard: Storyboard; + /** Raw USDA source for the product twin (resolved + read by the orchestrator). */ + usda: string; +}; + +export const SHORT_COMPOSITION_ID = "Short"; + +function dbToVolume(gainDb?: number): number { + return gainDb === undefined ? 1 : Math.pow(10, gainDb / 20); +} + +export function Short({ storyboard, usda }: ShortProps) { + const stage = parser.parseUsda(usda); + return ( + + + + {storyboard.audio ? ( + + ); +} diff --git a/packages/animated-shorts/src/composer/entry.tsx b/packages/animated-shorts/src/composer/entry.tsx new file mode 100644 index 0000000..568e9af --- /dev/null +++ b/packages/animated-shorts/src/composer/entry.tsx @@ -0,0 +1,5 @@ +import { registerRoot } from "remotion"; +import { RemotionRoot } from "./Root.js"; + +// Remotion bundle entry. The orchestrator points @remotion/renderer's bundler at this file. +registerRoot(RemotionRoot); diff --git a/packages/animated-shorts/src/composer/timeline.ts b/packages/animated-shorts/src/composer/timeline.ts new file mode 100644 index 0000000..4254cb6 --- /dev/null +++ b/packages/animated-shorts/src/composer/timeline.ts @@ -0,0 +1,174 @@ +import type { + CameraMove, + MaterialFinish, + Storyboard, +} from "../dsl/storyboard.js"; +import { FPS, orderedBeats } from "../dsl/storyboard.js"; + +// The deterministic core. Given a storyboard and an absolute frame index, compute +// EXACTLY what the scene looks like at that frame — camera transform, active variant +// selection, per-mesh material finish, visible overlays. No requestAnimationFrame, no +// wall-clock, no drift: the same (storyboard, frame) always yields the same state. This +// is what lets the Three.js bridge be frame-accurate inside Remotion. + +export interface CameraState { + position: [number, number, number]; + target: [number, number, number]; + fov: number; +} + +export interface OverlayState { + text: string; + placement: "lower-third" | "corner" | "centered"; + /** 0..1 fade envelope for this frame. */ + opacity: number; +} + +export interface FrameState { + camera: CameraState; + selection: Record; + finishes: Record; + overlays: OverlayState[]; +} + +// Editorial vertical framing. Target sits at the product's mid-height; the product is +// roughly 0.9 tall, centered on origin's base. Tuned for 9:16. +const TARGET: [number, number, number] = [0, 0.45, 0]; +const BASE_RADIUS = 2.4; +const BASE_HEIGHT = 0.85; +const BASE_AZIMUTH = Math.PI * 0.12; + +interface MoveFraming { + radius: (p: number) => number; + azimuth: (p: number, globalT: number) => number; + height: (p: number) => number; + fov: (p: number) => number; +} + +// Each camera move is a pure function of its local progress p∈[0,1] (and global time for +// continuous orbit). Deliberately calm — Kinfolk, not a car commercial. +const MOVES: Record = { + still: { + radius: () => BASE_RADIUS, + azimuth: () => BASE_AZIMUTH, + height: () => BASE_HEIGHT, + fov: () => 34, + }, + orbit: { + radius: () => BASE_RADIUS, + // ~120° glide across the active span; slow and smooth. + azimuth: (p) => BASE_AZIMUTH + p * (Math.PI * 0.66), + height: () => BASE_HEIGHT, + fov: () => 34, + }, + dolly: { + // Push in from wide to intimate. + radius: (p) => lerp(BASE_RADIUS * 1.25, BASE_RADIUS * 0.8, p), + azimuth: () => BASE_AZIMUTH, + height: (p) => lerp(BASE_HEIGHT * 1.1, BASE_HEIGHT * 0.92, p), + fov: (p) => lerp(38, 30, p), + }, + macro: { + radius: (p) => lerp(BASE_RADIUS * 0.7, BASE_RADIUS * 0.5, p), + azimuth: (p) => BASE_AZIMUTH + p * (Math.PI * 0.1), + height: () => BASE_HEIGHT * 0.78, + fov: () => 22, + }, +}; + +function lerp(a: number, b: number, t: number): number { + return a + (b - a) * t; +} + +function easeInOut(t: number): number { + return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; +} + +function clamp01(t: number): number { + return t < 0 ? 0 : t > 1 ? 1 : t; +} + +function cameraAt(storyboard: Storyboard, t: number): CameraState { + const beats = orderedBeats(storyboard).filter((b) => b.kind === "camera"); + if (beats.length === 0) { + return framingToState(MOVES.still, 0, t); + } + // Active beat = last camera beat at or before t. + let idx = 0; + for (let i = 0; i < beats.length; i++) { + if ((beats[i] as { t_s: number }).t_s <= t) idx = i; + } + const active = beats[idx] as { t_s: number; preset: CameraMove; ease?: string }; + const next = beats[idx + 1] as { t_s: number } | undefined; + const start = active.t_s; + const end = next ? next.t_s : storyboard.duration_s; + const span = Math.max(end - start, 1e-6); + let p = clamp01((t - start) / span); + if (active.ease === "ease-in-out") p = easeInOut(p); + return framingToState(MOVES[active.preset], p, t); +} + +function framingToState(m: MoveFraming, p: number, globalT: number): CameraState { + const r = m.radius(p); + const az = m.azimuth(p, globalT); + const h = m.height(p); + const x = Math.sin(az) * r; + const z = Math.cos(az) * r; + return { position: [x, h, z], target: TARGET, fov: m.fov(p) }; +} + +function selectionAt(storyboard: Storyboard, t: number): Record { + const selection: Record = {}; + for (const beat of orderedBeats(storyboard)) { + if (beat.kind === "variant" && beat.t_s <= t) selection[beat.set] = beat.option; + } + return selection; +} + +function finishesAt(storyboard: Storyboard, t: number): Record { + const finishes: Record = {}; + for (const beat of orderedBeats(storyboard)) { + if (beat.kind === "material" && beat.t_s <= t) finishes[beat.mesh] = beat.finish; + } + return finishes; +} + +const FADE_S = 0.4; + +function overlaysAt(storyboard: Storyboard, t: number): OverlayState[] { + const out: OverlayState[] = []; + const sources: { t_s: number; text: string; placement: OverlayState["placement"]; duration_s: number }[] = []; + for (const beat of orderedBeats(storyboard)) { + if (beat.kind === "overlay") { + sources.push({ t_s: beat.t_s, text: beat.text, placement: beat.placement, duration_s: beat.duration_s }); + } + } + for (const c of storyboard.captions ?? []) { + sources.push({ t_s: c.t_s, text: c.text, placement: c.placement ?? "lower-third", duration_s: c.duration_s }); + } + for (const s of sources) { + const local = t - s.t_s; + if (local < 0 || local > s.duration_s) continue; + const fadeIn = clamp01(local / FADE_S); + const fadeOut = clamp01((s.duration_s - local) / FADE_S); + out.push({ text: s.text, placement: s.placement, opacity: Math.min(fadeIn, fadeOut) }); + } + return out; +} + +/** Full deterministic state at an absolute frame index. */ +export function evaluateFrame(storyboard: Storyboard, frame: number, fps = FPS): FrameState { + const t = frame / fps; + return { + camera: cameraAt(storyboard, t), + selection: selectionAt(storyboard, t), + finishes: finishesAt(storyboard, t), + overlays: overlaysAt(storyboard, t), + }; +} + +export const ROUGHNESS_BY_FINISH: Record = { + matte: 0.85, + satin: 0.5, + gloss: 0.12, +}; diff --git a/packages/animated-shorts/src/config.ts b/packages/animated-shorts/src/config.ts new file mode 100644 index 0000000..355c654 --- /dev/null +++ b/packages/animated-shorts/src/config.ts @@ -0,0 +1,21 @@ +// Centralized env. Lazy so importing the package never throws when env is absent. + +export interface ShortsConfig { + routerUrl: string; + routerToken: string; + banditQueuePath: string; +} + +function optional(key: string, fallback: string): string { + const v = process.env[key]; + return v === undefined || v === "" ? fallback : v; +} + +export function loadConfig(overrides: Partial = {}): ShortsConfig { + return { + routerUrl: overrides.routerUrl ?? optional("SHOPLOOP_ROUTER_URL", "http://localhost:8080"), + routerToken: overrides.routerToken ?? optional("SHOPLOOP_ROUTER_TOKEN", ""), + banditQueuePath: + overrides.banditQueuePath ?? optional("BANDIT_QUEUE_PATH", "./var/bandit/queue.jsonl"), + }; +} diff --git a/packages/animated-shorts/src/dsl/storyboard.ts b/packages/animated-shorts/src/dsl/storyboard.ts new file mode 100644 index 0000000..7c58533 --- /dev/null +++ b/packages/animated-shorts/src/dsl/storyboard.ts @@ -0,0 +1,85 @@ +// Storyboard DSL — a tiny, obvious, typed timeline. If this ever starts to look +// like After Effects, it's wrong. A short is a product twin + an ordered list of +// beats (keyframes). Everything downstream (Remotion composer, Three.js bridge, +// brand gate, render orchestrator) reads this single shape. + +export type Duration = 6 | 9 | 15; +export type Preset = "hero" | "lifestyle" | "pdp" | "ugc"; + +export type CameraMove = "orbit" | "dolly" | "macro" | "still"; +export type Ease = "linear" | "ease-in-out"; +export type MaterialFinish = "matte" | "satin" | "gloss"; +export type OverlayPlacement = "lower-third" | "corner" | "centered"; + +export interface CameraBeat { + t_s: number; + kind: "camera"; + preset: CameraMove; + ease?: Ease; +} + +export interface VariantBeat { + t_s: number; + kind: "variant"; + set: string; + option: string; +} + +export interface MaterialBeat { + t_s: number; + kind: "material"; + mesh: string; + finish: MaterialFinish; +} + +export interface OverlayBeat { + t_s: number; + kind: "overlay"; + text: string; + placement: OverlayPlacement; + duration_s: number; +} + +export type Beat = CameraBeat | VariantBeat | MaterialBeat | OverlayBeat; + +export interface Caption { + t_s: number; + text: string; + duration_s: number; + placement?: OverlayPlacement; +} + +export interface Storyboard { + id: string; + duration_s: Duration; + beats: Beat[]; + /** Path or s3:// uri to the product_twin .usda. */ + product_twin: string; + /** Path to brand_rules.json applied at scene-compose time (Kinfolk gate). */ + brand_rules: string; + preset: Preset; + audio?: { uri: string; gain_db?: number }; + captions?: Caption[]; +} + +/** Vertical ad short: 9:16, 1080×1920. Locked — these never vary per the brief. */ +export const SHORT_WIDTH = 1080; +export const SHORT_HEIGHT = 1920; +/** Frame-accurate, deterministic. 30fps is the contract with the bandit/still pipeline. */ +export const FPS = 30; + +export function durationInFrames(s: Storyboard): number { + return s.duration_s * FPS; +} + +/** Beats sorted by time — the composer interpolates against this ordering. */ +export function orderedBeats(s: Storyboard): Beat[] { + return [...s.beats].sort((a, b) => a.t_s - b.t_s); +} + +export function beatsOfKind( + s: Storyboard, + kind: K, +): Extract[] { + return orderedBeats(s).filter((b): b is Extract => b.kind === kind); +} diff --git a/packages/animated-shorts/src/dsl/validate.ts b/packages/animated-shorts/src/dsl/validate.ts new file mode 100644 index 0000000..9fc60dc --- /dev/null +++ b/packages/animated-shorts/src/dsl/validate.ts @@ -0,0 +1,73 @@ +import { readFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv, { type ErrorObject } from "ajv"; +import type { Storyboard } from "./storyboard.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +// schema/ ships alongside the package; resolve from src/dsl → ../../schema. +const SCHEMA_PATH = resolve(here, "../../schema/storyboard.v1.json"); + +export class StoryboardValidationError extends Error { + constructor( + message: string, + readonly errors: string[], + ) { + super(message); + this.name = "StoryboardValidationError"; + } +} + +let validateFn: ((data: unknown) => boolean) & { errors?: ErrorObject[] | null }; + +async function getValidator() { + if (!validateFn) { + const schema = JSON.parse(await readFile(SCHEMA_PATH, "utf8")); + const ajv = new Ajv({ allErrors: true, strict: false }); + validateFn = ajv.compile(schema); + } + return validateFn; +} + +function formatErrors(errors: ErrorObject[] | null | undefined): string[] { + if (!errors) return []; + return errors.map((e) => `${e.instancePath || "(root)"} ${e.message ?? "invalid"}`.trim()); +} + +/** Validate an in-memory object against storyboard.v1.json. Throws on failure. */ +export async function validateStoryboard(data: unknown): Promise { + const validate = await getValidator(); + if (!validate(data)) { + const errs = formatErrors(validate.errors); + throw new StoryboardValidationError( + `storyboard failed schema validation:\n - ${errs.join("\n - ")}`, + errs, + ); + } + // Cross-field invariant the JSON schema can't express: every beat fits the duration. + const sb = data as Storyboard; + const overruns = sb.beats + .filter((b) => b.t_s > sb.duration_s) + .map((b) => `${b.kind}@${b.t_s}s exceeds duration ${sb.duration_s}s`); + if (overruns.length > 0) { + throw new StoryboardValidationError( + `storyboard beats fall outside duration:\n - ${overruns.join("\n - ")}`, + overruns, + ); + } + return sb; +} + +/** Read + parse + validate a storyboard JSON file. */ +export async function loadStoryboard(path: string): Promise { + const raw = await readFile(path, "utf8"); + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (e) { + throw new StoryboardValidationError(`storyboard is not valid JSON: ${(e as Error).message}`, [ + (e as Error).message, + ]); + } + return validateStoryboard(parsed); +} diff --git a/packages/animated-shorts/src/feedback/observation.ts b/packages/animated-shorts/src/feedback/observation.ts new file mode 100644 index 0000000..280989c --- /dev/null +++ b/packages/animated-shorts/src/feedback/observation.ts @@ -0,0 +1,53 @@ +import { mkdir, appendFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import { loadConfig } from "../config.js"; +import type { Preset, Duration } from "../dsl/storyboard.js"; +import type { CreativePack, RenderMode } from "../render/pack.js"; + +// Bandit feedback bridge — the producer side of the loop, for the RENDER half. The Google +// Ads connector writes PerformanceObservation (reward) to the same BANDIT_QUEUE_PATH; we +// write RenderProvenance (the arm that produced a creative). The Thompson Sampling loop +// joins the two streams by (twin_handle × preset): provenance says which arm made the +// short, performance says how that arm did. Same single-writer JSONL queue, decoupled +// consumer — nothing here knows about the sampler. + +export interface RenderProvenance { + schema: "render_provenance.v1"; + storyboard_id: string; + twin_handle: string; + preset: Preset; + duration_s: Duration; + brand_score: number; + output_uri: string; + render_mode: RenderMode; + observed_at: string; +} + +export interface EmitOptions { + queuePath?: string; +} + +export function provenanceFromPack(pack: CreativePack): RenderProvenance { + return { + schema: "render_provenance.v1", + storyboard_id: pack.storyboard_id, + twin_handle: pack.twin_handle, + preset: pack.preset, + duration_s: (pack.format.duration_s ?? 0) as Duration, + brand_score: pack.brand_score, + output_uri: pack.assets.find((a) => a.role === "primary")?.uri ?? "", + render_mode: pack.render_mode, + observed_at: new Date().toISOString(), + }; +} + +/** Append a RenderProvenance record to the durable bandit queue. */ +export async function emitRenderProvenance( + record: RenderProvenance, + opts: EmitOptions = {}, +): Promise { + const queuePath = opts.queuePath ?? loadConfig().banditQueuePath; + await mkdir(dirname(queuePath), { recursive: true }); + await appendFile(queuePath, JSON.stringify(record) + "\n", "utf8"); + return record; +} diff --git a/packages/animated-shorts/src/index.ts b/packages/animated-shorts/src/index.ts new file mode 100644 index 0000000..0ec7073 --- /dev/null +++ b/packages/animated-shorts/src/index.ts @@ -0,0 +1,37 @@ +// @shoploop/animated-shorts — deterministic vertical ad shorts for Varitea. +// USD twin → composed scene → Three.js animated capture (Remotion) → MP4 → Creative Pack. + +export * from "./dsl/storyboard.js"; +export { validateStoryboard, loadStoryboard, StoryboardValidationError } from "./dsl/validate.js"; + +export { + evaluateBrand, + gateStoryboard, + loadBrandRules, + BrandRulesViolation, +} from "./brand/kinfolk.js"; +export type { BrandRules, BrandReport, BrandViolation } from "./brand/kinfolk.js"; + +export { evaluateFrame, ROUGHNESS_BY_FINISH } from "./composer/timeline.js"; +export type { FrameState, CameraState, OverlayState } from "./composer/timeline.js"; + +export { Short, SHORT_COMPOSITION_ID } from "./composer/Short.js"; +export type { ShortProps } from "./composer/Short.js"; +export { RemotionRoot } from "./composer/Root.js"; + +export { ThreeCanvas } from "./three/ThreeCanvas.js"; +export type { ThreeCanvasHandle, ThreeCanvasProps } from "./three/ThreeCanvas.js"; + +export { renderShort } from "./render/orchestrator.js"; +export type { RenderOptions } from "./render/orchestrator.js"; +export type { CreativePack, CreativeAsset, RenderMode } from "./render/pack.js"; +export { twinHandle, readTwinSource, isRemoteUri } from "./render/twin.js"; + +export { + emitRenderProvenance, + provenanceFromPack, +} from "./feedback/observation.js"; +export type { RenderProvenance } from "./feedback/observation.js"; + +export { loadConfig } from "./config.js"; +export type { ShortsConfig } from "./config.js"; diff --git a/packages/animated-shorts/src/render/orchestrator.ts b/packages/animated-shorts/src/render/orchestrator.ts new file mode 100644 index 0000000..2ec2859 --- /dev/null +++ b/packages/animated-shorts/src/render/orchestrator.ts @@ -0,0 +1,267 @@ +import { mkdir, stat } from "node:fs/promises"; +import { dirname, resolve, isAbsolute } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + FPS, + SHORT_HEIGHT, + SHORT_WIDTH, + type Storyboard, +} from "../dsl/storyboard.js"; +import { gateStoryboard, type BrandReport } from "../brand/kinfolk.js"; +import { loadConfig, type ShortsConfig } from "../config.js"; +import { emitRenderProvenance, provenanceFromPack } from "../feedback/observation.js"; +import { isRemoteUri, readTwinSource, twinHandle } from "./twin.js"; +import type { CreativePack, RenderMode } from "./pack.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +// Remotion bundles from source (its own webpack), so always point at the .tsx entry in src/, +// whether this module runs from src/ (tsx) or dist/ (compiled). ../../ is the package root +// from either src/render or dist/render. +const ENTRY = resolve(here, "../../src/composer/entry.tsx"); + +export interface RenderOptions { + storyboard: Storyboard; + mode?: RenderMode; + /** Output MP4 path (local) or desired key (router). */ + out?: string; + /** URI to the storyboard JSON, required for open-imagine dispatch. */ + storyboardUri?: string; + config?: Partial; + /** Injectable fetch (router mode tests). Defaults to global fetch. */ + fetchImpl?: typeof fetch; + /** Override the bandit queue path for provenance emission. */ + queuePath?: string; + /** Skip appending RenderProvenance (used by tests that assert render only). */ + skipProvenance?: boolean; + /** Pixel scale for the local encode (1 = full 1080×1920). Lowered in tests for speed. */ + renderScale?: number; +} + +function defaultOut(storyboard: Storyboard): string { + return resolve(process.cwd(), "var/shorts", `${storyboard.id}.mp4`); +} + +async function fileBytes(path: string): Promise { + return (await stat(path)).size; +} + +// ---- Local render: Remotion headless capture → MP4 --------------------------- + +async function renderLocal( + storyboard: Storyboard, + out: string, + opts: RenderOptions, +): Promise { + const usda = await readTwinSource(storyboard.product_twin); + + // Kinfolk gate BEFORE rendering — refuse off-brand frames, don't waste a render. + const report: BrandReport = await gateStoryboard(storyboard, usda); + const handle = twinHandle(usda); + + await mkdir(dirname(out), { recursive: true }); + + // Dynamic import keeps the heavy renderer (chromium + ffmpeg) out of code paths that + // don't render (router mode, brand refusal). + const { bundle } = await import("@remotion/bundler"); + const { selectComposition, renderMedia } = await import("@remotion/renderer"); + + const inputProps = { storyboard, usda }; + // Our sources use TS-style ".js" specifiers in relative imports; teach Remotion's webpack + // to resolve them to the real .ts/.tsx files. + const serveUrl = await bundle({ + entryPoint: ENTRY, + webpackOverride: (config) => ({ + ...config, + resolve: { + ...config.resolve, + extensionAlias: { + ...(config.resolve?.extensionAlias ?? {}), + ".js": [".js", ".ts", ".tsx"], + ".jsx": [".jsx", ".tsx"], + }, + }, + }), + }); + // Software GL + no-sandbox so headless capture works in CI/containers without a GPU. + const chromiumOptions = { gl: "angle" as const }; + const composition = await selectComposition({ + serveUrl, + id: "Short", + inputProps, + chromiumOptions, + }); + + await renderMedia({ + composition, + serveUrl, + codec: "h264", + crf: 18, + outputLocation: out, + inputProps, + chromiumOptions, + concurrency: 1, + ...(opts.renderScale ? { scale: opts.renderScale } : {}), + }); + + const pack = buildPack({ + storyboard, + handle, + mode: "local", + brandScore: report.score, + uri: out, + bytes: await fileBytes(out), + }); + return finalize(pack, opts); +} + +// ---- Router render: dispatch to open-imagine --------------------------------- + +interface JobResponse { + id: string; + status: "queued" | "running" | "done" | "failed"; + error?: string; + creative_pack?: CreativePack; + brand_score?: number; + output_uri?: string; +} + +async function renderViaRouter( + storyboard: Storyboard, + out: string, + opts: RenderOptions, +): Promise { + const cfg = loadConfig(opts.config); + const doFetch = opts.fetchImpl ?? fetch; + const headers: Record = { "content-type": "application/json" }; + if (cfg.routerToken) headers.authorization = `Bearer ${cfg.routerToken}`; + + const storyboardUri = opts.storyboardUri ?? storyboard.id; + + const dispatch = await doFetch(`${cfg.routerUrl}/v1/render`, { + method: "POST", + headers, + body: JSON.stringify({ + kind: "animated-short", + stage_uri: storyboard.product_twin, + storyboard_uri: storyboardUri, + preset: storyboard.preset, + duration_s: storyboard.duration_s, + width: SHORT_WIDTH, + height: SHORT_HEIGHT, + fps: FPS, + out, + }), + }); + if (!dispatch.ok) { + throw new Error(`router POST /v1/render failed: ${dispatch.status} ${await safeText(dispatch)}`); + } + const job = (await dispatch.json()) as JobResponse; + + const done = await pollJob(cfg, doFetch, job.id, headers); + // Prefer the router-returned pack; synthesize one if the stub returns only fields. + const handleSource = isRemoteUri(storyboard.product_twin) + ? storyboard.product_twin + : await readTwinSource(storyboard.product_twin).then(twinHandle).catch(() => storyboard.id); + + const pack = + done.creative_pack ?? + buildPack({ + storyboard, + handle: typeof handleSource === "string" ? handleSource : storyboard.id, + mode: "open-imagine", + brandScore: done.brand_score ?? 1, + uri: done.output_uri ?? out, + }); + return finalize(pack, opts); +} + +async function pollJob( + cfg: ShortsConfig, + doFetch: typeof fetch, + id: string, + headers: Record, + maxPolls = 120, +): Promise { + for (let i = 0; i < maxPolls; i++) { + const res = await doFetch(`${cfg.routerUrl}/v1/jobs/${id}`, { headers }); + if (!res.ok) throw new Error(`router GET /v1/jobs/${id} failed: ${res.status}`); + const job = (await res.json()) as JobResponse; + if (job.status === "done") return job; + if (job.status === "failed") { + throw new Error(`router job ${id} failed: ${job.error ?? "unknown error"}`); + } + await sleep(1000); + } + throw new Error(`router job ${id} did not complete after ${maxPolls} polls`); +} + +let sleepImpl: (ms: number) => Promise = (ms) => new Promise((r) => setTimeout(r, ms)); +/** Test seam so polling doesn't actually wait. */ +export function __setSleep(fn: (ms: number) => Promise): void { + sleepImpl = fn; +} +function sleep(ms: number): Promise { + return sleepImpl(ms); +} + +async function safeText(res: Response): Promise { + try { + return await res.text(); + } catch { + return ""; + } +} + +// ---- Shared ----------------------------------------------------------------- + +function buildPack(args: { + storyboard: Storyboard; + handle: string; + mode: RenderMode; + brandScore: number; + uri: string; + bytes?: number; +}): CreativePack { + const { storyboard, handle, mode, brandScore, uri, bytes } = args; + return { + schema: "creative_pack.v1", + id: `${storyboard.id}-${mode}`, + twin_handle: handle, + preset: storyboard.preset, + format: { + kind: "short", + width: SHORT_WIDTH, + height: SHORT_HEIGHT, + duration_s: storyboard.duration_s, + fps: FPS, + }, + assets: [{ role: "primary", uri, mime: "video/mp4", ...(bytes !== undefined ? { bytes } : {}) }], + brand_score: brandScore, + storyboard_id: storyboard.id, + render_mode: mode, + created_at: new Date().toISOString(), + }; +} + +async function finalize(pack: CreativePack, opts: RenderOptions): Promise { + if (!opts.skipProvenance) { + await emitRenderProvenance(provenanceFromPack(pack), { queuePath: opts.queuePath }); + } + return pack; +} + +/** + * USD twin → composed scene → Three.js animated capture → encoded MP4 → Creative Pack. + * Default mode is local (dev, $0); set mode 'open-imagine' to dispatch to the router. + */ +export async function renderShort(opts: RenderOptions): Promise { + const mode = opts.mode ?? "local"; + const out = opts.out + ? isAbsolute(opts.out) + ? opts.out + : resolve(process.cwd(), opts.out) + : defaultOut(opts.storyboard); + return mode === "open-imagine" + ? renderViaRouter(opts.storyboard, out, opts) + : renderLocal(opts.storyboard, out, opts); +} diff --git a/packages/animated-shorts/src/render/pack.ts b/packages/animated-shorts/src/render/pack.ts new file mode 100644 index 0000000..be0850e --- /dev/null +++ b/packages/animated-shorts/src/render/pack.ts @@ -0,0 +1,33 @@ +import type { Preset } from "../dsl/storyboard.js"; + +// CreativePack — the unit the bandit scores. A short emits the identical shape a still does, +// so the Thompson Sampling loop joins shorts and stills by (twin_handle × preset) without +// caring which pipeline produced them. Matches creative_pack.v1.json. + +export type RenderMode = "local" | "open-imagine"; + +export interface CreativeAsset { + role: "primary" | "thumbnail" | "preview"; + uri: string; + mime: string; + bytes?: number; +} + +export interface CreativePack { + schema: "creative_pack.v1"; + id: string; + twin_handle: string; + preset: Preset; + format: { + kind: "still" | "short"; + width: number; + height: number; + duration_s?: number; + fps?: number; + }; + assets: CreativeAsset[]; + brand_score: number; + storyboard_id: string; + render_mode: RenderMode; + created_at: string; +} diff --git a/packages/animated-shorts/src/render/twin.ts b/packages/animated-shorts/src/render/twin.ts new file mode 100644 index 0000000..661da8f --- /dev/null +++ b/packages/animated-shorts/src/render/twin.ts @@ -0,0 +1,27 @@ +import { readFile } from "node:fs/promises"; +import { parser } from "@shoploop/twin-renderer"; + +// Resolve + read a product twin's USDA source. Local paths are read from disk; s3:// uris +// are dispatched to open-imagine (which has bucket access) rather than fetched here. + +export function isRemoteUri(uri: string): boolean { + return /^(s3|https?):\/\//.test(uri); +} + +export async function readTwinSource(productTwin: string): Promise { + if (isRemoteUri(productTwin)) { + throw new Error( + `local render cannot read remote twin '${productTwin}'; use --mode open-imagine for s3:// twins`, + ); + } + return readFile(productTwin, "utf8"); +} + +/** Stable bandit join key: the twin's commerce:handle, else its root prim name. */ +export function twinHandle(usda: string): string { + const stage = parser.parseUsda(usda); + const root = stage.root; + const handleAttr = root?.attributes.find((a) => a.name === "commerce:handle")?.value; + if (typeof handleAttr === "string" && handleAttr) return handleAttr; + return root?.name ?? "unknown-twin"; +} diff --git a/packages/animated-shorts/src/three/ThreeCanvas.tsx b/packages/animated-shorts/src/three/ThreeCanvas.tsx new file mode 100644 index 0000000..06c0bac --- /dev/null +++ b/packages/animated-shorts/src/three/ThreeCanvas.tsx @@ -0,0 +1,143 @@ +import { + forwardRef, + useImperativeHandle, + useMemo, + useRef, +} from "react"; +import * as THREE from "three"; +import { ThreeCanvas as RemotionThreeCanvas } from "@remotion/three"; +import { useCurrentFrame, useVideoConfig } from "remotion"; +import { parser, scene as sceneBuilder } from "@shoploop/twin-renderer"; +import type { CameraMove, MaterialFinish, Storyboard } from "../dsl/storyboard.js"; +import { evaluateFrame, ROUGHNESS_BY_FINISH, type CameraState } from "../composer/timeline.js"; + +// The Three.js ↔ Remotion bridge. The SAME parser + scene builder as the live web twin +// (re-exported from @shoploop/twin-renderer) compose the geometry here; the only difference +// is that camera/material/variant state is driven by Remotion's frame counter, NOT by an +// animation loop or user input. Auto-rotate is OFF. Everything is keyframe-driven and +// deterministic. + +export interface ThreeCanvasHandle { + setCameraPreset(preset: CameraMove): void; + setVariant(set: string, option: string): void; + setMaterialFinish(mesh: string, finish: MaterialFinish): void; +} + +export interface ThreeCanvasProps { + storyboard: Storyboard; + stage: parser.ProductTwinStage; +} + +interface Overrides { + camera?: CameraMove; + selection: Record; + finishes: Record; +} + +function applyCamera(camera: THREE.PerspectiveCamera, state: CameraState) { + camera.position.set(...state.position); + camera.fov = state.fov; + camera.lookAt(new THREE.Vector3(...state.target)); + camera.updateProjectionMatrix(); +} + +function applyFinishes(group: THREE.Object3D, finishes: Record) { + group.traverse((obj) => { + const mesh = obj as THREE.Mesh; + if (!mesh.isMesh) return; + const finish = finishes[mesh.name]; + if (!finish) return; + const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material]; + for (const m of mats) { + const std = m as THREE.MeshStandardMaterial; + if (std.isMaterial) std.roughness = ROUGHNESS_BY_FINISH[finish]; + } + }); +} + +function SceneContents({ + storyboard, + stage, + overridesRef, + camera, +}: ThreeCanvasProps & { + overridesRef: React.MutableRefObject; + camera: THREE.PerspectiveCamera; +}) { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const state = evaluateFrame(storyboard, frame, fps); + const o = overridesRef.current; + const selection = { ...state.selection, ...o.selection }; + const finishes = { ...state.finishes, ...o.finishes }; + + // Drive the (default) camera off the frame. No useThree/useFrame — the camera instance is + // owned by this component and handed to the Canvas, so it mutates deterministically per frame. + applyCamera(camera, state.camera); + + // Rebuild only when the active variant selection changes — variant switching is a + // timeline keyframe, never a runtime control. + const selKey = JSON.stringify(selection); + const built = useMemo( + () => sceneBuilder.buildScene(stage, { selection }), + // selection captured via selKey; stage is stable for the render. + // eslint-disable-next-line react-hooks/exhaustive-deps + [stage, selKey], + ); + + applyFinishes(built.group, finishes); + + return ( + <> + + + + + + ); +} + +export const ThreeCanvas = forwardRef(function ThreeCanvas( + { storyboard, stage }, + ref, +) { + const { width, height } = useVideoConfig(); + // Imperative overrides. The timeline drives state by default; these handles let a host + // (or a test) pin a value. They never auto-animate. + const overridesRef = useRef({ selection: {}, finishes: {} }); + // The camera the Canvas renders through. Owned here so it can be mutated frame-accurately + // without R3F hooks (keeps the bridge unit-testable outside a live Canvas context). + const camera = useMemo( + () => new THREE.PerspectiveCamera(34, width / height, 0.1, 100), + [width, height], + ); + + useImperativeHandle( + ref, + () => ({ + setCameraPreset(preset) { + overridesRef.current.camera = preset; + }, + setVariant(set, option) { + overridesRef.current.selection = { ...overridesRef.current.selection, [set]: option }; + }, + setMaterialFinish(mesh, finish) { + overridesRef.current.finishes = { ...overridesRef.current.finishes, [mesh]: finish }; + }, + }), + [], + ); + + return ( + + + + ); +}); diff --git a/packages/animated-shorts/storyboards/assets/brand_rules.json b/packages/animated-shorts/storyboards/assets/brand_rules.json new file mode 100644 index 0000000..91c70f9 --- /dev/null +++ b/packages/animated-shorts/storyboards/assets/brand_rules.json @@ -0,0 +1,9 @@ +{ + "brand": "Drink Varitea", + "note": "Kinfolk test as code. Warm kraft + linen, ink type, sage accent. Matte/satin only — high gloss reads as an ad. Calm camera moves.", + "palette": ["#c9a27a", "#f3ece2", "#2b2620", "#8a9a5b", "#d8c3a5"], + "palette_tolerance": 0.32, + "forbidden_finishes": ["gloss"], + "allowed_camera": ["orbit", "dolly", "macro", "still"], + "min_score": 0.6 +} diff --git a/packages/animated-shorts/storyboards/assets/varitea_flavors.usda b/packages/animated-shorts/storyboards/assets/varitea_flavors.usda new file mode 100644 index 0000000..7a64d0d --- /dev/null +++ b/packages/animated-shorts/storyboards/assets/varitea_flavors.usda @@ -0,0 +1,67 @@ +#usda 1.0 +( + defaultPrim = "Product_varitea_flavors" + metersPerUnit = 1 + upAxis = "Y" + customLayerData = { + string "commerce:shop" = "drinkvaritea.myshopify.com" + string "commerce:shopify_id" = "gid://shopify/Product/7700112233600" + string "commerce:generator" = "@shoploop/connector-shopify" + string "commerce:generated_at" = "2026-06-20T12:00:00.000Z" + } +) + +def Xform "Product_varitea_flavors" ( + kind = "component" + variantSets = "flavor" + variants = { + string flavor = "jasmine" + } +) +{ + custom string commerce:title = "Varitea Loose-Leaf Flavors" + custom string commerce:handle = "varitea-flavors" + custom string commerce:vendor = "Drink Varitea" + custom string commerce:product_type = "Tea" + custom string commerce:status = "ACTIVE" + custom string commerce:price = "15.00" + custom string[] brand:tags = ["loose-leaf", "tea", "wellness"] + custom string brand:rules_ref = "/etc/shoploop/brand_rules.json" + + def Scope "Looks" + { + def Material "PreviewMat" + { + custom asset commerce:preview_image = @https://cdn.shopify.com/s/files/1/varitea/flavors.jpg@ + token outputs:surface + } + } + + def Mesh "Placeholder" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 1, 3, 2, 2, 3, 5, 4, 4, 5, 7, 6, 6, 7, 1, 0, 1, 7, 5, 3, 6, 0, 2, 4] + point3f[] points = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5)] + uniform token subdivisionScheme = "none" + rel material:binding = + custom bool commerce:placeholder = true + } + + variantSet "flavor" = { + "jasmine" { + custom string commerce:option_value = "Jasmine Pearl" + custom string commerce:sku = "VAR-FL-JAS" + } + "hibiscus" { + custom string commerce:option_value = "Hibiscus Rose" + custom string commerce:sku = "VAR-FL-HIB" + } + "genmaicha" { + custom string commerce:option_value = "Genmaicha" + custom string commerce:sku = "VAR-FL-GEN" + } + } +} diff --git a/packages/animated-shorts/storyboards/assets/varitea_jasmine_pearl.usda b/packages/animated-shorts/storyboards/assets/varitea_jasmine_pearl.usda new file mode 100644 index 0000000..e11c5cd --- /dev/null +++ b/packages/animated-shorts/storyboards/assets/varitea_jasmine_pearl.usda @@ -0,0 +1,65 @@ +#usda 1.0 +( + defaultPrim = "Product_varitea_jasmine_pearl" + metersPerUnit = 1 + upAxis = "Y" + customLayerData = { + string "commerce:shop" = "drinkvaritea.myshopify.com" + string "commerce:shopify_id" = "gid://shopify/Product/7700112233500" + string "commerce:generator" = "@shoploop/connector-shopify" + string "commerce:generated_at" = "2026-06-20T12:00:00.000Z" + } +) + +def Xform "Product_varitea_jasmine_pearl" ( + kind = "component" + variantSets = "size" + variants = { + string size = "_50g" + } +) +{ + custom string commerce:title = "Varitea Jasmine Pearl" + custom string commerce:handle = "varitea-jasmine-pearl" + custom string commerce:vendor = "Drink Varitea" + custom string commerce:product_type = "Tea" + custom string commerce:status = "ACTIVE" + custom string commerce:price = "16.00" + custom string commerce:sku = "VAR-JP-50" + custom int commerce:inventory = 96 + custom string[] brand:tags = ["jasmine", "green-tea", "wellness"] + custom string brand:rules_ref = "/etc/shoploop/brand_rules.json" + + def Scope "Looks" + { + def Material "PreviewMat" + { + custom asset commerce:preview_image = @https://cdn.shopify.com/s/files/1/varitea/jasmine-pearl.jpg@ + token outputs:surface + } + } + + def Mesh "Placeholder" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 1, 3, 2, 2, 3, 5, 4, 4, 5, 7, 6, 6, 7, 1, 0, 1, 7, 5, 3, 6, 0, 2, 4] + point3f[] points = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5)] + uniform token subdivisionScheme = "none" + rel material:binding = + custom bool commerce:placeholder = true + } + + variantSet "size" = { + "_50g" { + custom string commerce:option_value = "50g" + custom string commerce:sku = "VAR-JP-50" + } + "_100g" { + custom string commerce:option_value = "100g" + custom string commerce:sku = "VAR-JP-100" + } + } +} diff --git a/packages/animated-shorts/storyboards/varitea_flavors_lifestyle_15s.json b/packages/animated-shorts/storyboards/varitea_flavors_lifestyle_15s.json new file mode 100644 index 0000000..30de040 --- /dev/null +++ b/packages/animated-shorts/storyboards/varitea_flavors_lifestyle_15s.json @@ -0,0 +1,15 @@ +{ + "id": "varitea_flavors_lifestyle_15s", + "duration_s": 15, + "preset": "lifestyle", + "product_twin": "storyboards/assets/varitea_flavors.usda", + "brand_rules": "storyboards/assets/brand_rules.json", + "beats": [ + { "t_s": 0, "kind": "camera", "preset": "dolly", "ease": "ease-in-out" }, + { "t_s": 0, "kind": "variant", "set": "flavor", "option": "jasmine" }, + { "t_s": 5, "kind": "variant", "set": "flavor", "option": "hibiscus" }, + { "t_s": 8, "kind": "camera", "preset": "still" }, + { "t_s": 10, "kind": "variant", "set": "flavor", "option": "genmaicha" }, + { "t_s": 12, "kind": "overlay", "text": "Three blends. One ritual.", "placement": "centered", "duration_s": 3 } + ] +} diff --git a/packages/animated-shorts/storyboards/varitea_jasmine_hero_6s.json b/packages/animated-shorts/storyboards/varitea_jasmine_hero_6s.json new file mode 100644 index 0000000..cb04867 --- /dev/null +++ b/packages/animated-shorts/storyboards/varitea_jasmine_hero_6s.json @@ -0,0 +1,11 @@ +{ + "id": "varitea_jasmine_hero_6s", + "duration_s": 6, + "preset": "hero", + "product_twin": "storyboards/assets/varitea_jasmine_pearl.usda", + "brand_rules": "storyboards/assets/brand_rules.json", + "beats": [ + { "t_s": 0, "kind": "camera", "preset": "orbit", "ease": "ease-in-out" }, + { "t_s": 2, "kind": "overlay", "text": "Jasmine Pearl", "placement": "lower-third", "duration_s": 3 } + ] +} diff --git a/packages/animated-shorts/test/brand.test.ts b/packages/animated-shorts/test/brand.test.ts new file mode 100644 index 0000000..8ae917f --- /dev/null +++ b/packages/animated-shorts/test/brand.test.ts @@ -0,0 +1,58 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, it, expect } from "vitest"; +import { renderShort } from "../src/render/orchestrator.js"; +import { gateStoryboard, evaluateBrand, BrandRulesViolation } from "../src/brand/kinfolk.js"; +import { parser } from "@shoploop/twin-renderer"; +import type { Storyboard } from "../src/dsl/storyboard.js"; + +const TWIN = "storyboards/assets/varitea_jasmine_pearl.usda"; +const STRICT = "test/fixtures/strict_brand_rules.json"; +const PASS = "storyboards/assets/brand_rules.json"; + +const offBrand: Storyboard = { + id: "off_brand_short", + duration_s: 6, + preset: "hero", + product_twin: TWIN, + brand_rules: STRICT, + beats: [ + { t_s: 0, kind: "camera", preset: "orbit" }, + { t_s: 1, kind: "material", mesh: "Placeholder", finish: "gloss" }, + ], +}; + +describe("Kinfolk brand gate", () => { + it("refuses to render a storyboard whose composed scene fails Kinfolk", async () => { + await expect(renderShort({ storyboard: offBrand, mode: "local", skipProvenance: true })).rejects.toBeInstanceOf( + BrandRulesViolation, + ); + }); + + it("throws with actionable reasons (palette, finish, camera)", async () => { + let err: BrandRulesViolation | undefined; + try { + await gateStoryboard(offBrand, readFileSync(resolve(process.cwd(), TWIN), "utf8")); + } catch (e) { + err = e as BrandRulesViolation; + } + expect(err).toBeInstanceOf(BrandRulesViolation); + const rules = err!.reasons.map((r) => r.split(":")[0]); + expect(rules).toContain("finish"); // gloss beat is forbidden + expect(rules).toContain("camera"); // orbit not in strict allowed=[still] + expect(rules).toContain("palette"); // kraft is nowhere near #0a0a0a + expect(err!.report.passed).toBe(false); + }); + + it("passes the checked-in Varitea brand rules", () => { + const stage = parser.parseUsda(readFileSync(resolve(process.cwd(), TWIN), "utf8")); + const rules = JSON.parse(readFileSync(resolve(process.cwd(), PASS), "utf8")); + const report = evaluateBrand( + { ...offBrand, beats: [{ t_s: 0, kind: "camera", preset: "orbit" }] }, + stage, + rules, + ); + expect(report.passed).toBe(true); + expect(report.score).toBeGreaterThanOrEqual(0.6); + }); +}); diff --git a/packages/animated-shorts/test/composer.test.tsx b/packages/animated-shorts/test/composer.test.tsx new file mode 100644 index 0000000..5288845 --- /dev/null +++ b/packages/animated-shorts/test/composer.test.tsx @@ -0,0 +1,68 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, it, expect, vi } from "vitest"; + +// Mock Remotion's frame context + the WebGL canvas so the composition mounts in jsdom-free +// node and renders a single, deterministic frame. The canvas itself is exercised end-to-end +// by orchestrator.test.ts via a real headless render. +const h = vi.hoisted(() => ({ frame: 0 })); + +vi.mock("remotion", () => ({ + useCurrentFrame: () => h.frame, + useVideoConfig: () => ({ fps: 30, width: 1080, height: 1920, durationInFrames: 180 }), + AbsoluteFill: ({ children, style }: { children?: React.ReactNode; style?: React.CSSProperties }) => + React.createElement("div", { style }, children), + Audio: () => null, + registerRoot: () => {}, + Composition: () => null, +})); + +vi.mock("@remotion/three", () => ({ + ThreeCanvas: ({ children }: { children?: React.ReactNode }) => + React.createElement("div", { "data-three": "1" }, children), +})); + +const { Short } = await import("../src/composer/Short.js"); +const { evaluateFrame } = await import("../src/composer/timeline.js"); +const { loadStoryboard } = await import("../src/dsl/validate.js"); + +const hero = await loadStoryboard(resolve(process.cwd(), "storyboards/varitea_jasmine_hero_6s.json")); +const flavors = await loadStoryboard( + resolve(process.cwd(), "storyboards/varitea_flavors_lifestyle_15s.json"), +); +const usda = readFileSync(resolve(process.cwd(), "storyboards/assets/varitea_jasmine_pearl.usda"), "utf8"); + +describe("Remotion composer", () => { + it("renders one frame of the composition without throwing", () => { + h.frame = 75; // t=2.5s — inside the lower-third overlay window + const html = renderToStaticMarkup(React.createElement(Short, { storyboard: hero, usda })); + expect(html).toContain("Jasmine Pearl"); + expect(html).toContain('data-three="1"'); + }); + + it("hides the overlay outside its time window", () => { + h.frame = 0; // t=0s — before the overlay appears + const html = renderToStaticMarkup(React.createElement(Short, { storyboard: hero, usda })); + expect(html).not.toContain("Jasmine Pearl"); + }); +}); + +describe("deterministic timeline", () => { + it("is pure: same (storyboard, frame) → identical state", () => { + expect(evaluateFrame(hero, 90)).toEqual(evaluateFrame(hero, 90)); + }); + + it("advances the orbit camera over time", () => { + const a = evaluateFrame(hero, 0).camera.position; + const b = evaluateFrame(hero, 150).camera.position; + expect(a).not.toEqual(b); + }); + + it("flips the active variant on the keyframe boundaries", () => { + expect(evaluateFrame(flavors, 30).selection.flavor).toBe("jasmine"); // t=1s + expect(evaluateFrame(flavors, 5 * 30 + 1).selection.flavor).toBe("hibiscus"); // t>5s + expect(evaluateFrame(flavors, 10 * 30 + 1).selection.flavor).toBe("genmaicha"); // t>10s + }); +}); diff --git a/packages/animated-shorts/test/dsl.test.ts b/packages/animated-shorts/test/dsl.test.ts new file mode 100644 index 0000000..b9ea412 --- /dev/null +++ b/packages/animated-shorts/test/dsl.test.ts @@ -0,0 +1,82 @@ +import { resolve } from "node:path"; +import { describe, it, expect } from "vitest"; +import { + loadStoryboard, + validateStoryboard, + StoryboardValidationError, +} from "../src/dsl/validate.js"; +import { orderedBeats, beatsOfKind, durationInFrames } from "../src/dsl/storyboard.js"; + +const HERO = resolve(process.cwd(), "storyboards/varitea_jasmine_hero_6s.json"); +const FLAVORS = resolve(process.cwd(), "storyboards/varitea_flavors_lifestyle_15s.json"); + +describe("storyboard schema validation", () => { + it("accepts the checked-in Varitea hero short", async () => { + const sb = await loadStoryboard(HERO); + expect(sb.id).toBe("varitea_jasmine_hero_6s"); + expect(sb.duration_s).toBe(6); + expect(durationInFrames(sb)).toBe(180); + expect(beatsOfKind(sb, "overlay")).toHaveLength(1); + }); + + it("accepts the checked-in Varitea flavors lifestyle short", async () => { + const sb = await loadStoryboard(FLAVORS); + expect(sb.duration_s).toBe(15); + const variants = beatsOfKind(sb, "variant"); + expect(variants.map((v) => v.option)).toEqual(["jasmine", "hibiscus", "genmaicha"]); + // ordering helper sorts by t_s + expect(orderedBeats(sb)[0].t_s).toBe(0); + }); + + it("rejects an invalid duration", async () => { + await expect( + validateStoryboard({ + id: "bad", + duration_s: 7, + preset: "hero", + product_twin: "x.usda", + brand_rules: "b.json", + beats: [{ t_s: 0, kind: "camera", preset: "orbit" }], + }), + ).rejects.toBeInstanceOf(StoryboardValidationError); + }); + + it("rejects an unknown camera preset", async () => { + await expect( + validateStoryboard({ + id: "bad", + duration_s: 6, + preset: "hero", + product_twin: "x.usda", + brand_rules: "b.json", + beats: [{ t_s: 0, kind: "camera", preset: "zoom" }], + }), + ).rejects.toBeInstanceOf(StoryboardValidationError); + }); + + it("rejects a beat that falls outside the duration", async () => { + await expect( + validateStoryboard({ + id: "overrun", + duration_s: 6, + preset: "hero", + product_twin: "x.usda", + brand_rules: "b.json", + beats: [{ t_s: 9, kind: "camera", preset: "orbit" }], + }), + ).rejects.toThrow(/outside duration/); + }); + + it("rejects a variant beat missing its option", async () => { + await expect( + validateStoryboard({ + id: "bad", + duration_s: 9, + preset: "pdp", + product_twin: "x.usda", + brand_rules: "b.json", + beats: [{ t_s: 0, kind: "variant", set: "flavor" }], + }), + ).rejects.toBeInstanceOf(StoryboardValidationError); + }); +}); diff --git a/packages/animated-shorts/test/fixtures/strict_brand_rules.json b/packages/animated-shorts/test/fixtures/strict_brand_rules.json new file mode 100644 index 0000000..8d96d9a --- /dev/null +++ b/packages/animated-shorts/test/fixtures/strict_brand_rules.json @@ -0,0 +1,9 @@ +{ + "brand": "Strict (refusal fixture)", + "note": "Deliberately impossible palette + bans gloss, so any composed Varitea frame fails the Kinfolk gate.", + "palette": ["#0a0a0a"], + "palette_tolerance": 0.03, + "forbidden_finishes": ["gloss"], + "allowed_camera": ["still"], + "min_score": 0.95 +} diff --git a/packages/animated-shorts/test/orchestrator.test.ts b/packages/animated-shorts/test/orchestrator.test.ts new file mode 100644 index 0000000..cf02e03 --- /dev/null +++ b/packages/animated-shorts/test/orchestrator.test.ts @@ -0,0 +1,127 @@ +import { mkdtemp, readFile, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { describe, it, expect } from "vitest"; +import { renderShort, __setSleep } from "../src/render/orchestrator.js"; +import type { Storyboard } from "../src/dsl/storyboard.js"; + +const TWIN = "storyboards/assets/varitea_jasmine_pearl.usda"; +const RULES = "storyboards/assets/brand_rules.json"; + +// A 1s storyboard for a fast render. duration_s is normally 6|9|15; cast for the test. +const tiny: Storyboard = { + id: "test_tiny_1s", + duration_s: 1 as Storyboard["duration_s"], + preset: "hero", + product_twin: TWIN, + brand_rules: RULES, + beats: [{ t_s: 0, kind: "camera", preset: "orbit", ease: "ease-in-out" }], +}; + +async function tmp(): Promise { + return mkdtemp(join(tmpdir(), "shorts-")); +} + +describe("orchestrator — local mode", () => { + it("produces an MP4 > 0 bytes and a CreativePack", async () => { + const dir = await tmp(); + const out = join(dir, "tiny.mp4"); + const queuePath = join(dir, "queue.jsonl"); + + const pack = await renderShort({ + storyboard: tiny, + mode: "local", + out, + queuePath, + renderScale: 0.25, // shrink pixels; logical size stays 1080×1920 in the pack + }); + + expect(pack.schema).toBe("creative_pack.v1"); + expect(pack.render_mode).toBe("local"); + expect(pack.format.kind).toBe("short"); + expect(pack.format.width).toBe(1080); + expect(pack.format.height).toBe(1920); + expect(pack.twin_handle).toBe("varitea-jasmine-pearl"); + + const size = (await stat(out)).size; + expect(size).toBeGreaterThan(0); + expect(pack.assets[0].bytes).toBe(size); + + // Provenance appended to the bandit queue. + const queue = await readFile(queuePath, "utf8"); + const rec = JSON.parse(queue.trim().split("\n").at(-1)!); + expect(rec.schema).toBe("render_provenance.v1"); + expect(rec.twin_handle).toBe("varitea-jasmine-pearl"); + expect(rec.preset).toBe("hero"); + }, 120_000); +}); + +describe("orchestrator — open-imagine mode", () => { + it("POSTs the right body, polls until done, returns a pack, emits provenance", async () => { + __setSleep(async () => {}); + const dir = await tmp(); + const queuePath = join(dir, "queue.jsonl"); + const storyboardUri = resolve(process.cwd(), "storyboards/varitea_flavors_lifestyle_15s.json"); + + const calls: { url: string; init?: RequestInit }[] = []; + let polls = 0; + const fakeFetch = (async (url: string | URL, init?: RequestInit) => { + const u = String(url); + calls.push({ url: u, init }); + if (u.endsWith("/v1/render")) { + return { ok: true, status: 200, json: async () => ({ id: "job-42", status: "queued" }) }; + } + polls++; + const status = polls < 2 ? "running" : "done"; + return { + ok: true, + status: 200, + json: async () => ({ + id: "job-42", + status, + brand_score: 0.88, + output_uri: "s3://shoploop-creative/varitea_flavors.mp4", + }), + }; + }) as unknown as typeof fetch; + + const flavors: Storyboard = { + id: "varitea_flavors_lifestyle_15s", + duration_s: 15, + preset: "lifestyle", + product_twin: "storyboards/assets/varitea_flavors.usda", + brand_rules: RULES, + beats: [{ t_s: 0, kind: "camera", preset: "dolly" }], + }; + + const pack = await renderShort({ + storyboard: flavors, + mode: "open-imagine", + storyboardUri, + fetchImpl: fakeFetch, + queuePath, + config: { routerUrl: "http://router.test", routerToken: "secret" }, + }); + + // POST body assertions + const post = calls.find((c) => c.url.endsWith("/v1/render"))!; + expect(post.init?.method).toBe("POST"); + expect((post.init?.headers as Record).authorization).toBe("Bearer secret"); + const body = JSON.parse(post.init!.body as string); + expect(body.kind).toBe("animated-short"); + expect(body.stage_uri).toBe("storyboards/assets/varitea_flavors.usda"); + expect(body.storyboard_uri).toBe(storyboardUri); + expect(body.duration_s).toBe(15); + + // Polled GET /v1/jobs/job-42 at least twice (running → done) + expect(calls.filter((c) => c.url.includes("/v1/jobs/job-42")).length).toBeGreaterThanOrEqual(2); + + expect(pack.render_mode).toBe("open-imagine"); + expect(pack.brand_score).toBe(0.88); + expect(pack.assets[0].uri).toBe("s3://shoploop-creative/varitea_flavors.mp4"); + expect(pack.twin_handle).toBe("varitea-flavors"); + + const queue = await readFile(queuePath, "utf8"); + expect(queue).toContain("render_provenance.v1"); + }); +}); diff --git a/packages/animated-shorts/tsconfig.build.json b/packages/animated-shorts/tsconfig.build.json new file mode 100644 index 0000000..d4e6512 --- /dev/null +++ b/packages/animated-shorts/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "noEmit": false + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["dist", "node_modules", "test"] +} diff --git a/packages/animated-shorts/tsconfig.json b/packages/animated-shorts/tsconfig.json new file mode 100644 index 0000000..ed19183 --- /dev/null +++ b/packages/animated-shorts/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "outDir": "./dist", + "rootDir": ".", + "declaration": true, + "sourceMap": true, + "strict": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "verbatimModuleSyntax": false, + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "test/**/*.tsx"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/animated-shorts/vitest.config.ts b/packages/animated-shorts/vitest.config.ts new file mode 100644 index 0000000..e87ee74 --- /dev/null +++ b/packages/animated-shorts/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["test/**/*.test.ts", "test/**/*.test.tsx"], + globals: true, + // Real Remotion renders pull a headless browser + ffmpeg; give them room. + testTimeout: 120_000, + hookTimeout: 120_000, + }, +}); diff --git a/packages/twin-renderer/.gitignore b/packages/twin-renderer/.gitignore new file mode 100644 index 0000000..4c61bf8 --- /dev/null +++ b/packages/twin-renderer/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +dist-demo +*.tsbuildinfo diff --git a/packages/twin-renderer/DESIGN.md b/packages/twin-renderer/DESIGN.md new file mode 100644 index 0000000..665f924 --- /dev/null +++ b/packages/twin-renderer/DESIGN.md @@ -0,0 +1,6 @@ +# DESIGN + +- **Kinfolk test:** every frame must fit a Kinfolk spread — warm paper tones, restraint, no clutter. A sterile gray-studio render is a regression. +- **Soft natural light:** Poly Haven CC0 HDRI key + warm directional fill, never a hard three-point rig. +- **DOF on hero preset:** the `hero` camera carries shallow depth-of-field + subtle bloom; `pdp`/`lifestyle` stay crisp. +- **No debug overlays in prod:** no axes/grid/stats helpers in the exported library — only the product, its shadow, and the post stack. diff --git a/packages/twin-renderer/README.md b/packages/twin-renderer/README.md new file mode 100644 index 0000000..558631e --- /dev/null +++ b/packages/twin-renderer/README.md @@ -0,0 +1,125 @@ +# @shoploop/twin-renderer + +Browser-side Three.js renderer for `product_twin.usda` files emitted by +[`@shoploop/connector-shopify`](../../connectors/shopify). It is the web-facing +counterpart to the open-imagine GPU render path — **same source-of-truth USD, +two consumers**: an interactive browser preview here, an offline EXR render there. + +- Minimal USDA → scene-graph parser (no heavy USD libs, no WASM). +- `react-three-fiber` renderer with an editorial light/post rig (HDRI, DOF, bloom, ground shadow). +- Variant sets from the twin become runtime UI controls. +- External GLB/USDZ mesh refs load in-browser; placeholder twins render a tasteful procedural stand-in. + +## Install + +```sh +pnpm add @shoploop/twin-renderer three @react-three/fiber @react-three/drei @react-three/postprocessing +``` + +`react`, `react-dom`, and the Three stack are peer/externalized — the host app owns them. + +## Quick start + +```tsx +import { ProductTwin, VariantSelector } from "@shoploop/twin-renderer"; + +export function Preview() { + return ( +
+ + console.log(selection, commerce["commerce:price"]) + } + > +
+ +
+
+
+ ); +} +``` + +`` fetches and parses the `.usda`, builds the scene, and provides a +context that the built-in `` reads. Drop the selector anywhere +inside `` as an HTML overlay, or use it standalone with explicit +`stage`/`value`/`onChange` props. + +### Props (`ProductTwin`) + +| prop | type | notes | +|---|---|---| +| `productTwinUrl` | `string` | URL/path to a `.usda` from `shoploop-shopify ingest` | +| `stage` | `ProductTwinStage?` | pre-parsed stage; skips the fetch | +| `initialVariants` | `Record?` | initial selection, e.g. `{ size: "_100g" }` | +| `onVariantChange` | `(c: VariantChange) => void` | fires on every selection change | +| `environment` | `string?` | HDRI URL or a drei preset name (`apartment`, `sunset`, …) | +| `cameraPreset` | `"hero" \| "pdp" \| "lifestyle"` | `hero` adds shallow DOF | +| `placeholder` | `Partial?` | `{ kind: "tea-tin" \| "box", color, radius, height }` | + +## Pointing at a real twin + +```sh +# in connectors/shopify +pnpm shoploop-shopify ingest --product > varitea_product_twin.usda +``` + +Serve the `.usda` as a static asset and pass its URL to `productTwinUrl`. When +the connector attaches a Shopify Model3d (`prepend references = @…glb@`), the +renderer fetches and loads it via `GLTFLoader`/`USDZLoader`. When the twin only +carries the placeholder `def Mesh`, a procedural rounded-cylinder tea tin renders +instead — no asset required. + +## Embedding + +### Next.js (App Router) + +The renderer is client-only (WebGL + `fetch`). Wrap it in a client component and +lazy-load to keep it out of the server bundle: + +```tsx +"use client"; +import dynamic from "next/dynamic"; +const ProductTwin = dynamic( + () => import("@shoploop/twin-renderer").then((m) => m.ProductTwin), + { ssr: false }, +); +``` + +### Remix + +Render inside a `` boundary (e.g. `remix-utils`) so the canvas only +mounts in the browser; pass the twin URL from your loader data. + +There is **no server-side GLB rendering** — all geometry work is browser-side Three.js. + +## Performance notes + +- Target **60 fps on a 2021 MBP** for the placeholder twin at the `hero` preset. +- Mobile budget: **≥30 fps** mid-tier phones. Bloom + DOF are the heaviest passes — + drop to `pdp`/`lifestyle` (DOF off) or set `bloom={0}` on low-end devices. +- `dpr` is capped at `[1, 2]`; shadow map is 2048² (halve for mobile if needed). +- Geometry is built once per stage; variant switches only restate commerce data, + so the size selector is allocation-free after first load. +- Keep external GLBs **< 2 MB** / **< 50k tris** for a snappy first paint. + +## Scripts + +```sh +pnpm --filter @shoploop/twin-renderer dev # Vite dev server on :5173 (demo) +pnpm --filter @shoploop/twin-renderer build # typecheck + ESM lib (+ .d.ts) + demo site +pnpm --filter @shoploop/twin-renderer test # vitest (parser + scene builder) +``` + +## Scope of the USDA parser + +Supports exactly the subset the Shopify connector emits: layer metadata + +`customLayerData`, `def`/`over`/`class` prims, `Xform`/`Mesh`/`Material`/`Scope`, +prim metadata (`kind`, `variantSets`, `variants`, `references`, `apiSchemas`), +typed attributes (`string`/`int`/`float`/`bool`/`token`/`asset` + `[]` arrays + +numeric tuples), relationships, `variantSet` blocks, and the `commerce:*` / +`brand:*` namespaces. It is **not** a general OpenUSD implementation. + +See [DESIGN.md](./DESIGN.md) for the aesthetic contract. diff --git a/packages/twin-renderer/demo/index.html b/packages/twin-renderer/demo/index.html new file mode 100644 index 0000000..1814e7f --- /dev/null +++ b/packages/twin-renderer/demo/index.html @@ -0,0 +1,31 @@ + + + + + + Varitea · Product Twin + + + +
+ + + diff --git a/packages/twin-renderer/demo/main.tsx b/packages/twin-renderer/demo/main.tsx new file mode 100644 index 0000000..81c1fa4 --- /dev/null +++ b/packages/twin-renderer/demo/main.tsx @@ -0,0 +1,75 @@ +import { StrictMode, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { ProductTwin, type VariantChange, VariantSelector } from "../src/index.js"; + +const USDA_URL = "/varitea_product_twin.usda"; + +function priceLabel(commerce: VariantChange["commerce"]): string { + const price = commerce["commerce:price"]; + const value = commerce["commerce:option_value"]; + const sku = commerce["commerce:sku"]; + const parts = [ + value ? String(value) : null, + price ? `$${String(price)}` : null, + sku ? String(sku) : null, + ].filter(Boolean); + return parts.join(" · "); +} + +function App() { + const [change, setChange] = useState(null); + + return ( +
+
+
+
+ Drink Varitea +
+

Loose-Leaf — Original

+
+
{change ? priceLabel(change.commerce) : "—"}
+
+ +
+ +
+ +
+
+
+ +
+ Small-batch loose-leaf tea blend. Stone-fruit and chamomile notes, cold-brew friendly. + Steep 3–5 min at 90°C. Geometry is a procedural placeholder until a GLB/USDZ ref lands in the + twin — same source-of-truth USD the open-imagine GPU path consumes. +
+
+ ); +} + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/packages/twin-renderer/package.json b/packages/twin-renderer/package.json new file mode 100644 index 0000000..26a26b3 --- /dev/null +++ b/packages/twin-renderer/package.json @@ -0,0 +1,58 @@ +{ + "name": "@shoploop/twin-renderer", + "version": "0.1.0", + "description": "Browser-side Three.js renderer for product_twin.usda emitted by @shoploop/connector-shopify. Web counterpart to the open-imagine GPU render path.", + "license": "UNLICENSED", + "private": true, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "sideEffects": false, + "scripts": { + "dev": "vite --mode demo --port 5173", + "build": "tsc -p tsconfig.build.json --noEmit && vite build && vite build --mode demo", + "build:lib": "vite build", + "build:demo": "vite build --mode demo", + "preview": "vite preview --mode demo --port 5173", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "dependencies": { + "@react-three/drei": "^9.114.0", + "@react-three/fiber": "^8.17.10", + "@react-three/postprocessing": "^2.16.3", + "three": "^0.169.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/three": "^0.169.0", + "@vitejs/plugin-react": "^4.3.0", + "jsdom": "^24.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.6.0", + "vite": "^5.4.0", + "vite-plugin-dts": "^4.2.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/twin-renderer/public/varitea_product_twin.usda b/packages/twin-renderer/public/varitea_product_twin.usda new file mode 100644 index 0000000..d5c95b8 --- /dev/null +++ b/packages/twin-renderer/public/varitea_product_twin.usda @@ -0,0 +1,82 @@ +#usda 1.0 +( + defaultPrim = "Product_varitea_loose_leaf_original" + metersPerUnit = 1 + upAxis = "Y" + customLayerData = { + string "commerce:shop" = "drinkvaritea.myshopify.com" + string "commerce:shopify_id" = "gid://shopify/Product/7700112233445" + string "commerce:generator" = "@shoploop/connector-shopify" + string "commerce:generated_at" = "2026-06-20T12:00:00.000Z" + } +) + +def Xform "Product_varitea_loose_leaf_original" ( + kind = "component" + variantSets = "size" + variants = { + string size = "_50g" + } +) +{ + custom string commerce:title = "Varitea Loose-Leaf — Original" + custom string commerce:handle = "varitea-loose-leaf-original" + custom string commerce:vendor = "Drink Varitea" + custom string commerce:product_type = "Tea" + custom string commerce:status = "ACTIVE" + custom string commerce:description = "Small-batch loose-leaf tea blend. Stone-fruit and chamomile notes, cold-brew friendly. Steep 3–5 min at 90°C." + custom string commerce:price = "14.00" + custom string commerce:sku = "VAR-LL-50" + custom int commerce:inventory = 120 + custom string[] brand:tags = ["loose-leaf", "tea", "wellness", "cold-brew"] + custom string brand:rules_ref = "/etc/shoploop/brand_rules.json" + + def Scope "Looks" + { + def Material "PreviewMat" + { + custom asset commerce:preview_image = @https://cdn.shopify.com/s/files/1/varitea/loose-leaf-original.jpg@ + custom string commerce:preview_alt = "Varitea loose-leaf original in a kraft pouch on linen" + token outputs:surface + } + } + + def Mesh "Placeholder" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 1, 3, 2, 2, 3, 5, 4, 4, 5, 7, 6, 6, 7, 1, 0, 1, 7, 5, 3, 6, 0, 2, 4] + point3f[] points = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5)] + uniform token subdivisionScheme = "none" + rel material:binding = + custom bool commerce:placeholder = true + } + + variantSet "size" = { + "_50g" { + custom string commerce:option_value = "50g" + custom string commerce:sku = "VAR-LL-50" + custom string commerce:price = "14.00" + custom int commerce:inventory = 120 + custom string commerce:variant_id = "gid://shopify/ProductVariant/41001" + } + + "_100g" { + custom string commerce:option_value = "100g" + custom string commerce:sku = "VAR-LL-100" + custom string commerce:price = "24.00" + custom int commerce:inventory = 80 + custom string commerce:variant_id = "gid://shopify/ProductVariant/41002" + } + + "_250g" { + custom string commerce:option_value = "250g" + custom string commerce:sku = "VAR-LL-250" + custom string commerce:price = "52.00" + custom int commerce:inventory = 35 + custom string commerce:variant_id = "gid://shopify/ProductVariant/41003" + } + } +} diff --git a/packages/twin-renderer/src/components/ProductTwin.tsx b/packages/twin-renderer/src/components/ProductTwin.tsx new file mode 100644 index 0000000..2b079d2 --- /dev/null +++ b/packages/twin-renderer/src/components/ProductTwin.tsx @@ -0,0 +1,144 @@ +import { OrbitControls } from "@react-three/drei"; +import { Canvas } from "@react-three/fiber"; +import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; +import type * as THREE from "three"; +import { + type AttrValue, + getVariantSets, + parseUsda, + type ProductTwinStage, + resolveVariantData, +} from "../parser/usda.js"; +import { buildScene, type PlaceholderConfig } from "../scene/build.js"; +import { createAssetLoader } from "../scene/loaders.js"; +import { CAMERA_PRESETS, type CameraPreset, Stage } from "./Stage.js"; +import { TwinContext } from "./context.js"; + +export interface VariantChange { + selection: Record; + commerce: Record; +} + +export interface ProductTwinProps { + /** URL/path to a `.usda` emitted by `shoploop-shopify ingest`. */ + productTwinUrl: string; + /** Pre-parsed stage instead of a URL (skips the fetch). */ + stage?: ProductTwinStage; + initialVariants?: Record; + onVariantChange?: (change: VariantChange) => void; + /** HDRI url or drei preset name. */ + environment?: string; + cameraPreset?: CameraPreset; + placeholder?: Partial; + /** HTML overlay (e.g. ) rendered above the canvas. */ + children?: ReactNode; + className?: string; + style?: React.CSSProperties; +} + +function defaultsFor(stage: ProductTwinStage, initial?: Record): Record { + const sel: Record = {}; + for (const set of getVariantSets(stage)) sel[set.name] = initial?.[set.name] ?? set.default; + return sel; +} + +export function ProductTwin({ + productTwinUrl, + stage: stageProp, + initialVariants, + onVariantChange, + environment, + cameraPreset = "hero", + placeholder, + children, + className, + style, +}: ProductTwinProps) { + const [stage, setStage] = useState(stageProp ?? null); + const [selection, setSelection] = useState>({}); + + // Fetch + parse the USDA (unless a stage was passed directly). + useEffect(() => { + if (stageProp) { + setStage(stageProp); + return; + } + let cancelled = false; + fetch(productTwinUrl) + .then((r) => r.text()) + .then((text) => { + if (!cancelled) setStage(parseUsda(text)); + }) + .catch(() => { + if (!cancelled) setStage(null); + }); + return () => { + cancelled = true; + }; + }, [productTwinUrl, stageProp]); + + // Seed the selection once the stage is known. + useEffect(() => { + if (stage) setSelection(defaultsFor(stage, initialVariants)); + // initialVariants is intentionally read once at stage load. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stage]); + + const loader = useMemo(() => createAssetLoader(), []); + const groupRef = useRef(null); + + // Geometry is built per-stage; variant changes only restate commerce data + // (our connector's variant sets are size options over one mesh). + const built = useMemo(() => { + if (!stage) return null; + return buildScene(stage, { selection, loadAsset: loader, placeholder }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stage, loader]); + + useEffect(() => { + groupRef.current = built?.group ?? null; + }, [built]); + + // Restate userData + emit on every selection change. + useEffect(() => { + if (!stage || !built) return; + const commerce = resolveVariantData(stage, selection); + built.group.userData = { ...built.group.userData, variant: selection, commerce }; + onVariantChange?.({ selection, commerce }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selection, built, stage]); + + const setVariant = (setName: string, value: string) => + setSelection((prev) => ({ ...prev, [setName]: value })); + + const cam = CAMERA_PRESETS[cameraPreset]; + + return ( + +
+ + + {built ? : null} + + + + {children} +
+
+ ); +} diff --git a/packages/twin-renderer/src/components/Stage.tsx b/packages/twin-renderer/src/components/Stage.tsx new file mode 100644 index 0000000..1d6a1cb --- /dev/null +++ b/packages/twin-renderer/src/components/Stage.tsx @@ -0,0 +1,98 @@ +import { ContactShadows, Environment } from "@react-three/drei"; +import { Bloom, DepthOfField, EffectComposer, Vignette } from "@react-three/postprocessing"; +import type { ReactNode } from "react"; + +export type CameraPreset = "hero" | "pdp" | "lifestyle"; + +export interface CameraConfig { + position: [number, number, number]; + fov: number; + target: [number, number, number]; + /** Depth-of-field focus distance; only applied when `dof` is true. */ + focusDistance: number; + dof: boolean; +} + +// Soft, editorial framings. Hero gets shallow DOF; pdp/lifestyle stay crisp. +export const CAMERA_PRESETS: Record = { + hero: { position: [0.9, 0.75, 2.0], fov: 32, target: [0, 0.45, 0], focusDistance: 0.012, dof: true }, + pdp: { position: [0, 0.6, 2.4], fov: 28, target: [0, 0.5, 0], focusDistance: 0.02, dof: false }, + lifestyle: { position: [1.8, 1.0, 2.6], fov: 40, target: [0, 0.4, 0], focusDistance: 0.02, dof: false }, +}; + +// Poly Haven CC0, soft natural light. Runtime-fetched to keep the package small; +// falls back to a built-in drei preset if the CDN is unreachable. +export const DEFAULT_HDRI = + "https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/studio_small_09_1k.hdr"; + +export interface StageProps { + preset?: CameraPreset; + /** HDRI url, a drei preset name, or a checked-in path. */ + environment?: string; + /** Subtle bloom intensity. 0 disables. */ + bloom?: number; + children: ReactNode; +} + +const DREI_PRESETS = new Set([ + "apartment", + "city", + "dawn", + "forest", + "lobby", + "night", + "park", + "studio", + "sunset", + "warehouse", +]); + +export function Stage({ preset = "hero", environment = DEFAULT_HDRI, bloom = 0.35, children }: StageProps) { + const cam = CAMERA_PRESETS[preset]; + const usePreset = DREI_PRESETS.has(environment); + + return ( + <> + {usePreset ? ( + + ) : ( + + )} + + {/* Soft key + warm fill. No sterile gray-studio three-point rig. */} + + + + + {children} + + + + + {cam.dof ? ( + + ) : ( + <> + )} + {bloom > 0 ? : <>} + + + + ); +} diff --git a/packages/twin-renderer/src/components/VariantSelector.tsx b/packages/twin-renderer/src/components/VariantSelector.tsx new file mode 100644 index 0000000..2d85a44 --- /dev/null +++ b/packages/twin-renderer/src/components/VariantSelector.tsx @@ -0,0 +1,63 @@ +import { getVariantSets, type ProductTwinStage } from "../parser/usda.js"; +import { useTwinContext } from "./context.js"; + +export interface VariantSelectorProps { + /** Optional explicit stage; falls back to the enclosing context. */ + stage?: ProductTwinStage | null; + value?: Record; + onChange?: (setName: string, value: string) => void; + className?: string; +} + +// Reads variant sets straight from the parsed stage and emits selection changes. +// Usable standalone (pass `stage`/`value`/`onChange`) or inside . +export function VariantSelector({ stage, value, onChange, className }: VariantSelectorProps) { + const ctx = useTwinContext(); + const activeStage = stage ?? ctx?.stage ?? null; + const selection = value ?? ctx?.selection ?? {}; + const emit = onChange ?? ctx?.setVariant; + + if (!activeStage) return null; + const sets = getVariantSets(activeStage); + if (sets.length === 0) return null; + + return ( +
+ {sets.map((set) => ( +
+ + {set.name} + +
+ {set.options.map((opt) => { + const selected = (selection[set.name] ?? set.default) === opt; + return ( + + ); + })} +
+
+ ))} +
+ ); +} diff --git a/packages/twin-renderer/src/components/context.ts b/packages/twin-renderer/src/components/context.ts new file mode 100644 index 0000000..557d725 --- /dev/null +++ b/packages/twin-renderer/src/components/context.ts @@ -0,0 +1,14 @@ +import { createContext, useContext } from "react"; +import type { ProductTwinStage } from "../parser/usda.js"; + +export interface TwinContextValue { + stage: ProductTwinStage | null; + selection: Record; + setVariant: (setName: string, value: string) => void; +} + +export const TwinContext = createContext(null); + +export function useTwinContext(): TwinContextValue | null { + return useContext(TwinContext); +} diff --git a/packages/twin-renderer/src/index.ts b/packages/twin-renderer/src/index.ts new file mode 100644 index 0000000..6fae83e --- /dev/null +++ b/packages/twin-renderer/src/index.ts @@ -0,0 +1,53 @@ +// Public API — keep this surface tiny and stable; it ships into the Varitea +// landing page and any host React app. + +export { ProductTwin } from "./components/ProductTwin.js"; +export type { ProductTwinProps, VariantChange } from "./components/ProductTwin.js"; + +export { VariantSelector } from "./components/VariantSelector.js"; +export type { VariantSelectorProps } from "./components/VariantSelector.js"; + +export { Stage, CAMERA_PRESETS, DEFAULT_HDRI } from "./components/Stage.js"; +export type { StageProps, CameraPreset, CameraConfig } from "./components/Stage.js"; + +export { TwinContext, useTwinContext } from "./components/context.js"; +export type { TwinContextValue } from "./components/context.js"; + +export { + parseUsda, + getVariantSets, + resolveVariantData, + getLayerCommerce, +} from "./parser/usda.js"; +export type { + ProductTwinStage, + Prim, + PrimMetadata, + Attribute, + Relationship, + VariantSet, + VariantOption, + VariantSetView, + LayerMetadata, + AttrValue, + AssetRef, + PathRef, +} from "./parser/usda.js"; + +export { buildScene, DEFAULT_PLACEHOLDER } from "./scene/build.js"; +export type { + BuildOptions, + BuiltScene, + PlaceholderConfig, + PlaceholderKind, + AssetLoader, +} from "./scene/build.js"; + +export { createAssetLoader } from "./scene/loaders.js"; + +// Namespaced surfaces for headless consumers (e.g. @shoploop/animated-shorts) that +// drive the SAME parser + scene builder inside a Remotion . Re-exported +// rather than duplicated so the offline shorts pipeline and the live web twin stay +// pixel-for-pixel in agreement. +export * as parser from "./parser/usda.js"; +export * as scene from "./scene/build.js"; diff --git a/packages/twin-renderer/src/parser/usda.ts b/packages/twin-renderer/src/parser/usda.ts new file mode 100644 index 0000000..9388f16 --- /dev/null +++ b/packages/twin-renderer/src/parser/usda.ts @@ -0,0 +1,556 @@ +// Minimal USDA parser for the subset emitted by @shoploop/connector-shopify. +// Scope is deliberately narrow — this is NOT a general OpenUSD implementation. +// Supported: layer metadata + customLayerData, `def`/`over`/`class` prims with +// metadata parens (kind, variantSets, variants, references, apiSchemas), +// typed attributes (string/int/float/bool/token/asset + [] arrays + numeric +// tuples), relationships, and `variantSet` blocks. Anything outside that surface +// is skipped rather than throwing, so connector changes degrade gracefully. + +export type AssetRef = { kind: "asset"; path: string }; +export type PathRef = { kind: "path"; path: string }; + +export type AttrValue = + | string + | number + | boolean + | AssetRef + | PathRef + | AttrValue[] + | number[] + | number[][] + | { [key: string]: AttrValue }; + +export interface Attribute { + name: string; + type: string; + custom: boolean; + uniform: boolean; + value?: AttrValue; +} + +export interface Relationship { + name: string; + target: string | string[]; +} + +export interface VariantOption { + name: string; + attributes: Attribute[]; + children: Prim[]; +} + +export interface VariantSet { + name: string; + variants: VariantOption[]; +} + +export interface PrimMetadata { + kind?: string; + /** Declared variant set names, e.g. ["size"]. */ + variantSets: string[]; + /** Default/selected variant per set, e.g. { size: "_50g" }. */ + variants: Record; + /** External asset references (GLB/USDZ) from `references = @file@`. */ + references: AssetRef[]; + apiSchemas: string[]; + raw: Record; +} + +export interface Prim { + specifier: "def" | "over" | "class"; + typeName?: string; + name: string; + path: string; + metadata: PrimMetadata; + attributes: Attribute[]; + relationships: Relationship[]; + children: Prim[]; + variantSets: VariantSet[]; +} + +export interface LayerMetadata { + defaultPrim?: string; + metersPerUnit?: number; + upAxis?: string; + customLayerData: Record; +} + +export interface ProductTwinStage { + version: string; + metadata: LayerMetadata; + prims: Prim[]; + /** Convenience: the prim named by defaultPrim, else the first prim. */ + root?: Prim; +} + +const WORD_RE = /[A-Za-z0-9_:.\-/]/; + +class Scanner { + private i = 0; + constructor(private readonly src: string) {} + + get pos(): number { + return this.i; + } + + eof(): boolean { + return this.i >= this.src.length; + } + + /** Skip whitespace, commas, and `#` line comments. */ + skip(): void { + while (this.i < this.src.length) { + const c = this.src[this.i]; + if (c === "#") { + while (this.i < this.src.length && this.src[this.i] !== "\n") this.i++; + } else if (c === " " || c === "\t" || c === "\r" || c === "\n" || c === ",") { + this.i++; + } else { + break; + } + } + } + + peekChar(): string { + this.skip(); + return this.src[this.i] ?? ""; + } + + match(ch: string): boolean { + this.skip(); + if (this.src[this.i] === ch) { + this.i++; + return true; + } + return false; + } + + expect(ch: string): void { + if (!this.match(ch)) { + throw new Error(`USDA parse error: expected '${ch}' at offset ${this.i}`); + } + } + + /** A bare word: identifier, namespaced name, or type (incl. trailing []). */ + word(): string { + this.skip(); + const start = this.i; + while (this.i < this.src.length && WORD_RE.test(this.src[this.i])) this.i++; + let w = this.src.slice(start, this.i); + // type tokens may carry an array suffix: string[] / float3[] / point3f[] + if (this.src[this.i] === "[" && this.src[this.i + 1] === "]") { + w += "[]"; + this.i += 2; + } + return w; + } + + /** Read a double-quoted string with USDA escapes. Assumes current char is `"`. */ + string(): string { + this.skip(); + if (this.src[this.i] !== '"') throw new Error(`expected string at ${this.i}`); + this.i++; + let out = ""; + while (this.i < this.src.length) { + const c = this.src[this.i++]; + if (c === "\\") { + const n = this.src[this.i++]; + out += n === "n" ? "\n" : n === "t" ? "\t" : n; + } else if (c === '"') { + break; + } else { + out += c; + } + } + return out; + } + + /** Read an `@asset@` ref. `@@` is an empty asset. Assumes current char is `@`. */ + asset(): string { + this.skip(); + this.i++; // opening @ + const start = this.i; + while (this.i < this.src.length && this.src[this.i] !== "@") this.i++; + const v = this.src.slice(start, this.i); + this.i++; // closing @ + return v; + } + + /** Read a `
` reference. Assumes current char is `<`. */ + path(): string { + this.skip(); + this.i++; // < + const start = this.i; + while (this.i < this.src.length && this.src[this.i] !== ">") this.i++; + const v = this.src.slice(start, this.i); + this.i++; // > + return v; + } +} + +function isNumberWord(w: string): boolean { + return /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(w); +} + +// Read a single USDA value starting at the scanner head. +function readValue(s: Scanner): AttrValue { + const c = s.peekChar(); + if (c === '"') return s.string(); + if (c === "@") return { kind: "asset", path: s.asset() }; + if (c === "<") return { kind: "path", path: s.path() }; + if (c === "[") return readArray(s); + if (c === "(") return readTuple(s); + if (c === "{") return readDict(s); + + const w = s.word(); + if (w === "true") return true; + if (w === "false") return false; + if (isNumberWord(w)) return Number(w); + return w; // bare token, e.g. an enum-like value +} + +function readArray(s: Scanner): AttrValue[] { + s.expect("["); + const items: AttrValue[] = []; + while (s.peekChar() !== "]" && !s.eof()) { + items.push(readValue(s)); + } + s.expect("]"); + return items; +} + +// Numeric tuple like (-0.5, 0.5, 0.5) -> number[]. +function readTuple(s: Scanner): number[] { + s.expect("("); + const nums: number[] = []; + while (s.peekChar() !== ")" && !s.eof()) { + const v = readValue(s); + if (typeof v === "number") nums.push(v); + } + s.expect(")"); + return nums; +} + +function readDict(s: Scanner): Record { + s.expect("{"); + const obj: Record = {}; + while (s.peekChar() !== "}" && !s.eof()) { + // entries: "key" = value OR key = value + s.word(); // type token (ignored) + const key = s.peekChar() === '"' ? s.string() : s.word(); + s.expect("="); + obj[key] = readValue(s); + } + s.expect("}"); + return obj; +} + +const SPECIFIERS = new Set(["def", "over", "class"]); +const QUALIFIERS = new Set(["custom", "uniform", "prepend", "append", "varying", "config"]); + +// Parse the body of a `{ ... }` prim/variant block into attrs/rels/children/variantSets. +interface Body { + attributes: Attribute[]; + relationships: Relationship[]; + children: Prim[]; + variantSets: VariantSet[]; +} + +function parseBody(s: Scanner, parentPath: string): Body { + s.expect("{"); + const body: Body = { attributes: [], relationships: [], children: [], variantSets: [] }; + + while (!s.eof()) { + const c = s.peekChar(); + if (c === "}") break; + + const w = s.word(); + + if (SPECIFIERS.has(w)) { + body.children.push(parsePrim(s, w as Prim["specifier"], parentPath)); + continue; + } + if (w === "variantSet") { + body.variantSets.push(parseVariantSet(s, parentPath)); + continue; + } + if (w === "rel") { + body.relationships.push(parseRelationship(s)); + continue; + } + // Otherwise it's an attribute line, possibly with qualifiers. + const attr = parseAttribute(s, w); + if (attr) body.attributes.push(attr); + } + + s.expect("}"); + return body; +} + +function pathString(v: AttrValue): string { + if (typeof v === "object" && v !== null && "kind" in v) { + const o = v as AssetRef | PathRef; + if (o.kind === "path" || o.kind === "asset") return o.path; + } + return String(v); +} + +function parseRelationship(s: Scanner): Relationship { + const name = s.word(); + let target: string | string[] = ""; + if (s.match("=")) { + const v = readValue(s); + target = Array.isArray(v) ? v.map(pathString) : pathString(v); + } + return { name, target }; +} + +// `firstWord` is the token already consumed (a qualifier or the type name). +function parseAttribute(s: Scanner, firstWord: string): Attribute | null { + let custom = false; + let uniform = false; + let type = firstWord; + + while (QUALIFIERS.has(type)) { + if (type === "custom") custom = true; + if (type === "uniform") uniform = true; + type = s.word(); + } + + if (!type) return null; + const name = s.word(); + if (!name) return null; + + let value: AttrValue | undefined; + if (s.match("=")) { + value = readValue(s); + } + return { name, type, custom, uniform, value }; +} + +function parseVariantSet(s: Scanner, parentPath: string): VariantSet { + const name = s.string(); + s.expect("="); + s.expect("{"); + const variants: VariantOption[] = []; + while (s.peekChar() !== "}" && !s.eof()) { + const vName = s.string(); + const vPath = `${parentPath}{${name}=${vName}}`; + const body = parseBody(s, vPath); + variants.push({ name: vName, attributes: body.attributes, children: body.children }); + } + s.expect("}"); + return { name, variants }; +} + +function parsePrim(s: Scanner, specifier: Prim["specifier"], parentPath: string): Prim { + // After specifier: optional typeName, then quoted name. + let typeName: string | undefined; + let name: string; + if (s.peekChar() === '"') { + name = s.string(); + } else { + typeName = s.word(); + name = s.string(); + } + + const path = `${parentPath}/${name}`; + const metadata: PrimMetadata = { + variantSets: [], + variants: {}, + references: [], + apiSchemas: [], + raw: {}, + }; + + // Optional metadata parens. + if (s.peekChar() === "(") { + parsePrimMetadata(s, metadata); + } + + const body: Body = s.peekChar() === "{" + ? parseBody(s, path) + : { attributes: [], relationships: [], children: [], variantSets: [] }; + + return { + specifier, + typeName, + name, + path, + metadata, + attributes: body.attributes, + relationships: body.relationships, + children: body.children, + variantSets: body.variantSets, + }; +} + +function parsePrimMetadata(s: Scanner, meta: PrimMetadata): void { + s.expect("("); + while (s.peekChar() !== ")" && !s.eof()) { + // Each line: [prepend|append] key = value (string-literal metadata also possible) + if (s.peekChar() === '"') { + // documentation string or similar; consume and ignore + s.string(); + continue; + } + let key = s.word(); + if (key === "prepend" || key === "append") key = s.word(); + if (!key) break; + if (!s.match("=")) continue; + const value = readValue(s); + meta.raw[key] = value; + + switch (key) { + case "kind": + if (typeof value === "string") meta.kind = value; + break; + case "variantSets": + meta.variantSets = toStringArray(value); + break; + case "variants": + if (isPlainObject(value)) { + for (const [k, v] of Object.entries(value)) meta.variants[k] = String(v); + } + break; + case "references": + meta.references = collectAssets(value); + break; + case "apiSchemas": + meta.apiSchemas = toStringArray(value); + break; + default: + break; + } + } + s.expect(")"); +} + +function isPlainObject(v: AttrValue): v is Record { + return typeof v === "object" && v !== null && !Array.isArray(v) && !("kind" in v); +} + +function toStringArray(v: AttrValue): string[] { + if (typeof v === "string") return [v]; + if (Array.isArray(v)) return v.map(String); + return []; +} + +function collectAssets(v: AttrValue): AssetRef[] { + if (Array.isArray(v)) return v.flatMap(collectAssets); + if (typeof v === "object" && v !== null && "kind" in v && v.kind === "asset") { + const path = (v as AssetRef).path; + return path ? [{ kind: "asset", path }] : []; + } + return []; +} + +function parseLayerMetadata(s: Scanner): LayerMetadata { + const meta: LayerMetadata = { customLayerData: {} }; + if (s.peekChar() !== "(") return meta; + s.expect("("); + while (s.peekChar() !== ")" && !s.eof()) { + const key = s.word(); + if (!key) break; + if (!s.match("=")) continue; + const value = readValue(s); + switch (key) { + case "defaultPrim": + if (typeof value === "string") meta.defaultPrim = value; + break; + case "metersPerUnit": + if (typeof value === "number") meta.metersPerUnit = value; + break; + case "upAxis": + if (typeof value === "string") meta.upAxis = value; + break; + case "customLayerData": + if (isPlainObject(value)) meta.customLayerData = value; + break; + default: + break; + } + } + s.expect(")"); + return meta; +} + +export function parseUsda(src: string): ProductTwinStage { + const s = new Scanner(src); + + // Magic line `#usda 1.0` is consumed as a comment by skip(); recover the version. + const versionMatch = src.match(/#usda\s+([\d.]+)/); + const version = versionMatch ? versionMatch[1] : "1.0"; + + const metadata = parseLayerMetadata(s); + + const prims: Prim[] = []; + while (!s.eof()) { + const w = s.word(); + if (!w) break; + if (SPECIFIERS.has(w)) { + prims.push(parsePrim(s, w as Prim["specifier"], "")); + } else if (s.eof()) { + break; + } + } + + const root = + (metadata.defaultPrim && prims.find((p) => p.name === metadata.defaultPrim)) || prims[0]; + + return { version, metadata, prims, root }; +} + +// ---- Stage helpers (used by the scene builder and components) ---------------- + +export interface VariantSetView { + name: string; + options: string[]; + /** Default selection from prim metadata, falls back to first option. */ + default: string; +} + +/** Flatten the declared + authored variant sets on the root prim into a UI view. */ +export function getVariantSets(stage: ProductTwinStage): VariantSetView[] { + const root = stage.root; + if (!root) return []; + return root.variantSets.map((vs) => { + const options = vs.variants.map((v) => v.name); + return { + name: vs.name, + options, + default: root.metadata.variants[vs.name] ?? options[0] ?? "", + }; + }); +} + +/** Merge the commerce attrs that a given variant selection authors over the root. */ +export function resolveVariantData( + stage: ProductTwinStage, + selection: Record, +): Record { + const root = stage.root; + const data: Record = {}; + if (!root) return data; + for (const attr of root.attributes) { + if (attr.value !== undefined) data[attr.name] = attr.value; + } + for (const vs of root.variantSets) { + const chosen = selection[vs.name] ?? root.metadata.variants[vs.name]; + const opt = vs.variants.find((v) => v.name === chosen); + if (!opt) continue; + for (const attr of opt.attributes) { + if (attr.value !== undefined) data[attr.name] = attr.value; + } + } + return data; +} + +/** Read the layer-level commerce:* metadata (shop, generator, timestamps). */ +export function getLayerCommerce(stage: ProductTwinStage): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(stage.metadata.customLayerData)) { + if (k.startsWith("commerce:")) out[k] = String(v); + } + return out; +} diff --git a/packages/twin-renderer/src/scene/build.ts b/packages/twin-renderer/src/scene/build.ts new file mode 100644 index 0000000..08d1e83 --- /dev/null +++ b/packages/twin-renderer/src/scene/build.ts @@ -0,0 +1,262 @@ +import * as THREE from "three"; +import { + type AttrValue, + type Prim, + type ProductTwinStage, + resolveVariantData, +} from "../parser/usda.js"; + +export type PlaceholderKind = "tea-tin" | "box"; + +export interface PlaceholderConfig { + kind: PlaceholderKind; + /** Base color of the placeholder body (kraft/warm by default). */ + color: string; + radius: number; + height: number; +} + +export const DEFAULT_PLACEHOLDER: PlaceholderConfig = { + kind: "tea-tin", + color: "#c9a27a", + radius: 0.32, + height: 0.9, +}; + +/** Loads an external GLB/USDZ ref into an Object3D. Injected so the builder stays + * synchronous + network-free for tests; ProductTwin wires real three loaders. */ +export type AssetLoader = (url: string, format: "glb" | "usdz") => Promise; + +export interface BuildOptions { + selection?: Record; + placeholder?: Partial; + loadAsset?: AssetLoader; + /** Called once any async external assets have been attached. */ + onAssetLoaded?: (object: THREE.Object3D) => void; +} + +export interface BuiltScene { + group: THREE.Group; + /** Resolved commerce/brand attrs for the active variant selection. */ + variantData: Record; + /** True when a procedural placeholder stands in for real geometry. */ + isPlaceholder: boolean; + /** External asset URLs discovered in the stage (may still be loading). */ + externalRefs: { url: string; format: "glb" | "usdz" }[]; +} + +function assetFormat(url: string): "glb" | "usdz" | null { + const clean = url.split("?")[0].toLowerCase(); + if (clean.endsWith(".glb") || clean.endsWith(".gltf")) return "glb"; + if (clean.endsWith(".usdz")) return "usdz"; + return null; +} + +// A tasteful stand-in: rounded cylinder reading as a tea tin. No debug look. +function buildTeaTin(cfg: PlaceholderConfig): THREE.Group { + const g = new THREE.Group(); + g.name = "PlaceholderTeaTin"; + + const bodyMat = new THREE.MeshStandardMaterial({ + color: new THREE.Color(cfg.color), + roughness: 0.55, + metalness: 0.15, + }); + const lidMat = new THREE.MeshStandardMaterial({ + color: new THREE.Color(cfg.color).multiplyScalar(0.82), + roughness: 0.4, + metalness: 0.25, + }); + + const r = cfg.radius; + const h = cfg.height; + const seg = 64; + + const body = new THREE.Mesh(new THREE.CylinderGeometry(r, r, h * 0.82, seg, 1, false), bodyMat); + body.position.y = h * 0.41; + body.castShadow = true; + body.receiveShadow = true; + g.add(body); + + // Rounded shoulder + lid for the tin silhouette. + const shoulder = new THREE.Mesh(new THREE.TorusGeometry(r * 0.96, r * 0.12, 16, seg), lidMat); + shoulder.rotation.x = Math.PI / 2; + shoulder.position.y = h * 0.82; + shoulder.castShadow = true; + g.add(shoulder); + + const lid = new THREE.Mesh(new THREE.CylinderGeometry(r * 0.92, r * 0.96, h * 0.16, seg), lidMat); + lid.position.y = h * 0.9; + lid.castShadow = true; + g.add(lid); + + return g; +} + +function buildBox(cfg: PlaceholderConfig): THREE.Group { + const g = new THREE.Group(); + g.name = "PlaceholderBox"; + const mat = new THREE.MeshStandardMaterial({ + color: new THREE.Color(cfg.color), + roughness: 0.6, + metalness: 0.1, + }); + const s = cfg.radius * 1.6; + const mesh = new THREE.Mesh(new THREE.BoxGeometry(s, cfg.height, s), mat); + mesh.position.y = cfg.height / 2; + mesh.castShadow = true; + mesh.receiveShadow = true; + g.add(mesh); + return g; +} + +function buildPlaceholder(cfg: PlaceholderConfig): THREE.Group { + return cfg.kind === "box" ? buildBox(cfg) : buildTeaTin(cfg); +} + +// Real authored mesh geometry (the connector's placeholder cube has explicit +// points/faceVertexIndices). We honor it when present so nothing is invented. +function buildAuthoredMesh(prim: Prim): THREE.Mesh | null { + const points = prim.attributes.find((a) => a.name === "points")?.value; + const counts = prim.attributes.find((a) => a.name === "faceVertexCounts")?.value; + const indices = prim.attributes.find((a) => a.name === "faceVertexIndices")?.value; + if (!Array.isArray(points) || !Array.isArray(indices) || !Array.isArray(counts)) return null; + + const flatPoints: number[] = []; + for (const p of points as number[][]) { + if (Array.isArray(p)) flatPoints.push(p[0], p[1], p[2]); + } + if (flatPoints.length === 0) return null; + + // Triangulate polygons (fan) using faceVertexCounts + faceVertexIndices. + const tri: number[] = []; + let cursor = 0; + for (const count of counts as number[]) { + const face = (indices as number[]).slice(cursor, cursor + count); + cursor += count; + for (let k = 1; k < count - 1; k++) { + tri.push(face[0], face[k], face[k + 1]); + } + } + + const geo = new THREE.BufferGeometry(); + geo.setAttribute("position", new THREE.Float32BufferAttribute(flatPoints, 3)); + geo.setIndex(tri); + geo.computeVertexNormals(); + + const mesh = new THREE.Mesh( + geo, + new THREE.MeshStandardMaterial({ color: new THREE.Color("#c9a27a"), roughness: 0.6 }), + ); + mesh.name = prim.name; + mesh.castShadow = true; + mesh.receiveShadow = true; + return mesh; +} + +function isPlaceholderMesh(prim: Prim): boolean { + const flag = prim.attributes.find((a) => a.name === "commerce:placeholder")?.value; + return flag === true; +} + +// Walk a prim subtree and attach geometry / external-ref placeholders. +function buildPrim( + prim: Prim, + cfg: PlaceholderConfig, + opts: BuildOptions, + out: { externalRefs: BuiltScene["externalRefs"]; usedPlaceholder: boolean }, +): THREE.Object3D | null { + // External GLB/USDZ reference on an Xform (connector's "Geometry" prim). + const ref = prim.metadata.references[0]; + if (ref) { + const fmt = assetFormat(ref.path); + if (fmt) { + out.externalRefs.push({ url: ref.path, format: fmt }); + const holder = new THREE.Group(); + holder.name = prim.name; + if (opts.loadAsset) { + opts + .loadAsset(ref.path, fmt) + .then((obj) => { + holder.add(obj); + opts.onAssetLoaded?.(obj); + }) + .catch(() => { + // Network/loader failure degrades to a procedural placeholder. + const ph = buildPlaceholder(cfg); + holder.add(ph); + opts.onAssetLoaded?.(ph); + }); + } + return holder; + } + } + + if (prim.typeName === "Mesh") { + if (isPlaceholderMesh(prim)) { + out.usedPlaceholder = true; + return buildPlaceholder(cfg); + } + const authored = buildAuthoredMesh(prim); + if (authored) return authored; + out.usedPlaceholder = true; + return buildPlaceholder(cfg); + } + + // Scope/Material/other: recurse into children only (no renderable geometry here). + if (prim.typeName === "Xform" || prim.typeName === "Scope" || !prim.typeName) { + const group = new THREE.Group(); + group.name = prim.name; + let added = false; + for (const child of prim.children) { + const obj = buildPrim(child, cfg, opts, out); + if (obj) { + group.add(obj); + added = true; + } + } + return added ? group : null; + } + + return null; +} + +export function buildScene(stage: ProductTwinStage, opts: BuildOptions = {}): BuiltScene { + const cfg: PlaceholderConfig = { ...DEFAULT_PLACEHOLDER, ...opts.placeholder }; + const selection = opts.selection ?? {}; + const root = stage.root; + + const group = new THREE.Group(); + group.name = root?.name ?? "ProductTwin"; + + const out = { externalRefs: [] as BuiltScene["externalRefs"], usedPlaceholder: false }; + + if (root) { + let rendered = false; + for (const child of root.children) { + const obj = buildPrim(child, cfg, opts, out); + if (obj) { + group.add(obj); + rendered = true; + } + } + // Root with no renderable children still gets a placeholder so the stage is never empty. + if (!rendered) { + group.add(buildPlaceholder(cfg)); + out.usedPlaceholder = true; + } + } else { + group.add(buildPlaceholder(cfg)); + out.usedPlaceholder = true; + } + + const variantData = resolveVariantData(stage, selection); + group.userData = { ...group.userData, variant: selection, commerce: variantData }; + + return { + group, + variantData, + isPlaceholder: out.usedPlaceholder, + externalRefs: out.externalRefs, + }; +} diff --git a/packages/twin-renderer/src/scene/loaders.ts b/packages/twin-renderer/src/scene/loaders.ts new file mode 100644 index 0000000..e2206ec --- /dev/null +++ b/packages/twin-renderer/src/scene/loaders.ts @@ -0,0 +1,30 @@ +import * as THREE from "three"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; +import { USDZLoader } from "three/examples/jsm/loaders/USDZLoader.js"; +import type { AssetLoader } from "./build.js"; + +// Browser-side loaders for external mesh refs. Kept out of build.ts so the +// builder remains synchronous and network-free under jsdom tests. +export function createAssetLoader(): AssetLoader { + const gltf = new GLTFLoader(); + const usdz = new USDZLoader(); + + return (url, format) => + new Promise((resolve, reject) => { + if (format === "glb") { + gltf.load( + url, + (g) => resolve(g.scene), + undefined, + (err) => reject(err), + ); + } else { + usdz.load( + url, + (obj) => resolve(obj), + undefined, + (err) => reject(err), + ); + } + }); +} diff --git a/packages/twin-renderer/test/build.test.ts b/packages/twin-renderer/test/build.test.ts new file mode 100644 index 0000000..1f3ab1e --- /dev/null +++ b/packages/twin-renderer/test/build.test.ts @@ -0,0 +1,90 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import * as THREE from "three"; +import { describe, expect, it, vi } from "vitest"; +import { parseUsda } from "../src/parser/usda.js"; +import { buildScene } from "../src/scene/build.js"; + +const FIXTURE = join(process.cwd(), "test/fixtures/varitea_product_twin.usda"); +const stage = parseUsda(readFileSync(FIXTURE, "utf8")); + +function meshCount(root: THREE.Object3D): number { + let n = 0; + root.traverse((o) => { + if ((o as THREE.Mesh).isMesh) n++; + }); + return n; +} + +describe("buildScene — Varitea fixture", () => { + it("produces a THREE.Group with a procedural placeholder for the default variant", () => { + const built = buildScene(stage, { selection: { size: "_50g" } }); + expect(built.group).toBeInstanceOf(THREE.Group); + expect(built.isPlaceholder).toBe(true); + expect(meshCount(built.group)).toBeGreaterThan(0); + expect(built.externalRefs).toHaveLength(0); + }); + + it("names the root group after the product prim", () => { + const built = buildScene(stage); + expect(built.group.name).toBe("Product_varitea_loose_leaf_original"); + }); + + it("attaches resolved commerce data to userData per selection", () => { + const fifty = buildScene(stage, { selection: { size: "_50g" } }); + expect(fifty.variantData["commerce:price"]).toBe("14.00"); + expect(fifty.group.userData.commerce["commerce:price"]).toBe("14.00"); + expect(fifty.group.userData.variant).toEqual({ size: "_50g" }); + + const big = buildScene(stage, { selection: { size: "_250g" } }); + expect(big.variantData["commerce:price"]).toBe("52.00"); + expect(big.variantData["commerce:sku"]).toBe("VAR-LL-250"); + }); + + it("uses the tea-tin placeholder by default and a box when configured", () => { + const tin = buildScene(stage, { placeholder: { kind: "tea-tin" } }); + let foundTin = false; + tin.group.traverse((o) => { + if (o.name === "PlaceholderTeaTin") foundTin = true; + }); + expect(foundTin).toBe(true); + + const box = buildScene(stage, { placeholder: { kind: "box" } }); + let foundBox = false; + box.group.traverse((o) => { + if (o.name === "PlaceholderBox") foundBox = true; + }); + expect(foundBox).toBe(true); + }); + + it("discovers external GLB refs and invokes the injected loader", async () => { + const src = `#usda 1.0 +def Xform "Product" ( +) +{ + def Xform "Geometry" ( + prepend references = @https://cdn.example.com/tin.glb@ + ) + { + custom string commerce:mesh_source = "https://cdn.example.com/tin.glb" + } +} +`; + const refStage = parseUsda(src); + const loaded = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1)); + loaded.name = "LoadedGLB"; + const loadAsset = vi.fn().mockResolvedValue(loaded); + + const built = buildScene(refStage, { loadAsset }); + expect(built.externalRefs).toEqual([{ url: "https://cdn.example.com/tin.glb", format: "glb" }]); + expect(loadAsset).toHaveBeenCalledWith("https://cdn.example.com/tin.glb", "glb"); + + await Promise.resolve(); + await Promise.resolve(); + let found = false; + built.group.traverse((o) => { + if (o.name === "LoadedGLB") found = true; + }); + expect(found).toBe(true); + }); +}); diff --git a/packages/twin-renderer/test/fixtures/varitea_product_twin.usda b/packages/twin-renderer/test/fixtures/varitea_product_twin.usda new file mode 100644 index 0000000..d5c95b8 --- /dev/null +++ b/packages/twin-renderer/test/fixtures/varitea_product_twin.usda @@ -0,0 +1,82 @@ +#usda 1.0 +( + defaultPrim = "Product_varitea_loose_leaf_original" + metersPerUnit = 1 + upAxis = "Y" + customLayerData = { + string "commerce:shop" = "drinkvaritea.myshopify.com" + string "commerce:shopify_id" = "gid://shopify/Product/7700112233445" + string "commerce:generator" = "@shoploop/connector-shopify" + string "commerce:generated_at" = "2026-06-20T12:00:00.000Z" + } +) + +def Xform "Product_varitea_loose_leaf_original" ( + kind = "component" + variantSets = "size" + variants = { + string size = "_50g" + } +) +{ + custom string commerce:title = "Varitea Loose-Leaf — Original" + custom string commerce:handle = "varitea-loose-leaf-original" + custom string commerce:vendor = "Drink Varitea" + custom string commerce:product_type = "Tea" + custom string commerce:status = "ACTIVE" + custom string commerce:description = "Small-batch loose-leaf tea blend. Stone-fruit and chamomile notes, cold-brew friendly. Steep 3–5 min at 90°C." + custom string commerce:price = "14.00" + custom string commerce:sku = "VAR-LL-50" + custom int commerce:inventory = 120 + custom string[] brand:tags = ["loose-leaf", "tea", "wellness", "cold-brew"] + custom string brand:rules_ref = "/etc/shoploop/brand_rules.json" + + def Scope "Looks" + { + def Material "PreviewMat" + { + custom asset commerce:preview_image = @https://cdn.shopify.com/s/files/1/varitea/loose-leaf-original.jpg@ + custom string commerce:preview_alt = "Varitea loose-leaf original in a kraft pouch on linen" + token outputs:surface + } + } + + def Mesh "Placeholder" ( + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 1, 3, 2, 2, 3, 5, 4, 4, 5, 7, 6, 6, 7, 1, 0, 1, 7, 5, 3, 6, 0, 2, 4] + point3f[] points = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5)] + uniform token subdivisionScheme = "none" + rel material:binding = + custom bool commerce:placeholder = true + } + + variantSet "size" = { + "_50g" { + custom string commerce:option_value = "50g" + custom string commerce:sku = "VAR-LL-50" + custom string commerce:price = "14.00" + custom int commerce:inventory = 120 + custom string commerce:variant_id = "gid://shopify/ProductVariant/41001" + } + + "_100g" { + custom string commerce:option_value = "100g" + custom string commerce:sku = "VAR-LL-100" + custom string commerce:price = "24.00" + custom int commerce:inventory = 80 + custom string commerce:variant_id = "gid://shopify/ProductVariant/41002" + } + + "_250g" { + custom string commerce:option_value = "250g" + custom string commerce:sku = "VAR-LL-250" + custom string commerce:price = "52.00" + custom int commerce:inventory = 35 + custom string commerce:variant_id = "gid://shopify/ProductVariant/41003" + } + } +} diff --git a/packages/twin-renderer/test/parser.test.ts b/packages/twin-renderer/test/parser.test.ts new file mode 100644 index 0000000..5e32af2 --- /dev/null +++ b/packages/twin-renderer/test/parser.test.ts @@ -0,0 +1,126 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + getLayerCommerce, + getVariantSets, + parseUsda, + resolveVariantData, +} from "../src/parser/usda.js"; + +const FIXTURE = join(process.cwd(), "test/fixtures/varitea_product_twin.usda"); +const usda = readFileSync(FIXTURE, "utf8"); + +describe("parseUsda — Varitea fixture", () => { + const stage = parseUsda(usda); + + it("captures layer metadata + commerce:* customLayerData", () => { + expect(stage.version).toBe("1.0"); + expect(stage.metadata.defaultPrim).toBe("Product_varitea_loose_leaf_original"); + expect(stage.metadata.upAxis).toBe("Y"); + expect(stage.metadata.metersPerUnit).toBe(1); + + const layer = getLayerCommerce(stage); + expect(layer["commerce:shop"]).toBe("drinkvaritea.myshopify.com"); + expect(layer["commerce:generator"]).toBe("@shoploop/connector-shopify"); + expect(layer["commerce:shopify_id"]).toBe("gid://shopify/Product/7700112233445"); + }); + + it("resolves the root Xform prim with kind + variant metadata", () => { + const root = stage.root!; + expect(root.specifier).toBe("def"); + expect(root.typeName).toBe("Xform"); + expect(root.name).toBe("Product_varitea_loose_leaf_original"); + expect(root.metadata.kind).toBe("component"); + expect(root.metadata.variantSets).toEqual(["size"]); + expect(root.metadata.variants).toEqual({ size: "_50g" }); + }); + + it("reads root-level commerce + brand attributes with correct types", () => { + const attrs = Object.fromEntries(stage.root!.attributes.map((a) => [a.name, a.value])); + expect(attrs["commerce:title"]).toBe("Varitea Loose-Leaf — Original"); + expect(attrs["commerce:price"]).toBe("14.00"); + expect(attrs["commerce:sku"]).toBe("VAR-LL-50"); + expect(attrs["commerce:inventory"]).toBe(120); + expect(attrs["brand:tags"]).toEqual(["loose-leaf", "tea", "wellness", "cold-brew"]); + expect(attrs["brand:rules_ref"]).toBe("/etc/shoploop/brand_rules.json"); + }); + + it("parses the Looks scope, Material, and asset preview ref", () => { + const looks = stage.root!.children.find((c) => c.name === "Looks"); + expect(looks?.typeName).toBe("Scope"); + const mat = looks?.children.find((c) => c.name === "PreviewMat"); + expect(mat?.typeName).toBe("Material"); + const preview = mat?.attributes.find((a) => a.name === "commerce:preview_image")?.value; + expect(preview).toEqual({ + kind: "asset", + path: "https://cdn.shopify.com/s/files/1/varitea/loose-leaf-original.jpg", + }); + }); + + it("parses the placeholder Mesh with geometry + material binding rel", () => { + const mesh = stage.root!.children.find((c) => c.name === "Placeholder"); + expect(mesh?.typeName).toBe("Mesh"); + expect(mesh?.metadata.apiSchemas).toEqual(["MaterialBindingAPI"]); + expect(mesh?.attributes.find((a) => a.name === "commerce:placeholder")?.value).toBe(true); + const points = mesh?.attributes.find((a) => a.name === "points")?.value as number[][]; + expect(points).toHaveLength(8); + expect(points[0]).toEqual([-0.5, -0.5, 0.5]); + const binding = mesh?.relationships.find((r) => r.name === "material:binding"); + expect(binding?.target).toBe("/Product_varitea_loose_leaf_original/Looks/PreviewMat"); + }); + + it("exposes the size variant set with three options", () => { + const sets = getVariantSets(stage); + expect(sets).toHaveLength(1); + expect(sets[0].name).toBe("size"); + expect(sets[0].options).toEqual(["_50g", "_100g", "_250g"]); + expect(sets[0].default).toBe("_50g"); + }); + + it("merges variant-authored commerce data per selection", () => { + expect(resolveVariantData(stage, { size: "_50g" })["commerce:price"]).toBe("14.00"); + expect(resolveVariantData(stage, { size: "_100g" })["commerce:price"]).toBe("24.00"); + const big = resolveVariantData(stage, { size: "_250g" }); + expect(big["commerce:price"]).toBe("52.00"); + expect(big["commerce:sku"]).toBe("VAR-LL-250"); + expect(big["commerce:inventory"]).toBe(35); + expect(big["commerce:variant_id"]).toBe("gid://shopify/ProductVariant/41003"); + }); +}); + +describe("parseUsda — external reference + over subset", () => { + it("captures external GLB references on an Xform", () => { + const src = `#usda 1.0 +def Xform "Geometry" ( + prepend references = @https://cdn.example.com/tin.glb@ +) +{ + custom string commerce:mesh_source = "https://cdn.example.com/tin.glb" + custom token commerce:mesh_format = "glb" +} +`; + const stage = parseUsda(src); + expect(stage.root!.metadata.references).toEqual([ + { kind: "asset", path: "https://cdn.example.com/tin.glb" }, + ]); + }); + + it("parses `over` specifier and multi-variantSet arrays", () => { + const src = `#usda 1.0 +over "Product" ( + variantSets = ["size", "color"] + variants = { + string size = "m" + string color = "sage" + } +) +{ +} +`; + const stage = parseUsda(src); + expect(stage.root!.specifier).toBe("over"); + expect(stage.root!.metadata.variantSets).toEqual(["size", "color"]); + expect(stage.root!.metadata.variants).toEqual({ size: "m", color: "sage" }); + }); +}); diff --git a/packages/twin-renderer/tsconfig.build.json b/packages/twin-renderer/tsconfig.build.json new file mode 100644 index 0000000..2dd3057 --- /dev/null +++ b/packages/twin-renderer/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/twin-renderer/tsconfig.json b/packages/twin-renderer/tsconfig.json new file mode 100644 index 0000000..8dc5f85 --- /dev/null +++ b/packages/twin-renderer/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "types": ["vitest/globals", "node"] + }, + "include": ["src", "demo", "test", "vite.config.ts", "vitest.config.ts"] +} diff --git a/packages/twin-renderer/vite.config.ts b/packages/twin-renderer/vite.config.ts new file mode 100644 index 0000000..6a0d933 --- /dev/null +++ b/packages/twin-renderer/vite.config.ts @@ -0,0 +1,47 @@ +import { resolve } from "node:path"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import dts from "vite-plugin-dts"; + +// Two build targets share one config: +// default mode -> ESM library (src/index.ts), externalized peers, .d.ts emitted +// --mode demo -> standalone static site from demo/index.html +export default defineConfig(({ mode }) => { + if (mode === "demo") { + return { + plugins: [react()], + root: resolve(__dirname, "demo"), + publicDir: resolve(__dirname, "public"), + build: { + outDir: resolve(__dirname, "dist-demo"), + emptyOutDir: true, + }, + }; + } + + return { + plugins: [ + react(), + dts({ include: ["src"], rollupTypes: true, tsconfigPath: "./tsconfig.build.json" }), + ], + build: { + lib: { + entry: resolve(__dirname, "src/index.ts"), + formats: ["es"], + fileName: () => "index.js", + }, + rollupOptions: { + // Keep the bundle tiny and embeddable: the host app owns React + Three. + external: [ + "react", + "react-dom", + "react/jsx-runtime", + "three", + "@react-three/fiber", + "@react-three/drei", + "@react-three/postprocessing", + ], + }, + }, + }; +}); diff --git a/packages/twin-renderer/vitest.config.ts b/packages/twin-renderer/vitest.config.ts new file mode 100644 index 0000000..f82cfeb --- /dev/null +++ b/packages/twin-renderer/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + include: ["test/**/*.test.ts", "test/**/*.test.tsx"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6ac39ea --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4852 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + packages/animated-shorts: + dependencies: + '@remotion/bundler': + specifier: ^4.0.482 + version: 4.0.482(postcss@8.5.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/renderer': + specifier: ^4.0.482 + version: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/three': + specifier: ^4.0.482 + version: 4.0.482(@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(remotion@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(three@0.169.0) + '@shoploop/twin-renderer': + specifier: workspace:* + version: link:../twin-renderer + ajv: + specifier: ^8.17.1 + version: 8.18.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + remotion: + specifier: ^4.0.482 + version: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + three: + specifier: ^0.169.0 + version: 0.169.0 + devDependencies: + '@types/node': + specifier: ^20.14.0 + version: 20.19.43 + '@types/react': + specifier: ^18.3.0 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + '@types/three': + specifier: ^0.169.0 + version: 0.169.0 + tsx: + specifier: ^4.19.0 + version: 4.22.4 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@20.19.43)(jsdom@24.1.3)(terser@5.48.0) + + packages/twin-renderer: + dependencies: + '@react-three/drei': + specifier: ^9.114.0 + version: 9.122.0(@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(@types/react@18.3.31)(@types/three@0.169.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0)(use-sync-external-store@1.6.0(react@18.3.1)) + '@react-three/fiber': + specifier: ^8.17.10 + version: 8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0) + '@react-three/postprocessing': + specifier: ^2.16.3 + version: 2.19.1(@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(@types/three@0.169.0)(react@18.3.1)(three@0.169.0) + three: + specifier: ^0.169.0 + version: 0.169.0 + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + '@types/three': + specifier: ^0.169.0 + version: 0.169.0 + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.7.0(vite@5.4.21(@types/node@20.19.43)(terser@5.48.0)) + jsdom: + specifier: ^24.1.0 + version: 24.1.3 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: ^5.4.0 + version: 5.4.21(@types/node@20.19.43)(terser@5.48.0) + vite-plugin-dts: + specifier: ^4.2.0 + version: 4.5.4(@types/node@20.19.43)(rollup@4.62.2)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.43)(terser@5.48.0)) + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@20.19.43)(jsdom@24.1.3)(terser@5.48.0) + +packages: + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mediabunny/aac-encoder@1.47.0': + resolution: {integrity: sha512-JNzgdJoHMFFnv5imi1+dmjZMudsJ1zNCUCEkKjBh90cqGhAFg8xu4V2gsyxE2i6oq/YpWx23P+OvoANLSMcDzA==} + peerDependencies: + mediabunny: ^1.0.0 + + '@mediabunny/flac-encoder@1.47.0': + resolution: {integrity: sha512-VpKmJO0xlYcFCRD6JvJlMbNQ6d/6YMHdO1gFIqWlZABHjSSL6BquNNEgWSCv5vdF8ELDBwIYBVZEslakIB/7GA==} + peerDependencies: + mediabunny: ^1.0.0 + + '@mediabunny/mp3-encoder@1.47.0': + resolution: {integrity: sha512-JyzZyGeGRm2HVUQaGJ/VZT9OG+kG00mA8SLP/f3CO7+qWAmBncKc16WYXWHbZEo8Jn/ZGA6De32S5R2bTQbDqA==} + peerDependencies: + mediabunny: ^1.0.0 + + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + + '@microsoft/api-extractor-model@7.33.8': + resolution: {integrity: sha512-aIcoQggPyer3B6Ze3usz0YWC/oBwUHfRH5ETUsr+oT2BRA6SfTJl7IKPcPZkX4UR+PohowzW4uMxsvjrn8vm+w==} + + '@microsoft/api-extractor@7.58.9': + resolution: {integrity: sha512-S2UF4yza5GoxCmf7hJQNxJNZN9ltOVuOQv8Dy+Z21aol5ERoBNMdWcQHm4MJMPPItW4H/4rZD906iaf4mUojJA==} + hasBin: true + + '@microsoft/tsdoc-config@0.18.1': + resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@module-federation/error-codes@0.22.0': + resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} + + '@module-federation/runtime-core@0.22.0': + resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} + + '@module-federation/runtime-tools@0.22.0': + resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} + + '@module-federation/runtime@0.22.0': + resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} + + '@module-federation/sdk@0.22.0': + resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} + + '@module-federation/webpack-bundler-runtime@0.22.0': + resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} + + '@monogrid/gainmap-js@3.4.0': + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + + '@napi-rs/wasm-runtime@1.0.7': + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + + '@react-spring/animated@9.7.5': + resolution: {integrity: sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/core@9.7.5': + resolution: {integrity: sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/rafz@9.7.5': + resolution: {integrity: sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==} + + '@react-spring/shared@9.7.5': + resolution: {integrity: sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/three@9.7.5': + resolution: {integrity: sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==} + peerDependencies: + '@react-three/fiber': '>=6.0' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + three: '>=0.126' + + '@react-spring/types@9.7.5': + resolution: {integrity: sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==} + + '@react-three/drei@9.122.0': + resolution: {integrity: sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==} + peerDependencies: + '@react-three/fiber': ^8 + react: ^18 + react-dom: ^18 + three: '>=0.137' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@8.18.0': + resolution: {integrity: sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=18 <19' + react-dom: '>=18 <19' + react-native: '>=0.64' + three: '>=0.133' + 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@2.19.1': + resolution: {integrity: sha512-7P25LOSToH/I6b3UipNK17IIFlX4FDUmWcaomfwu82+CzhXTOz8Fcc1ZXEZ7vFA/5Fr/2peNlXgXZJvoa+aCdA==} + peerDependencies: + '@react-three/fiber': ^8.0 + react: ^18.0 + three: '>= 0.138.0' + + '@remotion/bundler@4.0.482': + resolution: {integrity: sha512-JlzZ38dLmL8Lb0YmOM2I7+j+VJAjrWi2GLDlxJS2c8vydslrFfaQaooNHEjECQcJyvpqFpskdsoX4FKBWUaQfQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/canvas-capture@4.0.482': + resolution: {integrity: sha512-Sleez48kA1aU0GSC4s39fYJBBhJWqlgRhHhimdRoanUhDxPQ5tMc1ildAI2G9pikC3fo4ifM6mJQhaLOZym/7w==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/compositor-darwin-arm64@4.0.482': + resolution: {integrity: sha512-0FQzK5HDdo7vzOdl2+5ybs4zpdoDn5J4Mqerm9DWzEcfbd4GkMbYdl1pDVUJf4tYoWgPT3xuy7+cegZpSMwgWA==} + cpu: [arm64] + os: [darwin] + + '@remotion/compositor-darwin-x64@4.0.482': + resolution: {integrity: sha512-oEctZwCLmzbz+JTjhY9tT0vYpl7GlPmlWKmyZCKKEiY95xIkKSTMQ4B5PZIIpkBbmG1bZAPfLs6CdCoMXz+6dw==} + cpu: [x64] + os: [darwin] + + '@remotion/compositor-linux-arm64-gnu@4.0.482': + resolution: {integrity: sha512-WaFx7kju3RQBxBiOIXN4dxzYLXMlyhmBivlqeM9vhaZmjBzhzVbKGr1eZzsXWPiknA5JSkZyVw512XdT3JXOSQ==} + cpu: [arm64] + os: [linux] + + '@remotion/compositor-linux-arm64-musl@4.0.482': + resolution: {integrity: sha512-K7pyxrt1/8I242SMfk8Hp6D9Wkzqg4zHuVZtHKq6bv8864YT8txUCQAz1Hw9K62gRoxdwcmsYKo9GxwFogewbA==} + cpu: [arm64] + os: [linux] + + '@remotion/compositor-linux-x64-gnu@4.0.482': + resolution: {integrity: sha512-ZzLwZW5PB+dicmg1Y8tBgDwNHv/467qQoDWI8ciTkqv8OzEKYaHM0hIr9kmoUPJB4ivKzy89qXVUul2mwYsiyA==} + cpu: [x64] + os: [linux] + + '@remotion/compositor-linux-x64-musl@4.0.482': + resolution: {integrity: sha512-T6czdbQi6H/LT5UtD8coJ9exLsvvcZJoXS5ndgJgRJh7es/A25aof40r/+D9JuikQAn1nT5PcFwbYH/hTMI+rQ==} + cpu: [x64] + os: [linux] + + '@remotion/compositor-win32-x64-msvc@4.0.482': + resolution: {integrity: sha512-KU67TFQzUqePcfreWsLgYvgU7DrtxbyBEqGXBFOTyUiKjdlOOFClUggrNYMHWAdWeJBhFo4VILo6ycecGKRcRg==} + cpu: [x64] + os: [win32] + + '@remotion/licensing@4.0.482': + resolution: {integrity: sha512-IjA9Wn2HTXsmmu26CcBfMSWMRI2iC3JpUlzASUPBkNEsB7xL28t/goNQ6vHWHpqr4HZ9mcYT+wQ3CYx9mxTR9A==} + + '@remotion/media-parser@4.0.482': + resolution: {integrity: sha512-YMGdnpkrE3fLsjGLJvPSaslvQ/cWJudIWehkxhboSZlBrTH7Yfg13vwZQbNhEa7PlvkEhj5ckG5ppaF+YDmQug==} + + '@remotion/media-utils@4.0.482': + resolution: {integrity: sha512-J3hx68/CFBzZ/0Sty53gHNXC9LpNrTWN0aZQ06r8/QfSA5x8fYuOK3A+4uERu43EOrXpKLsPsqQbR6fSODKNmw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/player@4.0.482': + resolution: {integrity: sha512-fJDTDX6uWdJg514BCaVbO2mKItRZ3ySvAiQxCNxL6mLhO/uW6S/9Pkk7bgCuez749x8+t4dDb8XXZTYMP09WfQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/renderer@4.0.482': + resolution: {integrity: sha512-YfZx8/lnCim3TbGGc9egFIz/rX6FKlC9vXUX//F9bZxkn+SaVjokluR7rpVW7UT6WjkPKK2GYkVtz9hjLYAIXA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/streaming@4.0.482': + resolution: {integrity: sha512-S2L8m5fnEiUuPqmXC6Y6bH1YbWqg071e1pgHEY3k7JvsvVC4pwrvnNY/gnEAkn9i5IglDGjKeHpjay1HjhUdmw==} + + '@remotion/studio-shared@4.0.482': + resolution: {integrity: sha512-hhR0YUtUhdVUWEPRS9VywYqj6n9Qd1mbkNwYaSg2QVhw2Df7DiVoDe0Dhu4tpHP9USs/zfKwXe83z+2jjF9C1Q==} + + '@remotion/studio@4.0.482': + resolution: {integrity: sha512-YK6kP8bCORIbGdERG/j9e5j21p1LG2wkAj2wEbdaOJA++ixr/hvEpexjIjQxA+fQBwDSDcYDxixBdbz3stkUaw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/three@4.0.482': + resolution: {integrity: sha512-eWtWtDhbteepJX2GdLrH+GZPso30ixur4ajG03DJ6mVzuiY96NBRbCjNxo3gLuavOs9t9AXULIhGLk8wMbxF7A==} + peerDependencies: + '@react-three/fiber': '>=8.0.0' + react: '>=16.8.0' + react-dom: '>=16.8.0' + remotion: 4.0.482 + three: '>=0.137.0' + + '@remotion/timeline-utils@4.0.482': + resolution: {integrity: sha512-0gXcu+w8hDIuCu5ft4QauKdYb/mE7cD2+sbqkiG656mhoSsqWp0Ks6wBnO5odd3DRp6x6zNOHI/VlRq2nqBtdw==} + + '@remotion/web-renderer@4.0.482': + resolution: {integrity: sha512-ZIIyIz9CBLFf1KsQgV0V4EAB3GSDFmLDTunZQj8h1J8SNxor1p11x9hQ7uo69jT/k3imeSCJlImPRjD378MXHw==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@remotion/zod-types@4.0.482': + resolution: {integrity: sha512-4J9SfDuypm7QTjjWsM9+0iro6Sym8FNuKG0Ayps/JZbuvtOUnbFCmzBLr+VYRHo+1jD9MikmnHNVzfLXWNOUvw==} + peerDependencies: + zod: 4.3.6 + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@rspack/binding-darwin-arm64@1.7.11': + resolution: {integrity: sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@1.7.11': + resolution: {integrity: sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@1.7.11': + resolution: {integrity: sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-arm64-musl@1.7.11': + resolution: {integrity: sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-x64-gnu@1.7.11': + resolution: {integrity: sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==} + cpu: [x64] + os: [linux] + + '@rspack/binding-linux-x64-musl@1.7.11': + resolution: {integrity: sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==} + cpu: [x64] + os: [linux] + + '@rspack/binding-wasm32-wasi@1.7.11': + resolution: {integrity: sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@1.7.11': + resolution: {integrity: sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@1.7.11': + resolution: {integrity: sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@1.7.11': + resolution: {integrity: sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==} + cpu: [x64] + os: [win32] + + '@rspack/binding@1.7.11': + resolution: {integrity: sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==} + + '@rspack/core@1.7.11': + resolution: {integrity: sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@rspack/lite-tapable@1.1.0': + resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} + + '@rspack/plugin-react-refresh@1.6.1': + resolution: {integrity: sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==} + peerDependencies: + react-refresh: '>=0.10.0 <1.0.0' + webpack-hot-middleware: 2.x + peerDependenciesMeta: + webpack-hot-middleware: + optional: true + + '@rushstack/node-core-library@5.23.1': + resolution: {integrity: sha512-wlKmIKIYCKuCASbITvOxLZXepPbwXvrv7S6ig6XNWFchSyhL/E2txmVXspHY49Wu2dzf7nI27a2k/yV5BA3EiA==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/problem-matcher@0.2.1': + resolution: {integrity: sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.7.3': + resolution: {integrity: sha512-aAA518n6wxxjCfnTAOjQnm7ngNE0FVHxHAw2pxKlIhxrMn0XQjGcXKF0oKWpjBgJOmsaJpVob/v+zr3zxgPWuA==} + + '@rushstack/terminal@0.24.0': + resolution: {integrity: sha512-8ZQS4MMaGsv27EXCBiH7WMPkRZrffeDoIevs6z9TM5dzqiY6+Hn4evfK/G+gvgBTjfvfkHIZPQQmalmI2sM4TQ==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.3.10': + resolution: {integrity: sha512-fwI076HYknC0IrMXdY6UmjDv+PH7NHhNJX3/pY2UblSE5XrXgndXZPiOe/6ZtuFpn6DvVDVNhtkIzQ+Qu/MhVQ==} + + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/dom-mediacapture-transform@0.1.11': + resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==} + + '@types/dom-webcodecs@0.1.13': + resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==} + + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react-reconciler@0.26.7': + resolution: {integrity: sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==} + + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.169.0': + resolution: {integrity: sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==} + + '@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@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/compiler-core@3.5.38': + resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==} + + '@vue/compiler-dom@3.5.38': + resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/shared@3.5.38': + resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@webgpu/types@0.1.71': + resolution: {integrity: sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.38: + resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + camera-controls@2.10.1: + resolution: {integrity: sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==} + peerDependencies: + three: '>=0.126.1' + + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + 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'} + + css-loader@7.1.4: + resolution: {integrity: sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 + webpack: ^5.27.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.378: + resolution: {integrity: sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==} + + enhanced-resolve@5.24.1: + resolution: {integrity: sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==} + engines: {node: '>=10.13.0'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fs-extra@11.3.5: + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + engines: {node: '>=14.14'} + + fs-monkey@1.0.3: + resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hls.js@1.6.16: + resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + its-fine@1.2.5: + resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} + peerDependencies: + react: '>=18.0' + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@24.1.3: + resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + local-pkg@1.2.1: + resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} + engines: {node: '>=14'} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + 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==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mediabunny@1.47.0: + resolution: {integrity: sha512-XQMZAcaKPkJ7hQ/Q2fvBdl3ZazQl2WVxDysUbJWh4PuAnLoerdsQBdPTDWdUdK6hh26LQ8Ue94MLLnmpWvlUYg==} + + memfs@3.4.3: + resolution: {integrity: sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==} + engines: {node: '>= 4.0.0'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.3: + resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + n8ao@1.10.2: + resolution: {integrity: sha512-gh4i0xFP8DiRaNoX75kPiG3kGl7PmX9SW/OIn3Sv/YZmHrA8faLdSU1MM6M4gKtfKpxDnZagodu8FKob5URCVw==} + peerDependencies: + postprocessing: '>=6.30.0' + three: '>=0.137' + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-releases@2.0.49: + resolution: {integrity: sha512-f06bl1D+8ZDkn2oOQQKAh5/otFWqVnM1Q5oerA8Pex7UfT66Tx4IPHIqVVFKqFT3FUtaDstdgkM7yT7JWhqxfw==} + engines: {node: '>=18'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nwsapi@2.2.24: + resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + postprocessing@6.39.1: + resolution: {integrity: sha512-R2dG2zy+BAx3USl5EHw+PvnrlbT5PKnZVp3se0HCR0pWH8WQdh742yNG4YWOsq6c0bFpffk0Gd2RqPeoP/wKng==} + peerDependencies: + three: '>= 0.168.0 < 0.185.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==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + react-composer@5.0.3: + resolution: {integrity: sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-reconciler@0.27.0: + resolution: {integrity: sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^18.0.0 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + 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@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + remotion@4.0.482: + resolution: {integrity: sha512-FfKxdxL9fW9UnY/WS9RLitevoDCf+fa5WZaNz4/CHhrXF1nc3SV+6GBp7td+dwvN1+UnP9t5DheIGvvoxL22NQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.21.0: + resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.5.3: + resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + 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==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + 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@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + style-loader@4.0.0: + resolution: {integrity: sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.27.0 + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + + three-mesh-bvh@0.7.8: + resolution: {integrity: sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==} + deprecated: Deprecated due to three.js version incompatibility. Please use v0.8.0, instead. + peerDependencies: + three: '>= 0.151.0' + + three-stdlib@2.36.1: + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + + three@0.169.0: + resolution: {integrity: sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + 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'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + 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 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + + 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==} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-sources@3.5.0: + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + engines: {node: '>=10.13.0'} + + webpack@5.105.0: + resolution: {integrity: sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + 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'} + hasBin: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zustand@3.7.2: + resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + + 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'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mediabunny/aac-encoder@1.47.0(mediabunny@1.47.0)': + dependencies: + mediabunny: 1.47.0 + + '@mediabunny/flac-encoder@1.47.0(mediabunny@1.47.0)': + dependencies: + mediabunny: 1.47.0 + + '@mediabunny/mp3-encoder@1.47.0(mediabunny@1.47.0)': + dependencies: + mediabunny: 1.47.0 + + '@mediapipe/tasks-vision@0.10.17': {} + + '@microsoft/api-extractor-model@7.33.8(@types/node@20.19.43)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.23.1(@types/node@20.19.43) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.58.9(@types/node@20.19.43)': + dependencies: + '@microsoft/api-extractor-model': 7.33.8(@types/node@20.19.43) + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.23.1(@types/node@20.19.43) + '@rushstack/rig-package': 0.7.3 + '@rushstack/terminal': 0.24.0(@types/node@20.19.43) + '@rushstack/ts-command-line': 5.3.10(@types/node@20.19.43) + diff: 8.0.4 + minimatch: 10.2.3 + resolve: 1.22.12 + semver: 7.7.4 + source-map: 0.6.1 + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.18.1': + dependencies: + '@microsoft/tsdoc': 0.16.0 + ajv: 8.18.0 + jju: 1.4.0 + resolve: 1.22.12 + + '@microsoft/tsdoc@0.16.0': {} + + '@module-federation/error-codes@0.22.0': {} + + '@module-federation/runtime-core@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/runtime-tools@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/webpack-bundler-runtime': 0.22.0 + + '@module-federation/runtime@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/runtime-core': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/sdk@0.22.0': {} + + '@module-federation/webpack-bundler-runtime@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@monogrid/gainmap-js@3.4.0(three@0.169.0)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.169.0 + + '@napi-rs/wasm-runtime@1.0.7': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@react-spring/animated@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/core@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.5(react@18.3.1) + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/rafz@9.7.5': {} + + '@react-spring/shared@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/rafz': 9.7.5 + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/three@9.7.5(@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(react@18.3.1)(three@0.169.0)': + dependencies: + '@react-spring/animated': 9.7.5(react@18.3.1) + '@react-spring/core': 9.7.5(react@18.3.1) + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + '@react-three/fiber': 8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0) + react: 18.3.1 + three: 0.169.0 + + '@react-spring/types@9.7.5': {} + + '@react-three/drei@9.122.0(@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(@types/react@18.3.31)(@types/three@0.169.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0)(use-sync-external-store@1.6.0(react@18.3.1))': + dependencies: + '@babel/runtime': 7.29.7 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.169.0) + '@react-spring/three': 9.7.5(@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(react@18.3.1)(three@0.169.0) + '@react-three/fiber': 8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0) + '@use-gesture/react': 10.3.1(react@18.3.1) + camera-controls: 2.10.1(three@0.169.0) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.16 + maath: 0.10.8(@types/three@0.169.0)(three@0.169.0) + meshline: 3.3.1(three@0.169.0) + react: 18.3.1 + react-composer: 5.0.3(react@18.3.1) + stats-gl: 2.4.2(@types/three@0.169.0)(three@0.169.0) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.169.0 + three-mesh-bvh: 0.7.8(three@0.169.0) + three-stdlib: 2.36.1(three@0.169.0) + troika-three-text: 0.52.4(three@0.169.0) + tunnel-rat: 0.1.2(@types/react@18.3.31)(react@18.3.1) + utility-types: 3.11.0 + zustand: 5.0.14(@types/react@18.3.31)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + - use-sync-external-store + + '@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@types/react-reconciler': 0.26.7 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 1.2.5(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + react-reconciler: 0.27.0(react@18.3.1) + react-use-measure: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + scheduler: 0.21.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.169.0 + zustand: 3.7.2(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + '@react-three/postprocessing@2.19.1(@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(@types/three@0.169.0)(react@18.3.1)(three@0.169.0)': + dependencies: + '@react-three/fiber': 8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0) + buffer: 6.0.3 + maath: 0.6.0(@types/three@0.169.0)(three@0.169.0) + n8ao: 1.10.2(postprocessing@6.39.1(three@0.169.0))(three@0.169.0) + postprocessing: 6.39.1(three@0.169.0) + react: 18.3.1 + three: 0.169.0 + three-stdlib: 2.36.1(three@0.169.0) + transitivePeerDependencies: + - '@types/three' + + '@remotion/bundler@4.0.482(postcss@8.5.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@remotion/media-parser': 4.0.482 + '@remotion/studio': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/studio-shared': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/timeline-utils': 4.0.482 + '@rspack/core': 1.7.11 + '@rspack/plugin-react-refresh': 1.6.1(react-refresh@0.18.0) + css-loader: 7.1.4(@rspack/core@1.7.11)(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.15)) + esbuild: 0.28.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-refresh: 0.18.0 + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + style-loader: 4.0.0(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.15)) + webpack: 5.105.0(esbuild@0.28.1)(postcss@8.5.15) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/helpers' + - '@swc/html' + - bufferutil + - clean-css + - cssnano + - csso + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + - utf-8-validate + - webpack-cli + - webpack-hot-middleware + + '@remotion/canvas-capture@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + mediabunny: 1.47.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@remotion/compositor-darwin-arm64@4.0.482': + optional: true + + '@remotion/compositor-darwin-x64@4.0.482': + optional: true + + '@remotion/compositor-linux-arm64-gnu@4.0.482': + optional: true + + '@remotion/compositor-linux-arm64-musl@4.0.482': + optional: true + + '@remotion/compositor-linux-x64-gnu@4.0.482': + optional: true + + '@remotion/compositor-linux-x64-musl@4.0.482': + optional: true + + '@remotion/compositor-win32-x64-msvc@4.0.482': + optional: true + + '@remotion/licensing@4.0.482': {} + + '@remotion/media-parser@4.0.482': {} + + '@remotion/media-utils@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + mediabunny: 1.47.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@remotion/player@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@remotion/renderer@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@remotion/licensing': 4.0.482 + '@remotion/streaming': 4.0.482 + execa: 5.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + source-map: 0.8.0-beta.0 + ws: 8.21.0 + optionalDependencies: + '@remotion/compositor-darwin-arm64': 4.0.482 + '@remotion/compositor-darwin-x64': 4.0.482 + '@remotion/compositor-linux-arm64-gnu': 4.0.482 + '@remotion/compositor-linux-arm64-musl': 4.0.482 + '@remotion/compositor-linux-x64-gnu': 4.0.482 + '@remotion/compositor-linux-x64-musl': 4.0.482 + '@remotion/compositor-win32-x64-msvc': 4.0.482 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@remotion/streaming@4.0.482': {} + + '@remotion/studio-shared@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - react + - react-dom + + '@remotion/studio@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@remotion/canvas-capture': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/media-utils': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/player': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/renderer': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/studio-shared': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/timeline-utils': 4.0.482 + '@remotion/web-renderer': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remotion/zod-types': 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@4.3.6) + mediabunny: 1.47.0 + memfs: 3.4.3 + open: 8.4.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + semver: 7.5.3 + zod: 4.3.6 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@remotion/three@4.0.482(@react-three/fiber@8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(remotion@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(three@0.169.0)': + dependencies: + '@react-three/fiber': 8.18.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.169.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + three: 0.169.0 + + '@remotion/timeline-utils@4.0.482': + dependencies: + mediabunny: 1.47.0 + + '@remotion/web-renderer@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@mediabunny/aac-encoder': 1.47.0(mediabunny@1.47.0) + '@mediabunny/flac-encoder': 1.47.0(mediabunny@1.47.0) + '@mediabunny/mp3-encoder': 1.47.0(mediabunny@1.47.0) + '@remotion/licensing': 4.0.482 + mediabunny: 1.47.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@remotion/zod-types@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@4.3.6)': + dependencies: + remotion: 4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: 4.3.6 + transitivePeerDependencies: + - react + - react-dom + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/pluginutils@5.4.0(rollup@4.62.2)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.62.2 + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@rspack/binding-darwin-arm64@1.7.11': + optional: true + + '@rspack/binding-darwin-x64@1.7.11': + optional: true + + '@rspack/binding-linux-arm64-gnu@1.7.11': + optional: true + + '@rspack/binding-linux-arm64-musl@1.7.11': + optional: true + + '@rspack/binding-linux-x64-gnu@1.7.11': + optional: true + + '@rspack/binding-linux-x64-musl@1.7.11': + optional: true + + '@rspack/binding-wasm32-wasi@1.7.11': + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + + '@rspack/binding-win32-arm64-msvc@1.7.11': + optional: true + + '@rspack/binding-win32-ia32-msvc@1.7.11': + optional: true + + '@rspack/binding-win32-x64-msvc@1.7.11': + optional: true + + '@rspack/binding@1.7.11': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.7.11 + '@rspack/binding-darwin-x64': 1.7.11 + '@rspack/binding-linux-arm64-gnu': 1.7.11 + '@rspack/binding-linux-arm64-musl': 1.7.11 + '@rspack/binding-linux-x64-gnu': 1.7.11 + '@rspack/binding-linux-x64-musl': 1.7.11 + '@rspack/binding-wasm32-wasi': 1.7.11 + '@rspack/binding-win32-arm64-msvc': 1.7.11 + '@rspack/binding-win32-ia32-msvc': 1.7.11 + '@rspack/binding-win32-x64-msvc': 1.7.11 + + '@rspack/core@1.7.11': + dependencies: + '@module-federation/runtime-tools': 0.22.0 + '@rspack/binding': 1.7.11 + '@rspack/lite-tapable': 1.1.0 + + '@rspack/lite-tapable@1.1.0': {} + + '@rspack/plugin-react-refresh@1.6.1(react-refresh@0.18.0)': + dependencies: + error-stack-parser: 2.1.4 + html-entities: 2.6.0 + react-refresh: 0.18.0 + + '@rushstack/node-core-library@5.23.1(@types/node@20.19.43)': + dependencies: + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-formats: 3.0.1(ajv@8.18.0) + fs-extra: 11.3.5 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.12 + semver: 7.7.4 + optionalDependencies: + '@types/node': 20.19.43 + + '@rushstack/problem-matcher@0.2.1(@types/node@20.19.43)': + optionalDependencies: + '@types/node': 20.19.43 + + '@rushstack/rig-package@0.7.3': + dependencies: + jju: 1.4.0 + resolve: 1.22.12 + + '@rushstack/terminal@0.24.0(@types/node@20.19.43)': + dependencies: + '@rushstack/node-core-library': 5.23.1(@types/node@20.19.43) + '@rushstack/problem-matcher': 0.2.1(@types/node@20.19.43) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 20.19.43 + + '@rushstack/ts-command-line@5.3.10(@types/node@20.19.43)': + dependencies: + '@rushstack/terminal': 0.24.0(@types/node@20.19.43) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + + '@tweenjs/tween.js@23.1.3': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/argparse@1.0.38': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/dom-mediacapture-transform@0.1.11': + dependencies: + '@types/dom-webcodecs': 0.1.13 + + '@types/dom-webcodecs@0.1.13': {} + + '@types/draco3d@1.4.10': {} + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.9 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@types/offscreencanvas@2019.7.3': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + + '@types/react-reconciler@0.26.7': + dependencies: + '@types/react': 18.3.31 + + '@types/react-reconciler@0.28.9(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + + '@types/react@18.3.31': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/stats.js@0.17.4': {} + + '@types/three@0.169.0': + dependencies: + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.71 + fflate: 0.8.3 + meshoptimizer: 0.18.1 + + '@types/webxr@0.5.24': {} + + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.43)(terser@5.48.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@20.19.43)(terser@5.48.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.43)(terser@5.48.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43)(terser@5.48.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.38': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.38 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.38': + dependencies: + '@vue/compiler-core': 3.5.38 + '@vue/shared': 3.5.38 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.0(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.38 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.38 + alien-signals: 0.4.14 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/shared@3.5.38': {} + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@webgpu/types@0.1.71': {} + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + acorn-import-phases@1.0.4(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + agent-base@7.1.4: {} + + ajv-draft-04@1.0.0(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@2.1.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@5.1.0(ajv@8.18.0): + dependencies: + ajv: 8.18.0 + fast-deep-equal: 3.1.3 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.4.14: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.38: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.4: + dependencies: + baseline-browser-mapping: 2.10.38 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.378 + node-releases: 2.0.49 + update-browserslist-db: 1.2.3(browserslist@4.28.4) + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + camera-controls@2.10.1(three@0.169.0): + dependencies: + three: 0.169.0 + + caniuse-lite@1.0.30001799: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + chrome-trace-event@1.0.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.3: {} + + compare-versions@6.1.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + convert-source-map@2.0.0: {} + + 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 + + css-loader@7.1.4(@rspack/core@1.7.11)(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.15)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.15) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.15) + postcss-modules-scope: 3.2.1(postcss@8.5.15) + postcss-modules-values: 4.0.0(postcss@8.5.15) + postcss-value-parser: 4.2.0 + semver: 7.7.4 + optionalDependencies: + '@rspack/core': 1.7.11 + webpack: 5.105.0(esbuild@0.28.1)(postcss@8.5.15) + + cssesc@3.0.0: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + define-lazy-prop@2.0.0: {} + + delayed-stream@1.0.0: {} + + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + + diff@8.0.4: {} + + draco3d@1.5.7: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.378: {} + + enhanced-resolve@5.24.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@6.0.1: {} + + entities@7.0.1: {} + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + expect-type@1.3.0: {} + + exsolve@1.1.0: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + + fflate@0.6.10: {} + + fflate@0.8.3: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fs-extra@11.3.5: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-monkey@1.0.3: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@6.0.1: {} + + glob-to-regexp@0.4.1: {} + + glsl-noise@0.0.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + hls.js@1.6.16: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-entities@2.6.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + + ieee754@1.2.1: {} + + immediate@3.0.6: {} + + import-lazy@4.0.0: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-docker@2.2.1: {} + + is-potential-custom-element-name@1.0.1: {} + + is-promise@2.2.2: {} + + is-stream@2.0.1: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + its-fine@1.2.5(@types/react@18.3.31)(react@18.3.1): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@18.3.31) + react: 18.3.1 + transitivePeerDependencies: + - '@types/react' + + jest-worker@27.5.1: + dependencies: + '@types/node': 20.19.43 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jju@1.4.0: {} + + js-tokens@4.0.0: {} + + jsdom@24.1.3: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.6 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.24 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + kolorist@1.8.0: {} + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + loader-runner@4.3.2: {} + + local-pkg@1.2.1: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.1 + quansync: 0.2.11 + + lodash.sortby@4.7.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + maath@0.10.8(@types/three@0.169.0)(three@0.169.0): + dependencies: + '@types/three': 0.169.0 + three: 0.169.0 + + maath@0.6.0(@types/three@0.169.0)(three@0.169.0): + dependencies: + '@types/three': 0.169.0 + three: 0.169.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mediabunny@1.47.0: + dependencies: + '@types/dom-mediacapture-transform': 0.1.11 + '@types/dom-webcodecs': 0.1.13 + + memfs@3.4.3: + dependencies: + fs-monkey: 1.0.3 + + merge-stream@2.0.0: {} + + meshline@3.3.1(three@0.169.0): + dependencies: + three: 0.169.0 + + meshoptimizer@0.18.1: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + minimatch@10.2.3: + dependencies: + brace-expansion: 5.0.6 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + + mlly@1.8.2: + dependencies: + acorn: 8.17.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + n8ao@1.10.2(postprocessing@6.39.1(three@0.169.0))(three@0.169.0): + dependencies: + postprocessing: 6.39.1(three@0.169.0) + three: 0.169.0 + + nanoid@3.3.15: {} + + neo-async@2.6.2: {} + + node-releases@2.0.49: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nwsapi@2.2.24: {} + + object-assign@4.1.1: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-browserify@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.1.0 + pathe: 2.0.3 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.15): + dependencies: + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-selector-parser: 7.1.4 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-selector-parser: 7.1.4 + + postcss-modules-values@4.0.0(postcss@8.5.15): + dependencies: + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 + + postcss-selector-parser@7.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postprocessing@6.39.1(three@0.169.0): + dependencies: + three: 0.169.0 + + potpack@1.0.2: {} + + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + querystringify@2.2.0: {} + + react-composer@5.0.3(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react-reconciler@0.27.0(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.21.0 + + react-refresh@0.17.0: {} + + react-refresh@0.18.0: {} + + react-use-measure@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + remotion@4.0.482(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.21.0: + dependencies: + loose-envify: 1.4.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) + + semver@6.3.1: {} + + semver@7.5.3: + dependencies: + lru-cache: 6.0.0 + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + sprintf-js@1.0.3: {} + + stackback@0.0.2: {} + + stackframe@1.3.4: {} + + stats-gl@2.4.2(@types/three@0.169.0)(three@0.169.0): + dependencies: + '@types/three': 0.169.0 + three: 0.169.0 + + stats.js@0.17.0: {} + + std-env@3.10.0: {} + + string-argv@0.3.2: {} + + strip-final-newline@2.0.0: {} + + style-loader@4.0.0(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.15)): + dependencies: + webpack: 5.105.0(esbuild@0.28.1)(postcss@8.5.15) + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + suspend-react@0.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + + symbol-tree@3.2.4: {} + + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.1(esbuild@0.28.1)(postcss@8.5.15)(webpack@5.105.0(postcss@8.5.15)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.48.0 + webpack: 5.105.0(esbuild@0.28.1)(postcss@8.5.15) + optionalDependencies: + esbuild: 0.28.1 + postcss: 8.5.15 + + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.17.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + three-mesh-bvh@0.7.8(three@0.169.0): + dependencies: + three: 0.169.0 + + three-stdlib@2.36.1(three@0.169.0): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.169.0 + + three@0.169.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + troika-three-text@0.52.4(three@0.169.0): + dependencies: + bidi-js: 1.0.3 + three: 0.169.0 + troika-three-utils: 0.52.4(three@0.169.0) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.169.0): + dependencies: + three: 0.169.0 + + troika-worker-utils@0.52.0: {} + + tslib@2.8.1: + optional: true + + tsx@4.22.4: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + tunnel-rat@0.1.2(@types/react@18.3.31)(react@18.3.1): + dependencies: + zustand: 4.5.7(@types/react@18.3.31)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + - react + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + undici-types@6.21.0: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + update-browserslist-db@1.2.3(browserslist@4.28.4): + dependencies: + browserslist: 4.28.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + utility-types@3.11.0: {} + + vite-node@2.1.9(@types/node@20.19.43)(terser@5.48.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@20.19.43)(terser@5.48.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-dts@4.5.4(@types/node@20.19.43)(rollup@4.62.2)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.43)(terser@5.48.0)): + dependencies: + '@microsoft/api-extractor': 7.58.9(@types/node@20.19.43) + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + '@volar/typescript': 2.4.28 + '@vue/language-core': 2.2.0(typescript@5.9.3) + compare-versions: 6.1.1 + debug: 4.4.3 + kolorist: 1.8.0 + local-pkg: 1.2.1 + magic-string: 0.30.21 + typescript: 5.9.3 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43)(terser@5.48.0) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + + vite@5.4.21(@types/node@20.19.43)(terser@5.48.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.62.2 + optionalDependencies: + '@types/node': 20.19.43 + fsevents: 2.3.3 + terser: 5.48.0 + + vitest@2.1.9(@types/node@20.19.43)(jsdom@24.1.3)(terser@5.48.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.43)(terser@5.48.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@20.19.43)(terser@5.48.0) + vite-node: 2.1.9(@types/node@20.19.43)(terser@5.48.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.43 + jsdom: 24.1.3 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vscode-uri@3.1.0: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + + webidl-conversions@4.0.2: {} + + webidl-conversions@7.0.0: {} + + webpack-sources@3.5.0: {} + + webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.15): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.17.0 + acorn-import-phases: 1.0.4(acorn@8.17.0) + browserslist: 4.28.4 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.1 + es-module-lexer: 2.1.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.2 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(esbuild@0.28.1)(postcss@8.5.15)(webpack@5.105.0(postcss@8.5.15)) + watchpack: 2.5.2 + webpack-sources: 3.5.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + zod@4.3.6: {} + + zustand@3.7.2(react@18.3.1): + optionalDependencies: + react: 18.3.1 + + zustand@4.5.7(@types/react@18.3.31)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + react: 18.3.1 + + zustand@5.0.14(@types/react@18.3.31)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.31 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..15add63 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "connectors/*" + - "packages/*"