commit 08aea85c2f04e318758496e196dc5d38837ceccc Author: balaji Date: Wed Jun 10 16:10:18 2026 +0530 Initial commit of VIZ 3D project diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6032a24 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +dist +.git +.env +.DS_Store diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7a550fe --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# GEMINI_API_KEY: Required for Gemini AI API calls. +# AI Studio automatically injects this at runtime from user secrets. +# Users configure this via the Secrets panel in the AI Studio UI. +GEMINI_API_KEY="MY_GEMINI_API_KEY" + +# APP_URL: The URL where this applet is hosted. +# AI Studio automatically injects this at runtime with the Cloud Run service URL. +# Used for self-referential links, OAuth callbacks, and API endpoints. +APP_URL="MY_APP_URL" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a86d2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +build/ +dist/ +coverage/ +.DS_Store +*.log +.env* +!.env.example diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f02387f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# Base image +FROM node:20-alpine AS base +WORKDIR /app +# Copy package.json and package-lock.json +COPY package*.json ./ +# Install dependencies +RUN npm install + +# Development stage +FROM base AS development +# Copy all project files +COPY . . +# Expose Vite's default port +EXPOSE 3000 +# Run the development server +CMD ["npm", "run", "dev"] + +# Build stage +FROM base AS build +COPY . . +RUN npm run build + +# Production stage +FROM nginx:alpine AS production +# Copy built assets from the build stage to Nginx +COPY --from=build /app/dist /usr/share/nginx/html +# Expose port 80 for Nginx +EXPOSE 80 +# Start Nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..70b4cfa --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/ad0ed820-1760-462d-bbc0-fffffb37ffda + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/antigravity_brain_backup.zip b/antigravity_brain_backup.zip new file mode 100644 index 0000000..0216fa5 Binary files /dev/null and b/antigravity_brain_backup.zip differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e5f9a97 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + web: + restart: always + build: + context: . + target: development + ports: + - "3000:3000" + volumes: + # Map the host source code to the container for hot-reloading + - .:/app + # Prevents the container's node_modules from being overwritten by the host + - /app/node_modules + environment: + - NODE_ENV=development + # Optional: ensure container runs smoothly on Windows host + stdin_open: true + tty: true diff --git a/index.html b/index.html new file mode 100644 index 0000000..ba694f9 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + viz-3D_hun + + + +
+ + + + diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..9dcd51b --- /dev/null +++ b/metadata.json @@ -0,0 +1,6 @@ +{ + "name": "VIZ 3D", + "description": "A professional 3D product visualization studio.", + "requestFramePermissions": [], + "majorCapabilities": [] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..204b720 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5086 @@ +{ + "name": "viz-3D_hun", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "viz-3D_hun", + "version": "0.0.0", + "dependencies": { + "@fal-ai/client": "^1.10.1", + "@google/genai": "^1.29.0", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", + "@react-three/postprocessing": "^3.0.4", + "@tailwindcss/vite": "^4.1.14", + "@types/three": "^0.183.1", + "@vitejs/plugin-react": "^5.0.4", + "clsx": "^2.1.1", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "fflate": "^0.8.2", + "framer-motion": "^12.38.0", + "lucide-react": "^0.546.0", + "postprocessing": "^6.39.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^3.5.0", + "three": "^0.183.2", + "three-stdlib": "^2.36.1", + "vite": "^6.2.0", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fal-ai/client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@fal-ai/client/-/client-1.10.1.tgz", + "integrity": "sha512-c3AVeH31OioiI2J1BfW8Cryi1DhUYldnY3X35nv6xLMq3fU2NQOo+eYaR5mL2O8MoHHh+HzXdQuIyanIyeq+ug==", + "license": "MIT", + "dependencies": { + "@msgpack/msgpack": "^3.0.0-beta2", + "eventsource-parser": "^1.1.2", + "robot3": "^0.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@google/genai": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.48.0.tgz", + "integrity": "sha512-plonYK4ML2PrxsRD9SeqmFt76eREWkQdPCglOA6aYDzL1AAbE+7PUnT54SvpWGfws13L0AZEqGSpL7+1IPnTxQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", + "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", + "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/postprocessing": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz", + "integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==", + "license": "MIT", + "dependencies": { + "maath": "^0.6.0", + "n8ao": "^1.9.4", + "postprocessing": "^6.36.6" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19.0", + "three": ">= 0.156.0" + } + }, + "node_modules/@react-three/postprocessing/node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.183.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", + "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~1.0.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", + "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", + "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001784", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", + "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz", + "integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.546.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz", + "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/n8ao": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz", + "integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postprocessing": { + "version": "6.39.0", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz", + "integrity": "sha512-/G6JY8hs426lcto/pBZlnFSkyEo1fHsh4gy7FPJtq1SaSUOzJgDW6f6f1K/+aMOYzK/eQEefyOb3++jPPIUeDA==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.168.0 < 0.184.0" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/robot3": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/robot3/-/robot3-0.4.1.tgz", + "integrity": "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==", + "license": "BSD-2-Clause" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/three": { + "version": "0.183.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", + "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "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 + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "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 + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/zustand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "license": "MIT", + "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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..758aba5 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "viz-3D_hun", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port=3000 --host=0.0.0.0", + "build": "vite build", + "preview": "vite preview", + "clean": "rm -rf dist", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@fal-ai/client": "^1.10.1", + "@google/genai": "^1.29.0", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", + "@react-three/postprocessing": "^3.0.4", + "@tailwindcss/vite": "^4.1.14", + "@types/three": "^0.183.1", + "@vitejs/plugin-react": "^5.0.4", + "clsx": "^2.1.1", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "fflate": "^0.8.2", + "framer-motion": "^12.38.0", + "lucide-react": "^0.546.0", + "postprocessing": "^6.39.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^3.5.0", + "three": "^0.183.2", + "three-stdlib": "^2.36.1", + "vite": "^6.2.0", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..b5a9edd --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,4775 @@ +import { useState, Suspense, useRef, useEffect, useMemo } from "react"; +import { Canvas } from "@react-three/fiber"; +import { OrbitControls, Environment, ContactShadows, PerspectiveCamera, OrthographicCamera, Float, MeshReflectorMaterial, TransformControls, Grid, useTexture } from "@react-three/drei"; +import { GLTFExporter } from "three-stdlib"; +import { + Plus, + Layers, + Palette, + Grid3X3 as TextureIcon, + Camera, + Sun, + Image as ImageIcon, + Settings, + Search, + ChevronDown, + Play, + Pause, + Maximize2, + Box, + Cpu, + Eye, + EyeOff, + Wrench, + Import, + Library, + Layout, + Video, + Share2, + BoxSelect, + Smartphone, + Monitor, + Menu, + Loader2, + FolderPlus, + Edit2, + Trash2, + Undo2, + Redo2, + Copy, + Clipboard, + RotateCw, + Save, + FolderOpen +} from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { ErrorBoundary } from "./components/ErrorBoundary"; +import { ModelViewer } from "./components/ModelViewer"; +import { JuiceBox } from "./components/JuiceBox"; +import { RenderModal, RenderSettings } from "./components/RenderModal"; +import { ImageTo3DModal } from "./components/ImageTo3DModal"; +import { UVEditor } from "./components/UVEditor"; +import { EffectComposer, DepthOfField, Bloom, Vignette, BrightnessContrast, HueSaturation } from "@react-three/postprocessing"; +import { cn } from "./lib/utils"; +import * as THREE from "three"; +import { useThree } from "@react-three/fiber"; +import * as fflate from "fflate"; +import { GLTFLoader, FBXLoader, OBJLoader, MTLLoader, DRACOLoader } from "three-stdlib"; +import { USDLoader } from "three/examples/jsm/loaders/USDLoader.js"; +import { USDZExporter } from "three/examples/jsm/exporters/USDZExporter.js"; + +const SidebarItem = ({ icon: Icon, label, active, onClick }: { icon: any, label: string, active?: boolean, onClick?: () => void }) => ( + +); + +const ToolbarButton = ({ icon: Icon, label, active }: { icon: any, label?: string, active?: boolean }) => ( + +); + +const MaterialCard = ({ color, name, active, onClick, roughness = 0.5, metalness = 0, transmission = 0 }: { + color: string, + name: string, + active?: boolean, + onClick?: () => void, + roughness?: number, + metalness?: number, + transmission?: number +}) => ( + +); + +interface MaterialProps { + color: string; + roughness: number; + metalness: number; + clearcoat: number; + transmission: number; + thickness: number; + ior: number; + sheen: number; + opacity: number; + specularIntensity: number; + emissive?: string; + emissiveIntensity?: number; + attenuationDistance?: number; + attenuationColor?: string; + map?: string | null; + normalMap?: string | null; + specularMap?: string | null; + alphaMap?: string | null; + uvScale?: number; + uvTiling?: [number, number]; + uvOffset?: [number, number]; + uvRotation?: number; + mappingType?: 'uv' | 'planar' | 'box' | 'cylinder' | 'sphere'; + uvLayers?: any[]; + uvBackground?: string; + uvTransparent?: boolean; +} + +interface AdditionalLight { + id: string; + type: 'point' | 'spot'; + position: [number, number, number]; + intensity: number; + color: string; + distance: number; + decay: number; + angle?: number; + penumbra?: number; + castShadow: boolean; +} + +interface TransformProps { + position: [number, number, number]; + rotation: [number, number, number]; + scale: [number, number, number]; +} + +interface SceneObject { + id: string; + name: string; + type: 'cube' | 'model' | 'group' | 'mesh'; + parentId: string | null; + visible: boolean; + materialProps?: MaterialProps; + transformProps?: TransformProps; + fillProps?: { + enabled: boolean; + type: 'solid' | 'liquid'; + height: number; + color: string; + }; +} + +const SceneSetup = () => { + const { gl } = useThree(); + useEffect(() => { + gl.shadowMap.type = THREE.PCFSoftShadowMap; + gl.localClippingEnabled = true; + }, [gl]); + return null; +}; + +const FixedBackplate = ({ url }: { url: string }) => { + const texture = useTexture(url); + const { scene } = useThree(); + + useEffect(() => { + if (texture) { + texture.colorSpace = THREE.SRGBColorSpace; + const originalBackground = scene.background; + scene.background = texture; + return () => { + scene.background = originalBackground; + }; + } + }, [scene, texture]); + + return null; +}; + +const ShadowFloor = ({ bounds, opacity, rotation, softness }: { bounds: any, opacity: number, rotation: number, softness: number }) => { + const alphaTexture = useMemo(() => { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + const context = canvas.getContext('2d'); + if (!context) return null; + + // Softness affects the inner radius and the falloff + // Higher softness = more gradual fade + const innerRadius = 0; + const outerRadius = 128; + const gradient = context.createRadialGradient(128, 128, innerRadius, 128, 128, outerRadius); + + // At softness 0, it's a sharper circle (but still a gradient) + // At softness 25, it's an extremely soft fade + const midPoint = Math.max(0.001, 0.5 * Math.exp(-(softness || 0.5) * 0.2)); + gradient.addColorStop(0, 'white'); + gradient.addColorStop(midPoint, 'white'); + gradient.addColorStop(1, 'black'); + + context.fillStyle = gradient; + context.fillRect(0, 0, 256, 256); + return new THREE.CanvasTexture(canvas); + }, [softness]); + + if (!bounds || !alphaTexture) return null; + + const planeSize = (bounds.maxDim || 5) * 5; + const posY = bounds.minY !== undefined ? bounds.minY : -0.5; + + return ( + + + + + ); +}; + +export default function App() { + const [activeSidebar, setActiveSidebar] = useState("Materials"); + const [theme, setTheme] = useState<"light" | "dark">("light"); + const [loadedModel, setLoadedModel] = useState(null); + const [blobURLs, setBlobURLs] = useState([]); + + // Cleanup blob URLs on unmount + useEffect(() => { + return () => { + blobURLs.forEach(url => URL.revokeObjectURL(url)); + }; + }, [blobURLs]); + + // Dispose old model when new one is loaded + useEffect(() => { + return () => { + if (loadedModel) { + loadedModel.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.geometry.dispose(); + if (Array.isArray(child.material)) { + child.material.forEach(m => m.dispose()); + } else { + child.material.dispose(); + } + } + }); + } + }; + }, [loadedModel]); + const [isLoading, setIsLoading] = useState(false); + const [importProgress, setImportProgress] = useState(0); + const [sceneObjects, setSceneObjects] = useState([]); + const [selectedIds, setSelectedIds] = useState([]); + const [editingId, setEditingId] = useState(null); + const [activeRightTab, setActiveRightTab] = useState("Scene"); + const [showUVEditor, setShowUVEditor] = useState(false); + const [showImageTo3D, setShowImageTo3D] = useState(false); + + // History State + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const historyRef = useRef([]); + const historyIndexRef = useRef(-1); + const [isUndoingRedoing, setIsUndoingRedoing] = useState(false); + const skipHistoryRef = useRef(false); + + // Sync refs with state + useEffect(() => { + historyRef.current = history; + historyIndexRef.current = historyIndex; + }, [history, historyIndex]); + + const pushToHistory = (newObjects: SceneObject[]) => { + if (isUndoingRedoing) return; + const currentHistory = historyRef.current; + const currentIndex = historyIndexRef.current; + + const newHistory = currentHistory.slice(0, currentIndex + 1); + newHistory.push(JSON.parse(JSON.stringify(newObjects))); + if (newHistory.length > 50) newHistory.shift(); // Limit history + + setHistory(newHistory); + setHistoryIndex(newHistory.length - 1); + }; + + const undo = () => { + const currentIndex = historyIndexRef.current; + if (currentIndex > 0) { + skipHistoryRef.current = true; + setIsUndoingRedoing(true); + const prevIndex = currentIndex - 1; + const prevObjects = JSON.parse(JSON.stringify(historyRef.current[prevIndex])); + setSceneObjects(prevObjects); + setHistoryIndex(prevIndex); + // Wait a bit longer to ensure the effect doesn't trigger + setTimeout(() => { setIsUndoingRedoing(false); skipHistoryRef.current = false; }, 100); + } + }; + + const redo = () => { + const currentIndex = historyIndexRef.current; + if (currentIndex < historyRef.current.length - 1) { + skipHistoryRef.current = true; + setIsUndoingRedoing(true); + const nextIndex = currentIndex + 1; + const nextObjects = JSON.parse(JSON.stringify(historyRef.current[nextIndex])); + setSceneObjects(nextObjects); + setHistoryIndex(nextIndex); + // Wait a bit longer to ensure the effect doesn't trigger + setTimeout(() => { setIsUndoingRedoing(false); skipHistoryRef.current = false; }, 100); + } + }; + + // Clipboard State + const [clipboard, setClipboard] = useState<{ + materialProps?: MaterialProps; + transformProps?: TransformProps; + } | null>(null); + + const [contextMenu, setContextMenu] = useState<{ + x: number; + y: number; + objectId: string; + } | null>(null); + + // Default material props for new objects or global edits + const [globalMaterialProps, setGlobalMaterialProps] = useState({ + color: "#ffffff", + roughness: 0.2, + metalness: 0.1, + clearcoat: 0, + transmission: 0, + thickness: 0, + ior: 1.5, + sheen: 0, + opacity: 1, + specularIntensity: 1, + emissive: "#000000", + emissiveIntensity: 0, + map: undefined, + normalMap: undefined, + specularMap: undefined, + alphaMap: undefined, + uvScale: 1, + uvTiling: [1, 1], + uvOffset: [0, 0], + uvRotation: 0, + mappingType: 'uv' + }); + + const [lightProps, setLightProps] = useState({ + intensity: 1, + color: "#ffffff", + shadowBias: -0.0005, + shadowRadius: 4 + }); + + const [additionalLights, setAdditionalLights] = useState([]); + + const [groundProps, setGroundProps] = useState({ + showShadow: true, + shadowIntensity: 0.4, + shadowRotation: 0, + shadowSoftness: 0.5, + shadowLength: 10 + }); + + const [selectedCategory, setSelectedCategory] = useState("plastics"); + const [envPreset, setEnvPreset] = useState("studio"); + const [envRotation, setEnvRotation] = useState(0); + const [customHdri, setCustomHdri] = useState(null); + const [backplateImage, setBackplateImage] = useState(null); + const [backgroundColor, setBackgroundColor] = useState("#444444"); // Default grey + const [backgroundType, setBackgroundType] = useState<"color" | "hdri" | "image">("color"); + const [showGrid, setShowGrid] = useState(true); + const [showRenderModal, setShowRenderModal] = useState(false); + const [renderRequest, setRenderRequest] = useState(null); + const [exportRequest, setExportRequest] = useState(false); + const [exportFormat, setExportFormat] = useState<'glb' | 'usdz'>('glb'); + // Export / Save dialog + const [exportDialog, setExportDialog] = useState<{ + mode: 'scene' | 'glb' | 'usdz'; + defaultName: string; + } | null>(null); + const [exportDialogName, setExportDialogName] = useState(''); + const [isRendering, setIsRendering] = useState(false); + const [importStatus, setImportStatus] = useState(null); + const [importError, setImportError] = useState(null); + const [modelData, setModelData] = useState<{ name: string, data: string, extension: string } | null>(null); + const [cameraProps, setCameraProps] = useState({ + fov: 35, + zoom: 1, + autoRotate: false, + orthographic: false, + target: [0, 0.5, 0] as [number, number, number], + position: [5, 3, 5] as [number, number, number], + pivot: [0, 0, 0] as [number, number, number], + cameraMode: "absolute" as "spherical" | "absolute", + spherical: { + distance: 7.68, + azimuth: 45, + inclination: 20, + twist: 0 + }, + walkthroughMode: false, + groundGrid: false, + depthOfField: { + enabled: false, + focusDistance: 0.05, + focalLength: 0.05, + bokehScale: 3 + }, + bloom: { + enabled: false, + intensity: 1.0, + luminanceThreshold: 0.9, + luminanceSmoothing: 0.025, + mipmapBlur: true + }, + vignette: { + enabled: false, + offset: 0.5, + darkness: 0.5 + }, + colorGrading: { + enabled: false, + brightness: 0, + contrast: 0, + hue: 0, + saturation: 0 + } + }); + + const fovToFocalLength = (fov: number) => { + return 18 / Math.tan((fov * Math.PI) / 360); + }; + + const focalLengthToFov = (focalLength: number) => { + return (2 * Math.atan(18 / focalLength) * 180) / Math.PI; + }; + + const setStandardView = (view: string) => { + if (!orbitControlsRef.current) return; + const controls = orbitControlsRef.current; + if (!controls.object || !controls.target) return; + const dist = controls.object.position.distanceTo(controls.target); + + const t = controls.target; + const target = [t.x || 0, t.y || 0, t.z || 0]; + let position: [number, number, number] = [0, 0, 0]; + + switch (view) { + case "front": position = [target[0], target[1], target[2] + dist]; break; + case "back": position = [target[0], target[1], target[2] - dist]; break; + case "top": position = [target[0], target[1] + dist, target[2]]; break; + case "bottom": position = [target[0], target[1] - dist, target[2]]; break; + case "left": position = [target[0] - dist, target[1], target[2]]; break; + case "right": position = [target[0] + dist, target[1], target[2]]; break; + case "isometric": position = [target[0] + dist, target[1] + dist, target[2] + dist]; break; + } + + if (position.some(v => isNaN(v))) return; + + controls.object.position.set(...position); + controls.update(); + + // Get new spherical + const azimuth = (controls.getAzimuthalAngle() || 0) * (180 / Math.PI); + const inclination = (Math.PI / 2 - (controls.getPolarAngle() || 0)) * (180 / Math.PI); + + setCameraProps(prev => ({ + ...prev, + position, + spherical: { + ...prev.spherical, + distance: dist, + azimuth, + inclination + } + })); + }; + const [cameraPresets, setCameraPresets] = useState([ + { id: 'default', name: 'Default View', fov: 35, zoom: 1, position: [3, 2, 5], target: [0, 0, 0] } + ]); + const orbitControlsRef = useRef(null); + const [modelBounds, setModelBounds] = useState<{ + center: THREE.Vector3; + size: THREE.Vector3; + maxDim: number; + minY: number; + radius: number; + } | null>(null); + + const controlsRef = useRef(null); + const directionalLightRef = useRef(null); + const cameraRef = useRef(null); + const juiceBoxRef = useRef(null); + const cubeRef = useRef(null); + const modelRef = useRef(null); + const transformRef = useRef(null); + const fileInputRef = useRef(null); + const hdriInputRef = useRef(null); + const backplateInputRef = useRef(null); + const textureInputRef = useRef(null); + const normalInputRef = useRef(null); + const specularInputRef = useRef(null); + const alphaInputRef = useRef(null); + + const [openMenu, setOpenMenu] = useState(null); + const sceneInputRef = useRef(null); + + const newScene = () => { + // Reset all scene state to defaults + setSceneObjects([]); + setSelectedIds([]); + setLoadedModel(null); + setModelData(null); + setModelBounds(null); + setHistory([]); + setHistoryIndex(-1); + setAdditionalLights([]); + setBackplateImage(null); + setCustomHdri(null); + setBackgroundColor('#444444'); + setBackgroundType('color'); + setEnvPreset('studio'); + setEnvRotation(0); + setGlobalMaterialProps({ + color: '#ffffff', roughness: 0.2, metalness: 0.1, clearcoat: 0, + transmission: 0, thickness: 0, ior: 1.5, sheen: 0, opacity: 1, + specularIntensity: 1, emissive: '#000000', emissiveIntensity: 0, + map: undefined, normalMap: undefined, specularMap: undefined, alphaMap: undefined, + uvScale: 1, uvTiling: [1, 1], uvOffset: [0, 0], uvRotation: 0, mappingType: 'uv' + }); + setLightProps({ intensity: 1, color: '#ffffff', shadowBias: -0.0005, shadowRadius: 4 }); + setGroundProps({ showShadow: true, shadowIntensity: 0.4, shadowRotation: 0, shadowSoftness: 0.5, shadowLength: 10 }); + setCameraProps(prev => ({ + ...prev, fov: 35, zoom: 1, autoRotate: false, orthographic: false, + position: [5, 3, 5], target: [0, 0.5, 0], pivot: [0, 0, 0], + spherical: { distance: 7.68, azimuth: 45, inclination: 20, twist: 0 }, + })); + setCameraPresets([{ id: 'default', name: 'Default View', fov: 35, zoom: 1, position: [3, 2, 5], target: [0, 0, 0] }]); + blobURLs.forEach(url => URL.revokeObjectURL(url)); + setBlobURLs([]); + setOpenMenu(null); + setShowUVEditor(false); + setIsRendering(false); + setImportStatus(null); + setImportError(null); + if (fileInputRef.current) fileInputRef.current.value = ''; + if (sceneInputRef.current) sceneInputRef.current.value = ''; + }; + + const saveScene = (name?: string) => { + const sceneData = { + sceneObjects, lightProps, groundProps, cameraProps, + envPreset, envRotation, backgroundColor, backgroundType, + customHdri, backplateImage, additionalLights, modelBounds, modelData, cameraPresets + }; + const filename = (name || 'scene').replace(/\.json$/i, ''); + const blob = new Blob([JSON.stringify(sceneData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${filename}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + setOpenMenu(null); + }; + + const openSaveDialog = () => { + const defaultName = modelData?.name ? modelData.name.replace(/\.[^.]+$/, '') : 'scene'; + setExportDialogName(defaultName); + setExportDialog({ mode: 'scene', defaultName }); + setOpenMenu(null); + }; + + const openExportDialog = (fmt: 'glb' | 'usdz') => { + const defaultName = modelData?.name ? modelData.name.replace(/\.[^.]+$/, '') : 'model'; + setExportDialogName(defaultName); + setExportDialog({ mode: fmt, defaultName }); + setOpenMenu(null); + }; + + const confirmExportDialog = () => { + if (!exportDialog) return; + const name = exportDialogName.trim() || exportDialog.defaultName; + if (exportDialog.mode === 'scene') { + saveScene(name); + } else { + setExportFormat(exportDialog.mode); + // Store name so ExportManager can use it + setExportFileName(name); + setExportRequest(true); + } + setExportDialog(null); + }; + + const [exportFileName, setExportFileName] = useState('model'); + + const loadScene = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = JSON.parse(e.target?.result as string); + if (data.sceneObjects) setSceneObjects(data.sceneObjects); + if (data.lightProps) setLightProps(data.lightProps); + if (data.groundProps) setGroundProps(data.groundProps); + if (data.cameraProps) { + const cProps = data.cameraProps; + setCameraProps(cProps); + + // Use a longer timeout and multiple attempts to ensure OrbitControls are updated + const updateControls = (attempts = 0) => { + if (orbitControlsRef.current) { + const controls = orbitControlsRef.current; + if (cProps.target) controls.target.set(cProps.target[0], cProps.target[1], cProps.target[2]); + if (cProps.position) { + controls.object.position.set(cProps.position[0], cProps.position[1], cProps.position[2]); + } + controls.update(); + } else if (attempts < 10) { + setTimeout(() => updateControls(attempts + 1), 100); + } + }; + updateControls(); + } + if (data.envPreset !== undefined) setEnvPreset(data.envPreset); + if (data.envRotation !== undefined) setEnvRotation(data.envRotation); + if (data.backgroundColor) setBackgroundColor(data.backgroundColor); + if (data.backgroundType) setBackgroundType(data.backgroundType); + if (data.customHdri) setCustomHdri(data.customHdri); + if (data.backplateImage) setBackplateImage(data.backplateImage); + if (data.additionalLights) setAdditionalLights(data.additionalLights); + if (data.modelBounds) setModelBounds(data.modelBounds); + if (data.cameraPresets) setCameraPresets(data.cameraPresets); + + // Re-load model if data exists + if (data.modelData && data.modelData.data) { + reImportModel(data.modelData); + } + } catch (err) { + console.error("Failed to load scene", err); + } + }; + reader.readAsText(file); + setOpenMenu(null); + // Clear input so same file can be loaded again + event.target.value = ''; + }; + + const reImportModel = async (modelInfo: { name: string, data: string, extension: string }) => { + setIsLoading(true); + setImportStatus("Restoring model..."); + try { + const response = await fetch(modelInfo.data); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + + const manager = new THREE.LoadingManager(); + let object: THREE.Object3D | null = null; + const extension = modelInfo.extension; + + if (extension === 'glb' || extension === 'gltf') { + const loader = new GLTFLoader(manager); + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/'); + loader.setDRACOLoader(dracoLoader); + const gltf = await loader.loadAsync(url); + object = gltf.scene; + dracoLoader.dispose(); + } else if (extension === 'fbx') { + const loader = new FBXLoader(manager); + object = await loader.loadAsync(url); + } else if (extension === 'obj') { + const loader = new OBJLoader(manager); + object = await loader.loadAsync(url); + } else if (['usdz', 'usd', 'usda', 'usdc'].includes(extension)) { + const loader = new USDLoader(manager); + object = await loader.loadAsync(url); + } + + if (object) { + object.visible = true; + // Setup shadows + object.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.castShadow = true; + child.receiveShadow = true; + child.frustumCulled = false; // Prevent flickering on large models + } + }); + setLoadedModel(object); + setBlobURLs(prev => [...prev, url]); + } + setIsLoading(false); + } catch (error) { + console.error('Error re-importing model:', error); + setIsLoading(false); + } + }; + + const handleTextureUpload = (event: React.ChangeEvent, type: 'map' | 'normalMap' | 'specularMap' | 'alphaMap' = 'map') => { + const file = event.target.files?.[0]; + if (!file || !selectedObject) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const textureUrl = e.target?.result as string; + updateObjectMaterial(selectedObject.id, { [type]: textureUrl }); + }; + reader.readAsDataURL(file); + }; + + const handleHdriUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const hdriUrl = e.target?.result as string; + setCustomHdri(hdriUrl); + setEnvPreset(null); // Clear preset when custom HDRI is used + }; + reader.readAsDataURL(file); + }; + + const removeTexture = () => { + if (selectedObject) { + updateObjectMaterial(selectedObject.id, { map: null }); + if (textureInputRef.current) textureInputRef.current.value = ""; + } + }; + + const removeNormalMap = () => { + if (selectedObject) { + updateObjectMaterial(selectedObject.id, { normalMap: null }); + if (normalInputRef.current) normalInputRef.current.value = ""; + } + }; + + const removeSpecularMap = () => { + if (selectedObject) { + updateObjectMaterial(selectedObject.id, { specularMap: null }); + if (specularInputRef.current) specularInputRef.current.value = ""; + } + }; + + const removeAlphaMap = () => { + if (selectedObject) { + updateObjectMaterial(selectedObject.id, { alphaMap: null }); + if (alphaInputRef.current) alphaInputRef.current.value = ""; + } + }; + + const saveCameraPreset = (name: string) => { + if (!orbitControlsRef.current) return; + const controls = orbitControlsRef.current; + if (!controls.object || !controls.target) return; + + // Get current camera position and target + const p = controls.object.position; + const t = controls.target; + const position = [p.x || 0, p.y || 0, p.z || 0]; + const target = [t.x || 0, t.y || 0, t.z || 0]; + + const newPreset = { + id: Math.random().toString(36).substr(2, 9), + name, + fov: cameraProps.fov, + zoom: cameraProps.zoom, + autoRotate: cameraProps.autoRotate, + orthographic: cameraProps.orthographic, + cameraMode: cameraProps.cameraMode, + spherical: { ...cameraProps.spherical }, + position, + target + }; + + setCameraPresets(prev => [...prev, newPreset]); + }; + + const loadCameraPreset = (preset: any) => { + if (!orbitControlsRef.current) return; + const controls = orbitControlsRef.current; + + setCameraProps(prev => ({ + ...prev, + fov: preset.fov, + zoom: preset.zoom, + autoRotate: preset.autoRotate || false, + orthographic: preset.orthographic || false, + cameraMode: preset.cameraMode || "absolute", + spherical: preset.spherical || { distance: 6.326, azimuth: -91.475, inclination: -0.393, twist: 0 }, + target: preset.target || [0, 0, 0], + position: preset.position || [3, 2, 5], + pivot: preset.target || [0, 0, 0], + walkthroughMode: false, + groundGrid: false, + depthOfField: { enabled: false, focusDistance: 0.01, focalLength: 0.02, bokehScale: 2 } + })); + + // We need to set the camera position and the controls target + const p = preset.position; + const t = preset.target; + controls.object.position.set(p[0], p[1], p[2]); + controls.target.set(t[0], t[1], t[2]); + controls.update(); + }; + + // Keyboard Shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Don't intercept if user is typing in an input + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + return; + } + + // Prevent default for common shortcuts + if (e.ctrlKey || e.metaKey) { + switch (e.key.toLowerCase()) { + case 'n': + e.preventDefault(); + newScene(); + break; + case 's': + e.preventDefault(); + openSaveDialog(); + break; + case 'o': + e.preventDefault(); + sceneInputRef.current?.click(); + break; + case 'i': + e.preventDefault(); + fileInputRef.current?.click(); + break; + case 'e': + e.preventDefault(); + setExportRequest(true); + break; + case 'z': + e.preventDefault(); + if (e.shiftKey) { + redo(); + } else { + undo(); + } + break; + case 'y': + e.preventDefault(); + redo(); + break; + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + const RenderManager = () => { + const { gl, scene, camera } = useThree(); + + useEffect(() => { + if (!renderRequest) return; + const { width, height, includeAlpha, format, name } = renderRequest; + + setIsRendering(true); + + // Use an offscreen render target so we get exactly the requested resolution + // without resizing the visible canvas (which can produce multiple download events) + const renderFrame = () => { + // Save original state + const originalBackground = scene.background; + const originalToneMapping = gl.toneMapping; + const originalToneMappingExposure = gl.toneMappingExposure; + + // Create offscreen render target at exact requested size + const rt = new THREE.WebGLRenderTarget(width, height, { + minFilter: THREE.LinearFilter, + magFilter: THREE.LinearFilter, + format: includeAlpha ? THREE.RGBAFormat : THREE.RGBFormat, + colorSpace: THREE.SRGBColorSpace, + }); + + if (includeAlpha) { + scene.background = null; + } + + // Render scene into the offscreen target + gl.setRenderTarget(rt); + gl.render(scene, camera); + gl.setRenderTarget(null); + + // Read pixels from the render target + const pixels = new Uint8Array(width * height * 4); + gl.readRenderTargetPixels(rt, 0, 0, width, height, pixels); + + // Flip vertically (WebGL reads bottom-to-top) + const flipped = new Uint8Array(width * height * 4); + for (let row = 0; row < height; row++) { + const src = (height - 1 - row) * width * 4; + const dst = row * width * 4; + flipped.set(pixels.subarray(src, src + width * 4), dst); + } + + // Draw into a 2D canvas and export + const offscreen = document.createElement('canvas'); + offscreen.width = width; + offscreen.height = height; + const ctx = offscreen.getContext('2d')!; + const imageData = ctx.createImageData(width, height); + imageData.data.set(flipped); + ctx.putImageData(imageData, 0, 0); + + const mimeType = format === 'jpg' ? 'image/jpeg' : 'image/png'; + const dataUrl = offscreen.toDataURL(mimeType, 0.95); + const link = document.createElement('a'); + link.download = `${name}.${format}`; + link.href = dataUrl; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Restore + rt.dispose(); + scene.background = originalBackground; + gl.toneMapping = originalToneMapping; + gl.toneMappingExposure = originalToneMappingExposure; + + setRenderRequest(null); + setShowRenderModal(false); + setIsRendering(false); + }; + + // Wait for React to hide UI elements before capturing + setTimeout(renderFrame, 150); + }, [renderRequest, gl, scene, camera]); + + return null; + }; + + const ExportManager = () => { + const { scene } = useThree(); + + useEffect(() => { + if (exportRequest) { + if (exportFormat === 'glb') { + const exporter = new GLTFExporter(); + const exportScene = scene.clone(); + + // Collect things to remove from exportScene + const toRemove: THREE.Object3D[] = []; + exportScene.traverse((child: any) => { + if ( + child instanceof THREE.Camera || + child instanceof THREE.Light || + child instanceof THREE.GridHelper || + child instanceof THREE.PlaneHelper || + child.type === 'GridHelper' || + child.type === 'DirectionalLightHelper' || + child.name === '__background__' || + child.type === 'TransformControls' || + (child.name && child.name.includes('TransformControls')) || + (child.name && child.name.includes('Grid')) || + // Exclude Floor/Shadow plane + (child instanceof THREE.Mesh && child.geometry && child.geometry.type === 'PlaneGeometry') || + // Exclude TransformControls gizmo meshes + (child.parent && child.parent.type === 'TransformControls') + ) { + toRemove.push(child); + } + }); + + toRemove.forEach(c => c.removeFromParent()); + + exporter.parse( + exportScene, + (result) => { + const blob = new Blob([result as ArrayBuffer], { type: 'application/octet-stream' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + const fname = (exportFileName || 'model').replace(/\.glb$/i, ''); + link.download = `${fname}.glb`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + setExportRequest(false); + }, + (error) => { + console.error('GLB export error:', error); + setExportRequest(false); + }, + { binary: true, trs: true, onlyVisible: true, truncateDrawRange: true } + ); + } else if (exportFormat === 'usdz') { + const exporter = new USDZExporter(); + const exportScene = scene.clone(); + const toRemove: THREE.Object3D[] = []; + exportScene.traverse((child: any) => { + if ( + child instanceof THREE.Camera || + child instanceof THREE.Light || + child instanceof THREE.GridHelper || + child instanceof THREE.PlaneHelper || + child.type === 'GridHelper' || + child.type === 'DirectionalLightHelper' || + child.name === '__background__' || + child.type === 'TransformControls' || + (child.name && child.name.includes('TransformControls')) || + (child.name && child.name.includes('Grid')) || + (child instanceof THREE.Mesh && child.geometry && child.geometry.type === 'PlaneGeometry') || + (child.parent && child.parent.type === 'TransformControls') + ) { + toRemove.push(child); + } + }); + toRemove.forEach(c => c.removeFromParent()); + + exporter.parse( + exportScene, + (result) => { + const blob = new Blob([result], { type: 'model/vnd.usdz+zip' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + const fname = (exportFileName || 'model').replace(/\.usdz$/i, ''); + link.download = `${fname}.usdz`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + setExportRequest(false); + }, + (error) => { + console.error('USDZ export error:', error); + setExportRequest(false); + } + ); + } + } + }, [exportRequest, exportFormat, scene]); + + return null; + }; + + const loadBackplate = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + const result = e.target?.result; + if (typeof result === 'string') { + setBackplateImage(result); + setBackgroundType("image"); + } + }; + reader.readAsDataURL(file); + }; + + const handleImport = async (event: React.ChangeEvent) => { + const files = event.target.files; + if (!files || files.length === 0) return; + + setIsLoading(true); + setImportProgress(0); + setImportStatus("Preparing files..."); + setImportError(null); + + const fileMap: { [key: string]: string } = {}; + const currentBlobURLs: string[] = []; + let rootFile: { name: string, url: string } | null = null; + + try { + const processFile = (name: string, data: Uint8Array | Blob) => { + const blob = data instanceof Blob ? data : new Blob([data]); + const url = URL.createObjectURL(blob); + // Store both full name and just the filename for better matching + fileMap[name] = url; + const fileName = name.split('/').pop() || name; + fileMap[fileName] = url; + currentBlobURLs.push(url); + + const ext = name.split('.').pop()?.toLowerCase(); + if (!rootFile && ['glb', 'gltf', 'fbx', 'obj', 'usdz', 'usd', 'usda', 'usdc'].includes(ext || '')) { + rootFile = { name, url }; + } + }; + + // Handle Zip or Multiple Files + if (files.length === 1 && files[0].name.endsWith('.zip')) { + setImportStatus("Unzipping archive..."); + const buffer = await files[0].arrayBuffer(); + const unzipped = fflate.unzipSync(new Uint8Array(buffer)); + for (const name in unzipped) { + processFile(name, unzipped[name]); + } + } else { + for (let i = 0; i < files.length; i++) { + const file = files[i]; + processFile(file.name, file); + } + } + + if (!rootFile) { + throw new Error("No supported 3D model file found. Please select a .glb, .gltf, .fbx, .obj, or .usdz file."); + } + + setImportStatus("Parsing 3D data..."); + const extension = rootFile.name.split('.').pop()?.toLowerCase(); + + const manager = new THREE.LoadingManager(); + manager.setURLModifier((url) => { + const fileName = url.split('/').pop() || url; + // Try to find the file in our map + if (fileMap[fileName]) return fileMap[fileName]; + if (fileMap[url]) return fileMap[url]; + + // Handle cases where the path might be slightly different + const decodedUrl = decodeURIComponent(url); + const decodedFileName = decodedUrl.split('/').pop() || decodedUrl; + if (fileMap[decodedFileName]) return fileMap[decodedFileName]; + + return url; + }); + + let object: THREE.Object3D | null = null; + + if (extension === 'glb' || extension === 'gltf') { + const loader = new GLTFLoader(manager); + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/'); + loader.setDRACOLoader(dracoLoader); + const gltf = await loader.loadAsync(rootFile.url); + object = gltf.scene; + // Dispose draco loader after use to avoid memory issues + dracoLoader.dispose(); + } else if (extension === 'fbx') { + const loader = new FBXLoader(manager); + object = await loader.loadAsync(rootFile.url); + } else if (extension === 'obj') { + const loader = new OBJLoader(manager); + // Look for companion .mtl file + const mtlFile = Object.keys(fileMap).find(name => name.toLowerCase().endsWith('.mtl')); + if (mtlFile) { + const mtlLoader = new MTLLoader(manager); + try { + const materials = await mtlLoader.loadAsync(fileMap[mtlFile]); + materials.preload(); + loader.setMaterials(materials); + } catch (err) { + console.warn("Failed to load MTL file:", err); + } + } + object = await loader.loadAsync(rootFile.url); + } else if (['usdz', 'usd', 'usda', 'usdc'].includes(extension || '')) { + const loader = new USDLoader(manager); + object = await loader.loadAsync(rootFile.url); + } + + if (object) { + // Save model data for scene persistence + const modelBlob = await fetch(rootFile.url).then(r => r.blob()); + const modelBase64 = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.readAsDataURL(modelBlob); + }); + setModelData({ name: rootFile.name, data: modelBase64, extension: extension || '' }); + + setImportStatus("Optimizing geometry..."); + // 1. Calculate model bounds + const boundingBox = new THREE.Box3().setFromObject(object); + const center = new THREE.Vector3(); + boundingBox.getCenter(center); + const size = new THREE.Vector3(); + boundingBox.getSize(size); + const maxDim = Math.max(size.x || 0, size.y || 0, size.z || 0) || 1; + const radius = maxDim / 2; + const minY = boundingBox.min.y; + + setModelBounds({ center, size, maxDim, minY, radius }); + + // 2. Adjust Directional Light Shadow Camera + if (directionalLightRef.current) { + const light = directionalLightRef.current; + light.shadow.camera.left = -maxDim; + light.shadow.camera.right = maxDim; + light.shadow.camera.top = maxDim; + light.shadow.camera.bottom = -maxDim; + light.shadow.camera.near = 0.1; + light.shadow.camera.far = maxDim * 5; + light.shadow.camera.updateProjectionMatrix(); + light.shadow.mapSize.width = 2048; + light.shadow.mapSize.height = 2048; + } + + // 3. Auto-framing Camera + let cameraZDistance = 0; + if (cameraRef.current) { + const camera = cameraRef.current; + const fovRadians = camera.fov * (Math.PI / 180); + cameraZDistance = radius / Math.sin(fovRadians / 2); + const padding = 1.5; + cameraZDistance *= padding; + + camera.position.set( + center.x, + center.y + (maxDim / 4), + center.z + cameraZDistance + ); + camera.near = radius / 100; + camera.far = radius * 100; + camera.updateProjectionMatrix(); + camera.lookAt(center); + } + + // 4. Update OrbitControls + if (orbitControlsRef.current) { + const controls = orbitControlsRef.current; + if (center) controls.target.copy(center); + controls.minDistance = radius / 2; + controls.maxDistance = (cameraZDistance || radius * 10) * 5; + controls.update(); + } + + const mainModelId = `model-${Date.now()}`; + const newSceneObjects: SceneObject[] = [ + { + id: mainModelId, + name: rootFile.name, + type: 'model', + parentId: null, + visible: true, + transformProps: { + position: [0, 0, 0], + rotation: [0, 0, 0], + scale: [1, 1, 1] + } + } + ]; + + object.visible = true; + let mIdx = 0; + object.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.castShadow = true; + child.receiveShadow = true; + child.frustumCulled = false; + const meshId = `mesh-${mIdx}`; + mIdx++; + + // Extract mesh material properties if available + const material = Array.isArray(child.material) ? child.material[0] : child.material; + const mProps = { ...globalMaterialProps }; + + if (material) { + if (material.color) mProps.color = `#${material.color.getHexString()}`; + if (material.roughness !== undefined) mProps.roughness = material.roughness; + if (material.metalness !== undefined) mProps.metalness = material.metalness; + if (material.opacity !== undefined) mProps.opacity = material.opacity; + + // Standard material props + const m = material as any; + if (m.transmission !== undefined) mProps.transmission = m.transmission; + if (m.ior !== undefined) mProps.ior = m.ior; + if (m.thickness !== undefined) mProps.thickness = m.thickness; + if (m.clearcoat !== undefined) mProps.clearcoat = m.clearcoat; + if (m.sheen !== undefined) mProps.sheen = m.sheen; + if (m.specularIntensity !== undefined) mProps.specularIntensity = m.specularIntensity; + } + + newSceneObjects.push({ + id: meshId, + name: child.name || `Mesh ${meshId.slice(0, 4)}`, + type: 'mesh', + parentId: mainModelId, + visible: true, + transformProps: { + position: [child.position?.x || 0, child.position?.y || 0, child.position?.z || 0], + rotation: [child.rotation?.x || 0, child.rotation?.y || 0, child.rotation?.z || 0], + scale: [child.scale?.x || 1, child.scale?.y || 1, child.scale?.z || 1] + }, + materialProps: mProps + }); + } + }); + + setLoadedModel(object); + setBlobURLs(prev => { + prev.forEach(url => URL.revokeObjectURL(url)); + return currentBlobURLs; + }); + setSceneObjects(prev => [ + ...prev.filter(obj => obj.type !== 'cube'), + ...newSceneObjects + ]); + setSelectedIds([mainModelId]); + + setTimeout(() => { + setIsLoading(false); + setImportProgress(0); + }, 500); + } + } catch (error) { + console.error('Error loading model:', error); + setImportError(error instanceof Error ? error.message : "Failed to load model."); + setIsLoading(false); + setImportProgress(0); + } + }; + + const toggleSelect = (id: string, multi: boolean) => { + if (multi) { + setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]); + } else { + setSelectedIds([id]); + } + }; + + const renameObject = (id: string, newName: string) => { + updateSceneObjects(prev => prev.map(obj => obj.id === id ? { ...obj, name: newName } : obj)); + setEditingId(null); + }; + + const groupObjects = () => { + if (selectedIds.length < 2) return; + const groupId = `group-${Date.now()}`; + const groupName = "New Group"; + + updateSceneObjects(prev => [ + ...prev.map(obj => selectedIds.includes(obj.id) ? { ...obj, parentId: groupId } : obj), + { id: groupId, name: groupName, type: 'group', parentId: null, visible: true } + ]); + setSelectedIds([groupId]); + }; + + const deleteObjects = () => { + updateSceneObjects(prev => prev.filter(obj => !selectedIds.includes(obj.id))); + setSelectedIds([]); + }; + + const materialCategories = [ + { + id: "plastics", + name: "Plastics & Polymers", + items: [ + { name: "Matte ABS", color: "#64748b", roughness: 0.8, metalness: 0, specularIntensity: 0.2 }, + { name: "Glossy PVC", color: "#e2e8f0", roughness: 0.05, metalness: 0, specularIntensity: 1 }, + { name: "Polycarbonate", color: "#f8fafc", roughness: 0.05, metalness: 0, transmission: 0.95, ior: 1.58, thickness: 1 }, + { name: "Textured Polypropylene", color: "#334155", roughness: 0.6, metalness: 0, clearcoat: 0.1 }, + { name: "Frosted Acrylic", color: "#ffffff", roughness: 0.5, metalness: 0, transmission: 0.9, ior: 1.49, thickness: 0.5 }, + { name: "Translucent Silicone", color: "#cbd5e1", roughness: 0.7, metalness: 0, transmission: 0.4, ior: 1.4, thickness: 2, sheen: 0.3 }, + { name: "Soft-Touch Elastomer", color: "#1e293b", roughness: 0.9, metalness: 0, specularIntensity: 0.1, sheen: 0.5 }, + { name: "Carbon Fiber (CFRP)", color: "#111111", roughness: 0.3, metalness: 0.4, clearcoat: 1 }, + { name: "PETG", color: "#ffffff", roughness: 0.05, metalness: 0, transmission: 0.98, ior: 1.53, thickness: 0.1 }, + { name: "Melamine Resin", color: "#fef3c7", roughness: 0.1, metalness: 0, transmission: 0.1, ior: 1.5 }, + { name: "Nylon 6/6", color: "#f1f5f9", roughness: 0.5, metalness: 0, sheen: 0.2 }, + { name: "Bakelite", color: "#451a03", roughness: 0.1, metalness: 0, specularIntensity: 0.8 }, + { name: "Iridescent Acrylic", color: "#ffffff", roughness: 0.1, metalness: 0, transmission: 0.8, ior: 1.49, thickness: 0.5, clearcoat: 1, sheen: 1 }, + { name: "Recycled HDPE", color: "#86efac", roughness: 0.7, metalness: 0 }, + { name: "Pearlescent Polystyrene", color: "#fdf4ff", roughness: 0.1, metalness: 0, clearcoat: 1, sheen: 1 }, + { name: "Foamed Polystyrene", color: "#ffffff", roughness: 0.9, metalness: 0 }, + { name: "Clear Epoxy Resin", color: "#ffffff", roughness: 0.02, metalness: 0, transmission: 0.95, ior: 1.55, thickness: 2, clearcoat: 1 }, + { name: "Teflon (PTFE)", color: "#f8fafc", roughness: 0.9, metalness: 0, specularIntensity: 0 }, + { name: "Vinyl", color: "#1a1a1a", roughness: 0.4, metalness: 0, sheen: 0.3 }, + { name: "Polyurethane Foam", color: "#fef08a", roughness: 1.0, metalness: 0, specularIntensity: 0 } + ] + }, + { + id: "metals", + name: "Metals", + items: [ + { name: "Polished Chrome", color: "#ffffff", roughness: 0, metalness: 1 }, + { name: "Brushed Aluminum", color: "#a0a0a0", roughness: 0.4, metalness: 1 }, + { name: "Scratched Stainless Steel", color: "#888888", roughness: 0.3, metalness: 1 }, + { name: "Raw Cast Iron", color: "#222222", roughness: 0.8, metalness: 0.8 }, + { name: "Anodized Aluminum", color: "#991b1b", roughness: 0.3, metalness: 1 }, + { name: "Galvanized Steel", color: "#94a3b8", roughness: 0.5, metalness: 1 }, + { name: "Hammered Copper", color: "#b87333", roughness: 0.3, metalness: 1 }, + { name: "Tarnished Brass", color: "#8a7b31", roughness: 0.4, metalness: 0.9 }, + { name: "Polished Gold", color: "#ffd700", roughness: 0.05, metalness: 1 }, + { name: "Matte Rose Gold", color: "#b76e79", roughness: 0.3, metalness: 1 }, + { name: "Gunmetal", color: "#2a2a2a", roughness: 0.2, metalness: 1 }, + { name: "Titanium", color: "#71717a", roughness: 0.25, metalness: 1 }, + { name: "Rusted Corten Steel", color: "#7c2d12", roughness: 0.9, metalness: 0.2 }, + { name: "Diamond Plate Steel", color: "#cbd5e1", roughness: 0.3, metalness: 1 }, + { name: "Sintered Bronze", color: "#806020", roughness: 0.7, metalness: 0.8 }, + { name: "Wrought Iron", color: "#0a0a0a", roughness: 0.8, metalness: 0.6, specularIntensity: 0.1 }, + { name: "Lead", color: "#3f3f46", roughness: 0.6, metalness: 1 }, + { name: "Polished Silver", color: "#f8fafc", roughness: 0.02, metalness: 1 }, + { name: "Magnesium Alloy", color: "#52525b", roughness: 0.4, metalness: 1 }, + { name: "Bismuth", color: "#a78bfa", roughness: 0.2, metalness: 1, clearcoat: 0.5, sheen: 0.5 } + ] + }, + { + id: "glass", + name: "Glass", + items: [ + { name: "Clear Float Glass", color: "#ffffff", roughness: 0, metalness: 0, transmission: 1, ior: 1.52, thickness: 0.5 }, + { name: "Frosted Glass", color: "#ffffff", roughness: 0.4, metalness: 0, transmission: 0.9, ior: 1.52, thickness: 0.5 }, + { name: "Tinted Bronze Glass", color: "#785b46", roughness: 0, metalness: 0, transmission: 0.8, ior: 1.52, thickness: 1 }, + { name: "Fluted Glass", color: "#ffffff", roughness: 0.1, metalness: 0, transmission: 0.9, ior: 1.52, thickness: 1 }, + { name: "Tempered Glass", color: "#e0f2fe", roughness: 0, metalness: 0, transmission: 0.95, ior: 1.52, thickness: 0.8 }, + { name: "Leaded Crystal", color: "#ffffff", roughness: 0, metalness: 0, transmission: 1, ior: 1.7, thickness: 2 }, + { name: "Dichroic Glass", color: "#fbcfe8", roughness: 0.05, metalness: 0, transmission: 0.9, ior: 1.52, thickness: 0.5, sheen: 1 }, + { name: "Smoked Glass", color: "#1e293b", roughness: 0.02, metalness: 0, transmission: 0.7, ior: 1.52, thickness: 1 }, + { name: "One-Way Mirror", color: "#e2e8f0", roughness: 0, metalness: 0.8, transmission: 0.2, ior: 1.52, thickness: 0.1 }, + { name: "Wired Safety Glass", color: "#e2e8f0", roughness: 0.1, metalness: 0, transmission: 0.8, ior: 1.52, thickness: 1 }, + { name: "Sea Glass", color: "#99f6e4", roughness: 0.6, metalness: 0, transmission: 0.8, ior: 1.52, thickness: 1.5 }, + { name: "Amber Apothecary", color: "#d97706", roughness: 0.05, metalness: 0, transmission: 0.9, ior: 1.52, thickness: 1.2 }, + { name: "Bulletproof Glass", color: "#bbf7d0", roughness: 0, metalness: 0, transmission: 0.85, ior: 1.55, thickness: 3 }, + { name: "Anti-Reflective Coated", color: "#ffffff", roughness: 0, metalness: 0, transmission: 0.99, ior: 1.52, thickness: 0.2, specularIntensity: 0.1 }, + { name: "Obscured Glass", color: "#ffffff", roughness: 0.5, metalness: 0, transmission: 0.7, ior: 1.52, thickness: 1 }, + { name: "Stained Glass", color: "#3b82f6", roughness: 0.1, metalness: 0, transmission: 0.9, ior: 1.52, thickness: 0.3 }, + { name: "Opal Glass", color: "#f8fafc", roughness: 0.1, metalness: 0, transmission: 0.3, ior: 1.52, thickness: 2, sheen: 0.5 }, + { name: "Uranium Glass", color: "#86efac", roughness: 0.05, metalness: 0, transmission: 0.9, ior: 1.52, thickness: 1, emissive: "#22c55e", emissiveIntensity: 0.2 }, + { name: "Shattered Glass", color: "#ffffff", roughness: 0.1, metalness: 0, transmission: 0.9, ior: 1.52, thickness: 0.5 }, + { name: "Smart Glass (Opaque)", color: "#f1f5f9", roughness: 0.8, metalness: 0, transmission: 0.1, ior: 1.52, thickness: 0.2 } + ] + }, + { + id: "liquids", + name: "Liquids", + items: [ + { name: "Clear Water", color: "#ffffff", roughness: 0, metalness: 0, transmission: 1, ior: 1.33, thickness: 2 }, + { name: "Ocean Water", color: "#0284c7", roughness: 0.1, metalness: 0, transmission: 0.9, ior: 1.33, thickness: 5 }, + { name: "Engine Oil", color: "#451a03", roughness: 0.02, metalness: 0, transmission: 0.3, ior: 1.45, thickness: 3 }, + { name: "Milk", color: "#ffffff", roughness: 0.1, metalness: 0, transmission: 0.05, ior: 1.35, thickness: 3, sheen: 0.5 }, + { name: "Orange Juice", color: "#f97316", roughness: 0.05, metalness: 0, transmission: 0.4, ior: 1.35, thickness: 2 }, + { name: "Red Wine", color: "#7f1d1d", roughness: 0, metalness: 0, transmission: 0.8, ior: 1.34, thickness: 1.5 }, + { name: "Honey", color: "#d97706", roughness: 0, metalness: 0, transmission: 0.7, ior: 1.49, thickness: 2 }, + { name: "Liquid Mercury", color: "#e2e8f0", roughness: 0, metalness: 1, transmission: 0, ior: 1 }, + { name: "Coffee", color: "#291304", roughness: 0, metalness: 0, transmission: 0.2, ior: 1.33, thickness: 3 }, + { name: "Carbonated Soda", color: "#ffffff", roughness: 0, metalness: 0, transmission: 0.95, ior: 1.33, thickness: 1.5 }, + { name: "Liquid Nitrogen", color: "#ffffff", roughness: 0, metalness: 0, transmission: 1, ior: 1.2, thickness: 1 }, + { name: "Blood", color: "#7f1d1d", roughness: 0, metalness: 0, transmission: 0.1, ior: 1.35, thickness: 2 }, + { name: "Shampoo", color: "#c084fc", roughness: 0.05, metalness: 0, transmission: 0.8, ior: 1.38, thickness: 1.5, sheen: 0.8 }, + { name: "Beer", color: "#d97706", roughness: 0, metalness: 0, transmission: 0.8, ior: 1.34, thickness: 2 }, + { name: "Glycerin", color: "#ffffff", roughness: 0, metalness: 0, transmission: 1, ior: 1.47, thickness: 2 }, + { name: "Melted Chocolate", color: "#3b1e08", roughness: 0.2, metalness: 0, transmission: 0, ior: 1.5, thickness: 1 }, + { name: "Automotive Coolant", color: "#22c55e", roughness: 0, metalness: 0, transmission: 0.9, ior: 1.33, thickness: 1.5 }, + { name: "Olive Oil", color: "#84cc16", roughness: 0, metalness: 0, transmission: 0.85, ior: 1.47, thickness: 2 }, + { name: "Ink", color: "#000000", roughness: 0, metalness: 0, transmission: 0, ior: 1.33, thickness: 1 }, + { name: "Perfume", color: "#fbcfe8", roughness: 0, metalness: 0, transmission: 0.98, ior: 1.4, thickness: 1.2 } + ] + } + ]; + + const updateSceneObjects = (newObjects: SceneObject[] | ((prev: SceneObject[]) => SceneObject[])) => { + setSceneObjects(prev => { + const next = typeof newObjects === 'function' ? newObjects(prev) : newObjects; + // We'll use a separate effect or a callback to push to history to avoid state update during render + return next; + }); + }; + + useEffect(() => { + if (skipHistoryRef.current) return; + if (!isUndoingRedoing && sceneObjects.length > 0) { + const timer = setTimeout(() => { + pushToHistory(sceneObjects); + }, 500); // Debounce history pushes for performance + return () => clearTimeout(timer); + } + }, [sceneObjects]); + + useEffect(() => { + // Initial history + if (history.length === 0 && sceneObjects.length > 0) { + setHistory([JSON.parse(JSON.stringify(sceneObjects))]); + setHistoryIndex(0); + } + }, []); + + useEffect(() => { + if (theme === 'dark') { + document.body.classList.add('dark'); + } else { + document.body.classList.remove('dark'); + } + }, [theme]); + + const updateObjectMaterial = (id: string, props: Partial) => { + updateSceneObjects(prev => { + const targetObj = prev.find(obj => obj.id === id); + if (!targetObj) return prev; + + // If it's a model, apply material properties to all its child meshes + if (targetObj.type === 'model') { + return prev.map(obj => { + if (obj.id === id || obj.parentId === id) { + return { + ...obj, + materialProps: { ...(obj.materialProps || globalMaterialProps), ...props } + }; + } + return obj; + }); + } + + return prev.map(obj => + obj.id === id ? { ...obj, materialProps: { ...(obj.materialProps || globalMaterialProps), ...props } } : obj + ); + }); + }; + + const updateObjectTransform = (id: string, props: Partial) => { + updateSceneObjects(prev => prev.map(obj => + obj.id === id ? { ...obj, transformProps: { ...obj.transformProps!, ...props } } : obj + )); + }; + + const updateObjectVisibility = (id: string, visible: boolean) => { + updateSceneObjects(prev => prev.map(obj => + obj.id === id ? { ...obj, visible } : obj + )); + }; + + const selectedObject = sceneObjects.find(obj => selectedIds.includes(obj.id)); + const currentCategory = materialCategories.find(c => c.id === selectedCategory) || materialCategories[0]; + + return ( +
+ {/* Background Decorative Blobs */} +
+
+
+
+ + {/* Hidden File Input */} + + + + + + + {/* Top Menu Bar */} +
+
+
+
+ setOpenMenu(openMenu === 'file' ? null : 'file')} + > + File + + {openMenu === 'file' && ( +
+ +
+ +
+ + + + +
+ )} +
+ +
+ setOpenMenu(openMenu === 'edit' ? null : 'edit')} + > + Edit + + {openMenu === 'edit' && ( +
+ + +
+ )} +
+ + Environment + Lighting + Camera + Image + Render + Tools +
+ setOpenMenu(openMenu === 'view' ? null : 'view')} + > + View + + {openMenu === 'view' && ( +
+ + +
+ )} +
+ Window + Help +
+
+
+
+ Startup + +
+
+ 100 % + +
+
+
+ + {/* Main Toolbar */} +
+
+ +
+ + +
+ + +
+ + +
+ + +
+
+
+ + +
+ 50.0 FPS +
+
+
+ + {/* Main Studio Area */} +
+ {/* Left Sidebar */} +
+
+ setActiveSidebar("Materials")} /> + setActiveSidebar("Colors")} /> + setActiveSidebar("Textures")} /> + setActiveSidebar("Environ...")} /> + setActiveSidebar("Favorites")} /> + setActiveSidebar("Models")} /> + setShowImageTo3D(true)} /> +
+
+
+ {activeSidebar} +
+ + +
+
+
+
+ + +
+
+
+ {activeSidebar === "Materials" && materialCategories.map(cat => ( + + ))} + {activeSidebar === "Textures" && ["Patterns", "Materials", "Nature", "Abstract"].map(cat => ( + + ))} +
+
+ {activeSidebar === "Materials" && ( +
+ {currentCategory.items.map((mat) => ( + { + if (selectedIds.length > 0) { + selectedIds.forEach(id => { + updateObjectMaterial(id, { + color: mat.color, + roughness: mat.roughness, + metalness: mat.metalness, + clearcoat: mat.clearcoat || 0, + transmission: mat.transmission || 0, + thickness: mat.thickness || 0, + ior: mat.ior || 1.5, + sheen: mat.sheen || 0, + opacity: mat.opacity || 1, + specularIntensity: mat.specularIntensity || 1, + uvScale: mat.uvScale || 1 + }); + }); + } + }} + /> + ))} +
+ )} + + {activeSidebar === "Textures" && ( +
+ {[ + { name: "Carbon Fiber", url: "https://picsum.photos/seed/carbon/200/200" }, + { name: "Wood Grain", url: "https://picsum.photos/seed/wood/200/200" }, + { name: "Brushed Metal", url: "https://picsum.photos/seed/metal/200/200" }, + { name: "Denim Texture", url: "https://picsum.photos/seed/denim/200/200" }, + { name: "Leather", url: "https://picsum.photos/seed/leather/200/200" }, + { name: "Marble", url: "https://picsum.photos/seed/marble/200/200" }, + ].map((tex) => ( + + ))} + +
+ )} + + {activeSidebar === "Colors" && ( +
+ {["#ef4444", "#f97316", "#f59e0b", "#eab308", "#84cc16", "#22c55e", "#10b981", "#06b6d4", "#3b82f6", "#6366f1", "#8b5cf6", "#a855f7", "#d946ef", "#ec4899", "#f43f5e", "#ffffff", "#a1a1aa", "#3f3f46", "#18181b", "#000000"].map((color) => ( +
+ )} + + {activeSidebar === "Environ..." && ( +
+ Environment presets are also available in the right sidebar. +
+ {['studio', 'apartment', 'city', 'dawn', 'forest'].map(preset => ( + + ))} +
+
+ )} +
+
+
+ + {/* Viewport */} +
+ + + + {cameraProps.orthographic ? ( + + ) : ( + + )} + {backgroundType === "color" && } + + + + {sceneObjects.map(obj => { + if (obj.id === 'juicebox-1' && obj.visible) { + return ( + { + e.stopPropagation(); + setSelectedIds([obj.id]); + }} + /> + ); + } + if (obj.type === 'cube' && obj.visible) { + return ( + { + e.stopPropagation(); + setSelectedIds([obj.id]); + }} + > + + 0 || (obj.materialProps?.opacity ?? 1) < 1} + /> + + ); + } + return null; + })} + + + + {(!isRendering || !renderRequest?.includeAlpha) ? ( + customHdri ? ( + + ) : envPreset ? ( + + ) : null + ) : ( + /* During alpha render, provide environment but no background */ + customHdri ? ( + + ) : envPreset ? ( + + ) : null + )} + + {cameraProps.groundGrid && !isRendering && ( + + )} + + {showGrid && !cameraProps.groundGrid && !isRendering && ( + + )} + + {groundProps.showShadow && ( + + )} + + {selectedIds.length === 1 && !isRendering && ( + { + const id = selectedIds[0]; + const obj = sceneObjects.find(o => o.id === id); + if (!obj) return undefined; + + if (id === 'juicebox-1') return juiceBoxRef.current || undefined; + if (obj.type === 'cube') return cubeRef.current || undefined; + if (obj.type === 'model') return modelRef.current || undefined; + if (obj.type === 'mesh' && loadedModel) { + let targetMesh: THREE.Object3D | undefined; + let mIdx = 0; + loadedModel.traverse(child => { + if (child instanceof THREE.Mesh) { + const stableId = `mesh-${mIdx}`; + if (stableId === id) targetMesh = child; + mIdx++; + } + }); + return targetMesh; + } + return undefined; + })()} + onMouseDown={() => { + if (orbitControlsRef.current) orbitControlsRef.current.enabled = false; + }} + onMouseUp={() => { + if (orbitControlsRef.current) orbitControlsRef.current.enabled = true; + const target = transformRef.current?.object; + if (target && target.position && target.rotation && target.scale) { + updateObjectTransform(selectedIds[0], { + position: [target.position.x || 0, target.position.y || 0, target.position.z || 0], + rotation: [target.rotation.x || 0, target.rotation.y || 0, target.rotation.z || 0], + scale: [target.scale.x || 1, target.scale.y || 1, target.scale.z || 1] + }); + } + }} + /> + )} + {/* Background */} + {backgroundType === "image" && backplateImage && (!isRendering || !renderRequest?.includeAlpha) && ( + + )} + + + {/* Primary Light Source */} + + + + {/* Additional Lights */} + {additionalLights.map(light => ( + light.type === 'point' ? ( + + ) : ( + + ) + ))} + + + { + if (orbitControlsRef.current) { + const controls = orbitControlsRef.current; + const target = controls.target; + const position = controls.object?.position; + + if (!target || !position) return; + + // Spherical + const distance = controls.getDistance(); + const azimuth = controls.getAzimuthalAngle() * (180 / Math.PI); + const inclination = (Math.PI / 2 - controls.getPolarAngle()) * (180 / Math.PI); + + setCameraProps(prev => ({ + ...prev, + target: [target.x, target.y, target.z], + position: [position.x, position.y, position.z], + spherical: { + ...prev.spherical, + distance, + azimuth, + inclination + } + })); + } + }} + /> + + {(cameraProps.depthOfField.enabled || cameraProps.bloom.enabled || cameraProps.vignette.enabled || cameraProps.colorGrading.enabled) && ( + + {cameraProps.depthOfField.enabled && ( + + )} + {cameraProps.bloom.enabled && ( + + )} + {cameraProps.vignette.enabled && ( + + )} + {cameraProps.colorGrading.enabled && ( + <> + + + + )} + + )} + + + + {/* Viewport Overlays */} +
+
+
+
+ Real-time Render +
+
+
+ + {/* Loading Overlay */} + + {(isLoading || importError) && ( + +
+ {importError ? ( +
+
+ +
+
+ Import Failed + {importError} +
+ +
+ ) : ( + <> +
+ +
+
+
+ + + {importStatus || "Importing Model"} + +
+ {importProgress}% Complete +
+ + )} +
+
+ )} +
+ + setShowRenderModal(false)} + onRender={(settings) => setRenderRequest(settings)} + /> + + {/* Export / Save Dialog */} + {exportDialog && ( +
setExportDialog(null)}> + e.stopPropagation()} + className="w-full max-w-sm bg-zinc-900 border border-zinc-700 rounded-2xl shadow-2xl overflow-hidden" + > + {/* Header */} +
+
+ + + {exportDialog.mode === 'scene' ? 'Save Scene' : + exportDialog.mode === 'glb' ? 'Export GLB' : 'Export USDZ'} + +
+ +
+ + {/* Body */} +
+
+ + setExportDialogName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') confirmExportDialog(); if (e.key === 'Escape') setExportDialog(null); }} + placeholder={exportDialog.defaultName} + className="bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-sm text-zinc-100 outline-none focus:border-blue-500 transition-colors" + /> +
+
+ +

+ Downloads folder — browser default +

+

File will be saved as: {(exportDialogName || exportDialog.defaultName).trim()}.{exportDialog.mode === 'scene' ? 'json' : exportDialog.mode}

+
+
+ + {/* Footer */} +
+ + +
+
+
+ )} +
+ + {/* Right Sidebar */} +
+
+ + + + + +
+ +
+ {activeRightTab === "Scene" ? ( + <> +
+
+ + Show +
+
+ + +
+
+ +
+
+
+ Hierarchy +
+ + +
+
+ +
+ {sceneObjects.filter(obj => obj.parentId === null).map(obj => ( +
+
toggleSelect(obj.id, e.ctrlKey || e.metaKey)} + onContextMenu={(e) => { + e.preventDefault(); + setContextMenu({ x: e.clientX, y: e.clientY, objectId: obj.id }); + }} + className={cn( + "flex items-center gap-2 p-1 rounded text-[10px] cursor-pointer group transition-colors", + selectedIds.includes(obj.id) ? "bg-blue-500/20 text-blue-400" : "hover:bg-zinc-800 text-zinc-300" + )} + > + + {obj.type === 'group' ? : } + + {editingId === obj.id ? ( + renameObject(obj.id, e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') renameObject(obj.id, e.currentTarget.value); + if (e.key === 'Escape') setEditingId(null); + }} + /> + ) : ( + {obj.name} + )} + + {!editingId && selectedIds.includes(obj.id) && ( + + )} +
+ + {/* Render Children */} +
+ {sceneObjects.filter(child => child.parentId === obj.id).map(child => ( +
toggleSelect(child.id, e.ctrlKey || e.metaKey)} + onContextMenu={(e) => { + e.preventDefault(); + setContextMenu({ x: e.clientX, y: e.clientY, objectId: child.id }); + }} + className={cn( + "flex items-center gap-2 p-1 rounded text-[10px] cursor-pointer group transition-colors", + selectedIds.includes(child.id) ? "bg-blue-500/20 text-blue-400" : "hover:bg-zinc-800 text-zinc-300" + )} + > + + + + {editingId === child.id ? ( + renameObject(child.id, e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') renameObject(child.id, e.currentTarget.value); + if (e.key === 'Escape') setEditingId(null); + }} + /> + ) : ( + {child.name} + )} + + {!editingId && selectedIds.includes(child.id) && ( + + )} +
+ ))} +
+
+ ))} +
+
+
+ + ) : activeRightTab === "Transform" ? ( +
+
+ Transform +
+
+ + {!selectedObject || !selectedObject.transformProps ? ( +
+ + Select an object to edit its transform +
+ ) : ( + <> + {/* Position */} +
+ Position + {['x', 'y', 'z'].map((axis, i) => ( +
+ {axis} + { + const newPos = [...selectedObject.transformProps!.position] as [number, number, number]; + newPos[i] = parseFloat(e.target.value); + updateObjectTransform(selectedObject.id, { position: newPos }); + }} + className="flex-1 bg-zinc-800 border-none rounded py-1 px-2 text-[10px] text-zinc-300" + /> +
+ ))} +
+ + {/* Rotation */} +
+ Rotation + {['x', 'y', 'z'].map((axis, i) => ( +
+ {axis} + { + const newRot = [...selectedObject.transformProps!.rotation] as [number, number, number]; + newRot[i] = parseFloat(e.target.value); + updateObjectTransform(selectedObject.id, { rotation: newRot }); + }} + className="flex-1 bg-zinc-800 border-none rounded py-1 px-2 text-[10px] text-zinc-300" + /> +
+ ))} +
+ + {/* Scale */} +
+ Scale + {['x', 'y', 'z'].map((axis, i) => ( +
+ {axis} + { + const newScale = [...selectedObject.transformProps!.scale] as [number, number, number]; + newScale[i] = parseFloat(e.target.value); + updateObjectTransform(selectedObject.id, { scale: newScale }); + }} + className="flex-1 bg-zinc-800 border-none rounded py-1 px-2 text-[10px] text-zinc-300" + /> +
+ ))} +
+ + )} +
+ ) : activeRightTab === "Camera" ? ( +
+
+ Position and Orientation +
+
+ + {/* Mode Toggle */} +
+ + +
+ + {/* Spherical Controls */} + {cameraProps.cameraMode === "spherical" && ( +
+
+
+ Distance + {cameraProps.spherical.distance.toFixed(3)} m +
+ { + const dist = parseFloat(e.target.value); + setCameraProps(prev => ({ ...prev, spherical: { ...prev.spherical, distance: dist } })); + // Update camera position based on spherical + if (orbitControlsRef.current) { + const controls = orbitControlsRef.current; + const phi = (90 - cameraProps.spherical.inclination) * (Math.PI / 180); + const theta = cameraProps.spherical.azimuth * (Math.PI / 180); + const x = dist * Math.sin(phi) * Math.cos(theta) + cameraProps.target[0]; + const y = dist * Math.cos(phi) + cameraProps.target[1]; + const z = dist * Math.sin(phi) * Math.sin(theta) + cameraProps.target[2]; + controls.object.position.set(x, y, z); + controls.update(); + } + }} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ +
+
+ Azimuth + {cameraProps.spherical.azimuth.toFixed(3)} ° +
+ { + const az = parseFloat(e.target.value); + setCameraProps(prev => ({ ...prev, spherical: { ...prev.spherical, azimuth: az } })); + if (orbitControlsRef.current) { + const controls = orbitControlsRef.current; + const phi = (90 - cameraProps.spherical.inclination) * (Math.PI / 180); + const theta = az * (Math.PI / 180); + const x = cameraProps.spherical.distance * Math.sin(phi) * Math.cos(theta) + cameraProps.target[0]; + const y = cameraProps.spherical.distance * Math.cos(phi) + cameraProps.target[1]; + const z = cameraProps.spherical.distance * Math.sin(phi) * Math.sin(theta) + cameraProps.target[2]; + controls.object.position.set(x, y, z); + controls.update(); + } + }} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ +
+
+ Inclination + {cameraProps.spherical.inclination.toFixed(3)} ° +
+ { + const inc = parseFloat(e.target.value); + setCameraProps(prev => ({ ...prev, spherical: { ...prev.spherical, inclination: inc } })); + if (orbitControlsRef.current) { + const controls = orbitControlsRef.current; + const phi = (90 - inc) * (Math.PI / 180); + const theta = cameraProps.spherical.azimuth * (Math.PI / 180); + const x = cameraProps.spherical.distance * Math.sin(phi) * Math.cos(theta) + cameraProps.target[0]; + const y = cameraProps.spherical.distance * Math.cos(phi) + cameraProps.target[1]; + const z = cameraProps.spherical.distance * Math.sin(phi) * Math.sin(theta) + cameraProps.target[2]; + controls.object.position.set(x, y, z); + controls.update(); + } + }} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+ )} + + {/* Absolute Position Controls */} + {cameraProps.cameraMode === "absolute" && ( +
+ Position (Absolute) +
+ {["x", "y", "z"].map((axis, i) => ( +
+ {axis} + { + const newPos = [...cameraProps.position] as [number, number, number]; + newPos[i] = parseFloat(e.target.value); + setCameraProps(prev => ({ ...prev, position: newPos })); + if (orbitControlsRef.current) { + orbitControlsRef.current.object.position.set(...newPos); + orbitControlsRef.current.update(); + } + }} + className="w-full bg-zinc-900 border border-zinc-800 rounded px-1 py-0.5 text-[10px] text-zinc-300" + /> +
+ ))} +
+
+ )} + + {/* Target Controls */} +
+ Target +
+ {["x", "y", "z"].map((axis, i) => ( +
+ {axis} + { + const newTarget = [...cameraProps.target] as [number, number, number]; + newTarget[i] = parseFloat(e.target.value); + setCameraProps(prev => ({ ...prev, target: newTarget })); + if (orbitControlsRef.current) { + orbitControlsRef.current.target.set(...newTarget); + orbitControlsRef.current.update(); + } + }} + className="w-full bg-zinc-900 border border-zinc-800 rounded px-1 py-0.5 text-[10px] text-zinc-300" + /> +
+ ))} +
+
+ + {/* Pivot Controls */} +
+ Pivot +
+ {["x", "y", "z"].map((axis, i) => ( +
+ {axis} + { + const newPivot = [...cameraProps.pivot] as [number, number, number]; + newPivot[i] = parseFloat(e.target.value); + setCameraProps(prev => ({ ...prev, pivot: newPivot })); + }} + className="w-full bg-zinc-900 border border-zinc-800 rounded px-1 py-0.5 text-[10px] text-zinc-300" + /> +
+ ))} +
+
+ + {/* Standard Views */} +
+ Standard Views +
+ {["front", "back", "top", "bottom", "left", "right", "isometric"].map((view) => ( + + ))} +
+
+ +
+ Lens Settings +
+
+ + {/* Camera Type */} +
+ + +
+ + {/* Focal Length / FOV */} + {!cameraProps.orthographic && ( + <> +
+
+ Focal Length + {fovToFocalLength(cameraProps.fov).toFixed(1)} mm +
+ setCameraProps(prev => ({ ...prev, fov: focalLengthToFov(parseInt(e.target.value)) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ +
+
+ Field of View + {cameraProps.fov.toFixed(1)}° +
+ setCameraProps(prev => ({ ...prev, fov: parseInt(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + )} + + {/* Zoom */} +
+
+ Zoom Level + {cameraProps.zoom.toFixed(2)}x +
+ setCameraProps(prev => ({ ...prev, zoom: parseFloat(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Depth of Field Toggle */} +
+
+ Depth of Field + Enable bokeh effect +
+ +
+ + {/* DOF Settings */} + {cameraProps.depthOfField.enabled && ( +
+
+
+ Focus Distance + {cameraProps.depthOfField.focusDistance.toFixed(3)} +
+ setCameraProps(prev => ({ ...prev, depthOfField: { ...prev.depthOfField, focusDistance: parseFloat(e.target.value) } }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+
+ Bokeh Scale + {cameraProps.depthOfField.bokehScale.toFixed(1)} +
+ setCameraProps(prev => ({ ...prev, depthOfField: { ...prev.depthOfField, bokehScale: parseFloat(e.target.value) } }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+ )} + + {/* Post Processing Section */} +
+ Post Processing +
+
+ + {/* Bloom Toggle */} +
+
+ Bloom + Add glow to highlights +
+ +
+ + {/* Bloom Settings */} + {cameraProps.bloom.enabled && ( +
+
+
+ Intensity + {cameraProps.bloom.intensity.toFixed(2)} +
+ setCameraProps(prev => ({ ...prev, bloom: { ...prev.bloom, intensity: parseFloat(e.target.value) } }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+
+ Threshold + {cameraProps.bloom.luminanceThreshold.toFixed(2)} +
+ setCameraProps(prev => ({ ...prev, bloom: { ...prev.bloom, luminanceThreshold: parseFloat(e.target.value) } }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+ )} + + {/* Vignette Toggle */} +
+
+ Vignette + Darken image edges +
+ +
+ + {/* Vignette Settings */} + {cameraProps.vignette.enabled && ( +
+
+
+ Darkness + {cameraProps.vignette.darkness.toFixed(2)} +
+ setCameraProps(prev => ({ ...prev, vignette: { ...prev.vignette, darkness: parseFloat(e.target.value) } }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+ )} + + {/* Color Grading Toggle */} +
+
+ Color Grading + Adjust brightness & color +
+ +
+ + {/* Color Grading Settings */} + {cameraProps.colorGrading.enabled && ( +
+
+
+ Brightness + {cameraProps.colorGrading.brightness.toFixed(2)} +
+ setCameraProps(prev => ({ ...prev, colorGrading: { ...prev.colorGrading, brightness: parseFloat(e.target.value) } }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+
+ Contrast + {cameraProps.colorGrading.contrast.toFixed(2)} +
+ setCameraProps(prev => ({ ...prev, colorGrading: { ...prev.colorGrading, contrast: parseFloat(e.target.value) } }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+
+ Saturation + {cameraProps.colorGrading.saturation.toFixed(2)} +
+ setCameraProps(prev => ({ ...prev, colorGrading: { ...prev.colorGrading, saturation: parseFloat(e.target.value) } }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+ )} + + {/* Grid and Ground Options */} +
+
+
+ Ground Grid + Show grid on floor +
+ +
+ +
+
+ Stay Above Ground + Limit camera height +
+ +
+
+ + {/* Auto Rotate */} +
+
+ Auto-Rotate + Rotate around model +
+ +
+ + {/* Camera Presets */} +
+
+ Camera Presets + +
+
+ +
+ {cameraPresets.map((preset) => ( +
+ +
+ {preset.id !== 'default' && ( + + )} +
+
+ ))} +
+
+ +
+ +
+
+ ) : activeRightTab === "Environ..." ? ( +
+
+ Viewport Background +
+
+ + {/* Background Type Toggle */} +
+ + +
+ + {/* Background Color & Image Selection */} + {(backgroundType === "color" || backgroundType === "image") && ( +
+
+
+ Background Color +
+
+
+
+ +
+
+
+ Backplate Image + {backgroundType === "image" && ( + + )} +
+
+ + {[ + { name: "Studio", url: "https://picsum.photos/seed/studio/1920/1080?blur=4" }, + { name: "Outdoor", url: "https://picsum.photos/seed/outdoor/1920/1080?blur=4" }, + { name: "Interior", url: "https://picsum.photos/seed/interior/1920/1080?blur=4" }, + { name: "Abstract", url: "https://picsum.photos/seed/abstract/1920/1080?blur=4" } + ].map((plate) => ( + + ))} +
+
+
+ Custom Backplate URL +
+ { + if (e.target.value) { + setBackplateImage(e.target.value); + setBackgroundType("image"); + } + }} + /> + +
+
+
+
+ )} + + {/* Environment Rotation Wheel */} +
+
+ Environment Rotation + {envRotation}° +
+
+
{ + const rect = e.currentTarget.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + const handleMouseMove = (moveEvent: MouseEvent) => { + const angle = Math.atan2(moveEvent.clientY - centerY, moveEvent.clientX - centerX); + let deg = angle * (180 / Math.PI) + 90; + if (deg < 0) deg += 360; + setEnvRotation(Math.round(deg)); + }; + + const handleMouseUp = () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + }} + > + {/* Degree markers */} + {[0, 45, 90, 135, 180, 225, 270, 315].map(deg => ( +
+ ))} + {/* Pointer */} +
+
+
+
+ setEnvRotation(parseInt(e.target.value))} + className="w-full accent-blue-500 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer" + /> +
+ + 180° + 360° +
+
+
+
+ + {/* Grid Toggle */} +
+
+ Viewport Grid + Show spatial reference +
+ +
+ +
+ Primary Light +
+
+ + {/* Light Intensity */} +
+
+ Intensity + {lightProps.intensity.toFixed(2)} +
+ setLightProps(prev => ({ ...prev, intensity: parseFloat(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Light Color */} +
+
+ Light Color + setLightProps(prev => ({ ...prev, color: e.target.value }))} + className="w-6 h-6 rounded border border-zinc-700 bg-transparent cursor-pointer" + /> +
+
+ + {/* Advanced Lighting Controls */} +
+
+
+ Shadow Advanced +
+
+
+ Shadow Bias + {lightProps.shadowBias.toFixed(4)} +
+ setLightProps(prev => ({ ...prev, shadowBias: parseFloat(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+
+ Shadow Radius + {lightProps.shadowRadius.toFixed(1)} +
+ setLightProps(prev => ({ ...prev, shadowRadius: parseFloat(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+
+ + {/* Additional Lights Section */} +
+
+ Additional Lights +
+ +
+
+
+
+ + {/* Additional Lights List */} +
+ {additionalLights.map((light, index) => ( +
+
+
+ + {light.type} Light {index + 1} +
+ +
+ +
+
+
+ Intensity + {light.intensity.toFixed(1)} +
+ setAdditionalLights(prev => prev.map(l => l.id === light.id ? { ...l, intensity: parseFloat(e.target.value) } : l))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+
+ Color + setAdditionalLights(prev => prev.map(l => l.id === light.id ? { ...l, color: e.target.value } : l))} + className="w-4 h-4 rounded border border-zinc-700 bg-transparent cursor-pointer" + /> +
+
+
+ +
+ Position (X, Y, Z) +
+ {[0, 1, 2].map(i => ( + { + const newPos = [...light.position] as [number, number, number]; + newPos[i] = parseFloat(e.target.value); + setAdditionalLights(prev => prev.map(l => l.id === light.id ? { ...l, position: newPos } : l)); + }} + className="bg-zinc-900 border border-zinc-800 rounded px-1.5 py-1 text-[10px] text-zinc-300" + /> + ))} +
+
+
+ ))} + {additionalLights.length === 0 && ( +
+ No additional lights added +
+ )} +
+ +
+ Ground & Shadows +
+
+ + {/* Ground Shadow Toggle */} +
+
+ Ground Shadow + Enable soft shadows +
+ +
+ + {/* Shadow Controls */} + {groundProps.showShadow && ( + <> +
+
+ Shadow Intensity + {groundProps.shadowIntensity.toFixed(2)} +
+ setGroundProps(prev => ({ ...prev, shadowIntensity: parseFloat(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ +
+
+ Shadow Softness + {groundProps.shadowSoftness.toFixed(2)} +
+ setGroundProps(prev => ({ ...prev, shadowSoftness: parseFloat(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ +
+
+ Shadow Length + {groundProps.shadowLength.toFixed(1)} +
+ setGroundProps(prev => ({ ...prev, shadowLength: parseFloat(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ +
+
+ Shadow Rotation + {groundProps.shadowRotation}° +
+
+
+
+
+
+
+ setGroundProps(prev => ({ ...prev, shadowRotation: parseInt(e.target.value) }))} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10" + /> + +
+
+ setGroundProps(prev => ({ ...prev, shadowRotation: parseInt(e.target.value) }))} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + 180° + 360° +
+
+
+
+ + )} + +
+ Environment Presets +
+
+ +
+ + + {customHdri && ( + + )} + + {[ + { id: 'studio', name: 'Studio', desc: 'Clean, neutral lighting' }, + { id: 'apartment', name: 'Apartment', desc: 'Indoor home lighting' }, + { id: 'city', name: 'City', desc: 'Urban outdoor lighting' }, + { id: 'dawn', name: 'Dawn', desc: 'Soft morning light' }, + { id: 'forest', name: 'Forest', desc: 'Natural outdoor light' }, + { id: 'lobby', name: 'Lobby', desc: 'Commercial indoor light' }, + { id: 'night', name: 'Night', desc: 'Low light, dark environment' }, + { id: 'park', name: 'Park', desc: 'Bright outdoor light' }, + { id: 'sunset', name: 'Sunset', desc: 'Warm evening light' }, + { id: 'warehouse', name: 'Warehouse', desc: 'Industrial lighting' }, + ].map((preset) => ( + + ))} +
+
+ ) : ( +
+
+ Material Properties +
+
+ + {!selectedObject || (selectedObject.type !== 'mesh' && selectedObject.type !== 'model') ? ( +
+ + Select a model or mesh to edit its material properties +
+ ) : ( + <> +
+ {selectedObject.name} + {selectedObject.type === 'model' ? "Bulk Editing Model" : "Mesh Editor"} +
+ + {/* Fill Settings */} +
+
+ Mesh Fill + +
+ + {selectedObject.fillProps?.enabled && ( + <> +
+ + +
+ +
+
+ Height + {Math.round(selectedObject.fillProps.height * 100)}% +
+ { + const newObjects = sceneObjects.map(obj => + obj.id === selectedObject.id ? { ...obj, fillProps: { ...obj.fillProps!, height: parseFloat(e.target.value) } } : obj + ); + setSceneObjects(newObjects); + }} + onMouseUp={() => pushToHistory(sceneObjects)} + className="w-full accent-primary" + /> +
+ +
+ Fill Color +
+
+ { + const newObjects = sceneObjects.map(obj => + obj.id === selectedObject.id ? { ...obj, fillProps: { ...obj.fillProps!, color: e.target.value } } : obj + ); + setSceneObjects(newObjects); + }} + onBlur={() => pushToHistory(sceneObjects)} + className="opacity-0 w-5 h-5 cursor-pointer absolute right-0" + /> +
+
+ + )} +
+ + {/* Color Picker */} +
+
+ Diffuse Color +
+
+ updateObjectMaterial(selectedObject.id, { color: e.target.value })} + className="w-full h-8 bg-zinc-800 border-none rounded cursor-pointer" + /> +
+ + {/* Texture Mapping */} +
+
+ Texture Map + {selectedObject.materialProps?.map ? ( + + ) : ( + None + )} +
+ +
textureInputRef.current?.click()} + className={cn( + "w-full h-24 rounded border-2 border-dashed flex flex-col items-center justify-center gap-2 cursor-pointer transition-all", + selectedObject.materialProps?.map + ? "border-blue-500/50 bg-blue-500/5 overflow-hidden" + : "border-zinc-800 hover:border-zinc-700 bg-zinc-800/30" + )} + > + {selectedObject.materialProps?.map ? ( + Texture Preview + ) : ( + <> + + Upload Texture + + )} +
+ +
+ + {/* UV Scale & Mapping */} +
+
+ Mapping +
+
+ +
+ +
+ + {/* UV Projection Dropdown */} +
+ UV Projection + +
+ + {/* UV Offset */} +
+ Offset (X, Y) +
+ updateObjectMaterial(selectedObject.id, { uvOffset: [parseFloat(e.target.value), selectedObject.materialProps?.uvOffset?.[1] ?? 0] })} + className="flex-1 bg-zinc-800 border-none rounded py-1.5 px-2 text-[10px] text-zinc-300" /> + updateObjectMaterial(selectedObject.id, { uvOffset: [selectedObject.materialProps?.uvOffset?.[0] ?? 0, parseFloat(e.target.value)] })} + className="flex-1 bg-zinc-800 border-none rounded py-1.5 px-2 text-[10px] text-zinc-300" /> +
+
+ + {/* UV Scale / Tiling */} +
+ Scale / Tiling (X, Y) +
+ updateObjectMaterial(selectedObject.id, { uvTiling: [parseFloat(e.target.value), selectedObject.materialProps?.uvTiling?.[1] ?? 1] })} + className="flex-1 bg-zinc-800 border-none rounded py-1.5 px-2 text-[10px] text-zinc-300" /> + updateObjectMaterial(selectedObject.id, { uvTiling: [selectedObject.materialProps?.uvTiling?.[0] ?? 1, parseFloat(e.target.value)] })} + className="flex-1 bg-zinc-800 border-none rounded py-1.5 px-2 text-[10px] text-zinc-300" /> +
+
+ + {/* UV Rotation */} +
+
+ Rotation + {(selectedObject.materialProps?.uvRotation ?? 0).toFixed(0)}° +
+ updateObjectMaterial(selectedObject.id, { uvRotation: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" /> +
+
+ + {/* Roughness */} +
+
+ Roughness + {(selectedObject.materialProps?.roughness ?? 0.5).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { roughness: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Metalness */} +
+
+ Metalness + {(selectedObject.materialProps?.metalness ?? 0.5).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { metalness: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Clearcoat */} +
+
+ Clearcoat + {(selectedObject.materialProps?.clearcoat ?? 0).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { clearcoat: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Transmission */} +
+
+ Transmission + {(selectedObject.materialProps?.transmission ?? 0).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { transmission: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* IOR */} +
+
+ Index of Refraction + {(selectedObject.materialProps?.ior ?? 1.5).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { ior: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Thickness */} +
+
+ Thickness + {(selectedObject.materialProps?.thickness ?? 0).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { thickness: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Sheen */} +
+
+ Sheen + {(selectedObject.materialProps?.sheen ?? 0).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { sheen: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Opacity */} +
+
+ Opacity + {(selectedObject.materialProps?.opacity ?? 1).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { opacity: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Specular Intensity */} +
+
+ Specular Intensity + {(selectedObject.materialProps?.specularIntensity ?? 1).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { specularIntensity: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Emissive */} +
+
+ Emissive Color +
+
+ updateObjectMaterial(selectedObject.id, { emissive: e.target.value })} + className="w-full h-8 bg-zinc-800 border-none rounded cursor-pointer" + /> +
+ +
+
+ Emissive Intensity + {(selectedObject.materialProps?.emissiveIntensity ?? 0).toFixed(2)} +
+ updateObjectMaterial(selectedObject.id, { emissiveIntensity: parseFloat(e.target.value) })} + className="w-full h-1 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ + {/* Advanced Maps */} +
+ Advanced Maps + {/* Normal Map */} +
+
+ Normal Map + {selectedObject.materialProps?.normalMap && ( + + )} +
+
normalInputRef.current?.click()} + className={cn( + "w-full h-12 rounded border border-dashed flex items-center justify-center gap-2 cursor-pointer transition-all", + selectedObject.materialProps?.normalMap ? "border-blue-500/50 bg-blue-500/5" : "border-zinc-800 hover:border-zinc-700 bg-zinc-800/30" + )} + > + {selectedObject.materialProps?.normalMap ? "Normal Map Loaded" : "Upload Normal Map"} +
+ handleTextureUpload(e, 'normalMap')} accept="image/*" className="hidden" /> +
+ + {/* Specular Map */} +
+
+ Specular Map + {selectedObject.materialProps?.specularMap && ( + + )} +
+
specularInputRef.current?.click()} + className={cn( + "w-full h-12 rounded border border-dashed flex items-center justify-center gap-2 cursor-pointer transition-all", + selectedObject.materialProps?.specularMap ? "border-blue-500/50 bg-blue-500/5" : "border-zinc-800 hover:border-zinc-700 bg-zinc-800/30" + )} + > + {selectedObject.materialProps?.specularMap ? "Specular Map Loaded" : "Upload Specular Map"} +
+ handleTextureUpload(e, 'specularMap')} accept="image/*" className="hidden" /> +
+ + {/* Alpha Map */} +
+
+ Alpha Map + {selectedObject.materialProps?.alphaMap && ( + + )} +
+
alphaInputRef.current?.click()} + className={cn( + "w-full h-12 rounded border border-dashed flex items-center justify-center gap-2 cursor-pointer transition-all", + selectedObject.materialProps?.alphaMap ? "border-blue-500/50 bg-blue-500/5" : "border-zinc-800 hover:border-zinc-700 bg-zinc-800/30" + )} + > + {selectedObject.materialProps?.alphaMap ? "Alpha Map Loaded" : "Upload Alpha Map"} +
+ handleTextureUpload(e, 'alphaMap')} accept="image/*" className="hidden" /> +
+
+ +
+ +
+ + )} +
+ )} + +
+
+ Scene Information + +
+
+
+ Triangles: + {loadedModel ? "Dynamic" : "12,452"} +
+
+ Vertices: + {loadedModel ? "Dynamic" : "8,210"} +
+
+ Objects: + 1 +
+
+
+
+
+
+ + {/* Bottom Toolbar */} +
+
+ + + + + + + + +
+
+
+ Cloud Library | VIZ3D Hub +
+ +
+
+ + {/* Context Menu */} + + {contextMenu && ( + <> +
setContextMenu(null)} + onContextMenu={(e) => { e.preventDefault(); setContextMenu(null); }} + /> + + + + +
+ + + + + + )} + + + {/* UV Editor Overlay */} + {showUVEditor && selectedObject && ( + setShowUVEditor(false)} + onSave={(dataUrl, mappingType, layers, bgColor, bgTransparent) => { + updateObjectMaterial(selectedObject.id, { map: dataUrl, mappingType: (mappingType as 'uv'|'box'|'planar'|'cylinder'|'sphere') || 'uv', uvLayers: layers, uvBackground: bgColor, uvTransparent: bgTransparent }); + }} + targetObject={selectedObject} + loadedModel={loadedModel as THREE.Group} + sceneObjects={sceneObjects} + /> + )} + + setShowImageTo3D(false)} + onPushToApp={async (modelUrl) => { + setIsLoading(true); + setImportStatus("Importing generated model..."); + try { + const manager = new THREE.LoadingManager(); + const loader = new GLTFLoader(manager); + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/'); + loader.setDRACOLoader(dracoLoader); + + const gltf = await loader.loadAsync(modelUrl); + const object = gltf.scene; + dracoLoader.dispose(); + + const mainModelId = `model-${Date.now()}`; + const newSceneObjects: SceneObject[] = [ + { + id: mainModelId, + name: 'Generated 3D Model', + type: 'model', + parentId: null, + visible: true, + transformProps: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] } + } + ]; + + object.visible = true; + let mIdx = 0; + object.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.castShadow = true; + child.receiveShadow = true; + child.frustumCulled = false; + const meshId = `mesh-${mIdx}`; + mIdx++; + + const material = Array.isArray(child.material) ? child.material[0] : child.material; + const mProps = { ...globalMaterialProps }; + + if (material) { + if (material.color) mProps.color = `#${material.color.getHexString()}`; + if (material.roughness !== undefined) mProps.roughness = material.roughness; + if (material.metalness !== undefined) mProps.metalness = material.metalness; + if (material.opacity !== undefined) mProps.opacity = material.opacity; + } + + newSceneObjects.push({ + id: meshId, + name: child.name || `Mesh ${meshId.slice(0, 4)}`, + type: 'mesh', + parentId: mainModelId, + visible: true, + transformProps: { + position: [child.position?.x || 0, child.position?.y || 0, child.position?.z || 0], + rotation: [child.rotation?.x || 0, child.rotation?.y || 0, child.rotation?.z || 0], + scale: [child.scale?.x || 1, child.scale?.y || 1, child.scale?.z || 1] + }, + materialProps: mProps + }); + } + }); + + setLoadedModel(object); + setBlobURLs(prev => [...prev, modelUrl]); + setSceneObjects(prev => [ + ...prev.filter(obj => obj.type !== 'cube'), + ...newSceneObjects + ]); + setSelectedIds([mainModelId]); + setShowImageTo3D(false); + + setTimeout(() => { + setIsLoading(false); + setImportStatus(""); + }, 500); + + } catch (error) { + console.error('Error loading generated model:', error); + setIsLoading(false); + setImportStatus(""); + } + }} + /> +
+ ); +} diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..953a03a --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,45 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + } + + public render() { + if (this.state.hasError) { + return this.props.fallback || ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ); + } + + return this.props.children; + } +} diff --git a/src/components/ImageTo3DModal.tsx b/src/components/ImageTo3DModal.tsx new file mode 100644 index 0000000..a4894a0 --- /dev/null +++ b/src/components/ImageTo3DModal.tsx @@ -0,0 +1,688 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Upload, Settings as SettingsIcon, Image as ImageIcon, Box, Download, AlertCircle, Loader2, Check } from 'lucide-react'; +import { fal } from '@fal-ai/client'; +import { cn } from '../lib/utils'; +import { Canvas } from '@react-three/fiber'; +import { OrbitControls, Stage } from '@react-three/drei'; +import { GLTFLoader } from 'three-stdlib'; +import * as THREE from 'three'; + +interface ImageTo3DModalProps { + isOpen: boolean; + onClose: () => void; + onPushToApp: (modelUrl: string) => void; +} + +const MODELS = [ + { id: 'fal-ai/hunyuan3d/v2/image-to-3d', name: 'Hunyuan3D-2 (Latest)' }, + { id: 'fal-ai/hunyuan3d/v1', name: 'Hunyuan3D-1' }, + { id: 'fal-ai/meshy/v6/image-to-3d', name: 'Meshy v6' }, + { id: 'fal-ai/tripo3d', name: 'Tripo3D' } +]; + +const ModelPreview = ({ url }: { url: string }) => { + const [model, setModel] = useState(null); + + useEffect(() => { + const loader = new GLTFLoader(); + loader.load(url, (gltf) => { + setModel(gltf.scene); + }); + }, [url]); + + if (!model) return
; + + return ( + + + + + + + ); +}; + +export const ImageTo3DModal: React.FC = ({ isOpen, onClose, onPushToApp }) => { + const [activeTab, setActiveTab] = useState<'generator' | 'settings'>('generator'); + const [apiProvider, setApiProvider] = useState<'fal' | 'tripo' | 'meshy'>('fal'); + const [apiKey, setApiKey] = useState(''); + const [tripoApiKey, setTripoApiKey] = useState(''); + const [meshyApiKey, setMeshyApiKey] = useState(''); + const [selectedModel, setSelectedModel] = useState(MODELS[0].id); + const [imageFile, setImageFile] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + + // Meshy specific state + const [meshyModelType, setMeshyModelType] = useState('standard'); + const [meshyAiModel, setMeshyAiModel] = useState('latest'); + const [meshyShouldTexture, setMeshyShouldTexture] = useState(true); + const [meshyEnablePbr, setMeshyEnablePbr] = useState(false); + const [meshyHdTexture, setMeshyHdTexture] = useState(false); + const [meshyTexturePrompt, setMeshyTexturePrompt] = useState(''); + const [meshyTextureImageUrl, setMeshyTextureImageUrl] = useState(''); + const [meshyShouldRemesh, setMeshyShouldRemesh] = useState(false); + const [meshyTopology, setMeshyTopology] = useState('triangle'); + const [meshyTargetPolycount, setMeshyTargetPolycount] = useState(30000); + + const [isGenerating, setIsGenerating] = useState(false); + const [progressMessage, setProgressMessage] = useState(''); + const [generatedModelUrl, setGeneratedModelUrl] = useState(null); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + const [isValidatingKey, setIsValidatingKey] = useState(false); + const [keyValidationStatus, setKeyValidationStatus] = useState<'idle' | 'valid' | 'invalid' | 'error'>('idle'); + + useEffect(() => { + setKeyValidationStatus('idle'); + }, [apiKey, tripoApiKey, meshyApiKey, apiProvider]); + + const validateKey = async () => { + setIsValidatingKey(true); + setKeyValidationStatus('idle'); + try { + if (apiProvider === 'fal') { + const res = await fetch('https://fal.run/', { + headers: { Authorization: `Key ${apiKey}` } + }); + if (res.status === 401 || res.status === 403) setKeyValidationStatus('invalid'); + else setKeyValidationStatus('valid'); + } else if (apiProvider === 'tripo') { + const res = await fetch('https://api.tripo3d.ai/v2/openapi/task', { + headers: { Authorization: `Bearer ${tripoApiKey}` } + }); + if (res.status === 401 || res.status === 403) setKeyValidationStatus('invalid'); + else setKeyValidationStatus('valid'); + } else if (apiProvider === 'meshy') { + const res = await fetch('https://api.meshy.ai/openapi/v1/image-to-3d', { + headers: { Authorization: `Bearer ${meshyApiKey}` } + }); + if (res.status === 401 || res.status === 403) setKeyValidationStatus('invalid'); + else setKeyValidationStatus('valid'); + } + } catch (e) { + setKeyValidationStatus('error'); + } finally { + setIsValidatingKey(false); + } + }; + + useEffect(() => { + const savedProvider = localStorage.getItem('API_PROVIDER') as 'fal' | 'tripo' | 'meshy' | null; + if (savedProvider) setApiProvider(savedProvider); + const savedKey = localStorage.getItem('FAL_API_KEY'); + if (savedKey) setApiKey(savedKey); + const savedTripoKey = localStorage.getItem('TRIPO_API_KEY'); + if (savedTripoKey) setTripoApiKey(savedTripoKey); + const savedMeshyKey = localStorage.getItem('MESHY_API_KEY'); + if (savedMeshyKey) setMeshyApiKey(savedMeshyKey); + const savedModel = localStorage.getItem('FAL_MODEL'); + if (savedModel) setSelectedModel(savedModel); + }, []); + + const handleSaveSettings = () => { + localStorage.setItem('API_PROVIDER', apiProvider); + localStorage.setItem('FAL_API_KEY', apiKey); + localStorage.setItem('TRIPO_API_KEY', tripoApiKey); + localStorage.setItem('MESHY_API_KEY', meshyApiKey); + localStorage.setItem('FAL_MODEL', selectedModel); + if (apiProvider === 'fal') fal.config({ credentials: apiKey }); + setActiveTab('generator'); + }; + + const handleImageUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setImageFile(file); + const url = URL.createObjectURL(file); + setImagePreview(url); + setGeneratedModelUrl(null); + setError(null); + } + }; + + // Utility to convert file to data URL for Fal API if needed + const fileToDataUrl = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }; + + const generate3DModel = async () => { + if (!imageFile) { + setError("Please upload an image first."); + return; + } + if (apiProvider === 'fal' && !apiKey) { + setError("Fal API Key is required. Please check settings."); + setActiveTab('settings'); + return; + } + if (apiProvider === 'tripo' && !tripoApiKey) { + setError("Tripo API Key is required. Please check settings."); + setActiveTab('settings'); + return; + } + if (apiProvider === 'meshy' && !meshyApiKey) { + setError("Meshy API Key is required. Please check settings."); + setActiveTab('settings'); + return; + } + + try { + setIsGenerating(true); + setError(null); + setProgressMessage("Uploading image..."); + + if (apiProvider === 'fal') { + fal.config({ credentials: apiKey }); + const dataUrl = await fileToDataUrl(imageFile); + setProgressMessage("Generating 3D Model... This might take up to a minute."); + + let inputPayload: any = { image_url: dataUrl }; + if (selectedModel.includes('tripo')) { + inputPayload = { image_url: dataUrl }; + } + + const result = await fal.subscribe(selectedModel, { + input: inputPayload, + logs: true, + onQueueUpdate: (update) => { + if (update.status === "IN_PROGRESS") { + update.logs?.map((log) => setProgressMessage(log.message)); + } + }, + }); + + console.log("Fal API Result:", result); + + let modelUrl = null; + if (result.data) { + if (result.data.model_3d && result.data.model_3d.url) { + modelUrl = result.data.model_3d.url; // Hunyuan v2 + } else if (result.data.video && result.data.video.url && result.data.video.url.endsWith('.glb')) { + modelUrl = result.data.video.url; + } else if (result.data.mesh_url) { + modelUrl = result.data.mesh_url; + } else if (result.data.model_url) { + modelUrl = result.data.model_url; + } + } + + if (modelUrl) { + setGeneratedModelUrl(modelUrl); + setProgressMessage("Done!"); + } else { + setError("Failed to extract 3D model from response."); + } + } else if (apiProvider === 'tripo') { + // TRIPO NATIVE API + const formData = new FormData(); + formData.append('file', imageFile); + + const uploadRes = await fetch('https://api.tripo3d.ai/v2/openapi/upload', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${tripoApiKey}` + }, + body: formData + }); + + if (!uploadRes.ok) throw new Error("Tripo API: Upload failed."); + const uploadData = await uploadRes.json(); + if (uploadData.code !== 0) throw new Error(`Tripo API Upload Error: ${uploadData.message || 'Unknown'}`); + + const imageToken = uploadData.data.image_token; + setProgressMessage("Generating 3D Model with Tripo3D... This might take up to a minute."); + + const taskRes = await fetch('https://api.tripo3d.ai/v2/openapi/task', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${tripoApiKey}` + }, + body: JSON.stringify({ + type: "image_to_model", + file: { + type: imageFile.type.includes('png') ? 'png' : 'jpg', + file_token: imageToken + } + }) + }); + + if (!taskRes.ok) throw new Error("Tripo API: Task creation failed."); + const taskData = await taskRes.json(); + if (taskData.code !== 0) throw new Error(`Tripo API Task Error: ${taskData.message || 'Unknown'}`); + + const taskId = taskData.data.task_id; + + let isDone = false; + while (!isDone) { + await new Promise(r => setTimeout(r, 2000)); + const pollRes = await fetch(`https://api.tripo3d.ai/v2/openapi/task/${taskId}`, { + headers: { + 'Authorization': `Bearer ${tripoApiKey}` + } + }); + if (!pollRes.ok) throw new Error("Tripo API: Polling failed."); + const pollData = await pollRes.json(); + if (pollData.code !== 0) throw new Error(`Tripo API Poll Error: ${pollData.message}`); + + const status = pollData.data.status; + const progress = pollData.data.progress || 0; + setProgressMessage(`Generating... ${progress}%`); + + if (status === 'success') { + const modelUrl = pollData.data.result.model.url || (pollData.data.result.pbr && pollData.data.result.pbr.model && pollData.data.result.pbr.model.url); + if (!modelUrl) throw new Error("Tripo API: Model URL not found in result."); + setGeneratedModelUrl(modelUrl); + setProgressMessage("Done!"); + isDone = true; + } else if (status === 'failed' || status === 'cancelled') { + throw new Error(`Tripo API Task ${status}`); + } + } + } else if (apiProvider === 'meshy') { + // MESHY NATIVE API + const dataUrl = await fileToDataUrl(imageFile); + setProgressMessage("Generating 3D Model with Meshy... This might take up to a minute."); + + let payload: any = { + image_url: dataUrl, + model_type: meshyModelType, + should_texture: meshyShouldTexture + }; + + if (meshyModelType !== 'lowpoly') { + payload.ai_model = meshyAiModel; + payload.should_remesh = meshyShouldRemesh; + if (meshyShouldRemesh) { + payload.topology = meshyTopology; + payload.target_polycount = meshyTargetPolycount; + } + } + + if (meshyShouldTexture) { + payload.enable_pbr = meshyEnablePbr; + if (meshyAiModel !== 'meshy-5') { + payload.hd_texture = meshyHdTexture; + } + if (meshyTextureImageUrl) { + payload.texture_image_url = meshyTextureImageUrl; + } else if (meshyTexturePrompt) { + payload.texture_prompt = meshyTexturePrompt; + } + } + + const taskRes = await fetch('https://api.meshy.ai/openapi/v1/image-to-3d', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${meshyApiKey}` + }, + body: JSON.stringify(payload) + }); + + if (!taskRes.ok) throw new Error("Meshy API: Task creation failed."); + const taskData = await taskRes.json(); + const taskId = taskData.result; + + let isDone = false; + while (!isDone) { + await new Promise(r => setTimeout(r, 2000)); + const pollRes = await fetch(`https://api.meshy.ai/openapi/v1/image-to-3d/${taskId}`, { + headers: { + 'Authorization': `Bearer ${meshyApiKey}` + } + }); + if (!pollRes.ok) throw new Error("Meshy API: Polling failed."); + const pollData = await pollRes.json(); + + const status = pollData.status; + const progress = pollData.progress || 0; + setProgressMessage(`Generating... ${progress}%`); + + if (status === 'SUCCEEDED') { + const modelUrl = pollData.model_urls?.glb; + if (!modelUrl) throw new Error("Meshy API: Model URL not found in result."); + setGeneratedModelUrl(modelUrl); + setProgressMessage("Done!"); + isDone = true; + } else if (status === 'FAILED' || status === 'EXPIRED') { + throw new Error(`Meshy API Task ${status}`); + } + } + } + } catch (err: any) { + console.error(err); + setError(err.message || "An error occurred during generation."); + } finally { + setIsGenerating(false); + } + }; + + if (!isOpen) return null; + + return ( + +
+ + {/* Header */} +
+
+ + Image to 3D Generation +
+ +
+ +
+ {/* Sidebar Tabs */} +
+ + +
+ + {/* Content Area */} +
+ {activeTab === 'settings' ? ( +
+
+

API Configuration

+

Configure your API credentials to access AI models.

+
+ +
+ + +
+ + {apiProvider === 'fal' ? ( + <> +
+ + setApiKey(e.target.value)} + placeholder="Enter your fal.ai API key" + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2.5 text-sm text-zinc-300 focus:outline-none focus:border-blue-500/50" + /> +
+ +
+ + +
+ + ) : apiProvider === 'tripo' ? ( +
+ + setTripoApiKey(e.target.value)} + placeholder="Enter your Tripo3D API key (starts with tsk_...)" + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2.5 text-sm text-zinc-300 focus:outline-none focus:border-blue-500/50" + /> +
+ ) : ( +
+ + setMeshyApiKey(e.target.value)} + placeholder="Enter your Meshy API key (starts with msy_...)" + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2.5 text-sm text-zinc-300 focus:outline-none focus:border-blue-500/50" + /> +
+ )} + +
+ + {keyValidationStatus === 'valid' && Valid Key} + {keyValidationStatus === 'invalid' && Invalid Key} + {keyValidationStatus === 'error' && Network Error} +
+ + +
+ ) : ( +
+ {/* Left Column: Upload */} +
+
+

Input Image

+ Front View +
+ + + + {apiProvider === 'meshy' && ( +
+

Meshy Settings

+ +
+
+ + +
+
+ + +
+
+ +
+ + + +
+ + {meshyShouldTexture && ( +
+ setMeshyTexturePrompt(e.target.value)} maxLength={600} placeholder="Texture prompt (optional)..." className="bg-zinc-950 border border-zinc-800 rounded px-2 py-1.5 text-xs text-zinc-300" /> + setMeshyTextureImageUrl(e.target.value)} placeholder="Texture Image URL (optional)..." className="bg-zinc-950 border border-zinc-800 rounded px-2 py-1.5 text-xs text-zinc-300" /> +
+ )} + +
+ +
+ + {meshyShouldRemesh && meshyModelType !== 'lowpoly' && ( +
+
+ + +
+
+ + setMeshyTargetPolycount(parseInt(e.target.value) || 30000)} className="bg-zinc-950 border border-zinc-800 rounded px-2 py-1.5 text-xs text-zinc-300" /> +
+
+ )} +
+ )} + + {error && ( +
+ +

{error}

+
+ )} + + +
+ + {/* Right Column: Output */} +
+

3D Preview

+
+ {generatedModelUrl ? ( + <> +
+ +
+
+ + + +
+ + ) : isGenerating ? ( +
+ +

{progressMessage}

+
+ ) : ( +
+ +

Your 3D model will appear here

+
+ )} +
+ + {generatedModelUrl && ( + + )} +
+
+ )} +
+
+
+
+
+ ); +}; diff --git a/src/components/JuiceBox.tsx b/src/components/JuiceBox.tsx new file mode 100644 index 0000000..049fc41 --- /dev/null +++ b/src/components/JuiceBox.tsx @@ -0,0 +1,112 @@ +import { useRef, useMemo, forwardRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import { RoundedBox, Text } from "@react-three/drei"; +import * as THREE from "three"; +import { Liquid } from "./LiquidShader"; + +export const JuiceBox = forwardRef(({ + color = "#4ade80", + roughness = 0.1, + metalness = 0.05, + clearcoat = 1.0, + transmission = 0, + thickness = 0, + ior = 1.5, + sheen = 0, + map = undefined, + uvScale = 1, + showLiquid = true, + liquidColor = "#ff9900", + ...props +}, ref) => { + const meshRef = useRef(null); + + // Create a custom texture for the juice box + const customTexture = useMemo(() => { + if (map) return null; + const canvas = document.createElement("canvas"); + canvas.width = 512; + canvas.height = 1024; + const ctx = canvas.getContext("2d"); + if (ctx) { + // Background + ctx.fillStyle = color; + ctx.fillRect(0, 0, 512, 1024); + + // Header + ctx.fillStyle = "#166534"; + ctx.fillRect(0, 0, 512, 100); + + // Text + ctx.fillStyle = "white"; + ctx.font = "bold 80px sans-serif"; + ctx.textAlign = "center"; + ctx.fillText("MAA", 256, 300); + + ctx.font = "bold 60px sans-serif"; + ctx.fillStyle = "#be123c"; + ctx.fillText("GUAVA", 256, 400); + + // Guava illustration (simple circles) + ctx.fillStyle = "#f472b6"; + ctx.beginPath(); + ctx.arc(256, 650, 150, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = "#166534"; + ctx.lineWidth = 10; + ctx.stroke(); + + ctx.fillStyle = "white"; + ctx.font = "24px sans-serif"; + ctx.fillText("REFRESHING FRUIT DRINK", 256, 150); + } + const tex = new THREE.CanvasTexture(canvas); + tex.needsUpdate = true; + return tex; + }, [color, map]); + + const uploadedTexture = useMemo(() => { + if (!map) return null; + const loader = new THREE.TextureLoader(); + const tex = loader.load(map); + tex.colorSpace = THREE.SRGBColorSpace; + tex.wrapS = tex.wrapT = THREE.RepeatWrapping; + tex.repeat.set(uvScale, uvScale); + return tex; + }, [map, uvScale]); + + return ( + + + 0} + envMapIntensity={1.5} + /> + + + {showLiquid && ( + + + + )} + + ); +}); diff --git a/src/components/LiquidShader.tsx b/src/components/LiquidShader.tsx new file mode 100644 index 0000000..5cc4b94 --- /dev/null +++ b/src/components/LiquidShader.tsx @@ -0,0 +1,134 @@ +import { useRef, useMemo } from "react"; +import { useFrame, useThree, extend, ThreeElement } from "@react-three/fiber"; +import * as THREE from "three"; +import { shaderMaterial } from "@react-three/drei"; + +// Type declaration for JSX +declare global { + namespace JSX { + interface IntrinsicElements { + liquidMaterialImpl: any; + } + } +} + +const LiquidMaterialImpl = shaderMaterial( + { + uTime: 0, + uColor: new THREE.Color("#ff9900"), + uResolution: new THREE.Vector2(), + uFresnelBias: 0.1, + uFresnelScale: 1.0, + uFresnelPower: 2.0, + uRefractionRatio: 0.98, + uIor: 1.33, + }, + // Vertex Shader + ` + #include + varying vec3 vNormal; + varying vec3 vViewPosition; + varying vec2 vUv; + varying vec3 vWorldPosition; + + void main() { + vUv = uv; + vNormal = normalize(normalMatrix * normal); + vec4 worldPosition = modelMatrix * vec4(position, 1.0); + vWorldPosition = worldPosition.xyz; + vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); + vViewPosition = -mvPosition.xyz; + #include + gl_Position = projectionMatrix * mvPosition; + } + `, + // Fragment Shader + ` + #include + + uniform float uTime; + uniform vec3 uColor; + uniform vec2 uResolution; + uniform float uFresnelBias; + uniform float uFresnelScale; + uniform float uFresnelPower; + uniform float uIor; + + varying vec3 vNormal; + varying vec3 vViewPosition; + varying vec2 vUv; + varying vec3 vWorldPosition; + + // Simple noise function for caustics and imperfections + float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); + } + + float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), f.x), + mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x), f.y); + } + + void main() { + #include + vec3 normal = normalize(vNormal); + vec3 viewDir = normalize(vViewPosition); + + // Fresnel effect + float fresnel = uFresnelBias + uFresnelScale * pow(1.0 + dot(viewDir, normal), uFresnelPower); + + // Caustics simulation + vec2 causticUv = vWorldPosition.xz * 2.0 + vWorldPosition.y * 0.5; + float caustic = noise(causticUv * 5.0 + uTime * 0.5) * 0.5 + 0.5; + caustic *= noise(causticUv * 10.0 - uTime * 0.3) * 0.5 + 0.5; + caustic = pow(caustic, 3.0) * 1.5; + + // Surface imperfections + float imperfection = noise(vUv * 20.0 + uTime * 0.1) * 0.05; + + vec3 finalColor = uColor; + finalColor += caustic * 0.2 * uColor; + finalColor -= imperfection; + + // Simple refraction-like look by blending with fresnel + vec3 skyColor = vec3(0.8, 0.9, 1.0); + finalColor = mix(finalColor, skyColor, fresnel * 0.3); + + gl_FragColor = vec4(finalColor, 0.9); + } + ` +); + +extend({ LiquidMaterialImpl }); + +export function Liquid({ color = "#ff9900", size = [0.9, 1.8, 0.5] }) { + const materialRef = useRef(null); + const { size: winSize } = useThree(); + + useFrame((state) => { + if (materialRef.current) { + const mat = materialRef.current; + if (mat.uTime !== undefined) mat.uTime = state.clock.elapsedTime; + if (mat.uResolution && mat.uResolution.set) { + mat.uResolution.set(winSize.width || 800, winSize.height || 600); + } + } + }); + + return ( + + + {/* @ts-ignore */} + + + ); +} diff --git a/src/components/ModelViewer.tsx b/src/components/ModelViewer.tsx new file mode 100644 index 0000000..184ffd4 --- /dev/null +++ b/src/components/ModelViewer.tsx @@ -0,0 +1,271 @@ +import { useRef, useEffect, forwardRef, useMemo, useState } from "react"; +import * as THREE from "three"; +import { useFrame, useThree, createPortal } from "@react-three/fiber"; +import { MaterialFactory } from "../materials/MaterialFactory"; +import { useTexturePipeline } from "../hooks/useTexturePipeline"; + +interface ModelViewerProps { + object: THREE.Object3D | null; + sceneObjects: any[]; +} + +interface MeshMaterialManagerProps { + mesh: THREE.Mesh; + sceneObj: any; + textureCache: React.MutableRefObject>; +} + +const MeshMaterialManager = ({ mesh, sceneObj, textureCache }: MeshMaterialManagerProps) => { + useEffect(() => { + if (!mesh || !sceneObj.materialProps) return; + + const props = sceneObj.materialProps; + const mappingType = props.mappingType || 'uv'; + const uvScale = props.uvScale || 1; + const uvTiling = props.uvTiling || [1, 1]; + const uvOffset = props.uvOffset || [0, 0]; + const uvRotation = props.uvRotation || 0; + + const origMat = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as THREE.MeshPhysicalMaterial; + + const baseParams: any = { + color: origMat.color, + roughness: origMat.roughness !== undefined ? origMat.roughness : 0.5, + metalness: origMat.metalness !== undefined ? origMat.metalness : 0, + map: origMat.map, + normalMap: origMat.normalMap, + roughnessMap: origMat.roughnessMap, + metalnessMap: origMat.metalnessMap, + aoMap: origMat.aoMap, + emissiveMap: origMat.emissiveMap, + emissive: origMat.emissive, + emissiveIntensity: origMat.emissiveIntensity, + envMapIntensity: origMat.envMapIntensity, + alphaMap: origMat.alphaMap, + transparent: origMat.transparent, + opacity: origMat.opacity, + side: origMat.side, + alphaTest: origMat.alphaTest, + }; + + // Use MaterialFactory to get reusable materials + let activeMat: any; + if (mappingType === 'triplanar') { + activeMat = MaterialFactory.getInstance().getTriplanarMaterial( + baseParams, + mesh.uuid + ); + + activeMat.triplanarUniforms.uTriplanarScale.value = uvScale; + activeMat.triplanarUniforms.uBlendSharpness.value = 5.0; // Default or from props + } else { + activeMat = MaterialFactory.getInstance().getStandardUVMaterial( + baseParams, + mesh.uuid + ); + + let modeIndex = 0; + if (mappingType === 'planar') modeIndex = 1; + else if (mappingType === 'box') modeIndex = 2; + else if (mappingType === 'cylinder') modeIndex = 3; + + activeMat.uvUniforms.uMappingMode.value = modeIndex; + activeMat.updateUVTransform(uvOffset, [uvTiling[0] * uvScale, uvTiling[1] * uvScale], uvRotation * (Math.PI / 180)); + } + + // Apply basic props + if (props.color) activeMat.color.set(props.color); + if (props.roughness !== undefined) activeMat.roughness = props.roughness; + if (props.metalness !== undefined) activeMat.metalness = props.metalness; + if (props.opacity !== undefined) activeMat.opacity = props.opacity; + if (props.transmission !== undefined) activeMat.transmission = props.transmission; + activeMat.transparent = (props.transmission > 0) || (props.opacity < 1) || !!props.alphaMap; + + if (Array.isArray(mesh.material)) { + mesh.material = mesh.material.map(() => activeMat); + } else { + mesh.material = activeMat; + } + + // Textures + const textureTypes: ('map' | 'normalMap' | 'specularMap' | 'alphaMap')[] = ['map', 'normalMap', 'specularMap', 'alphaMap']; + textureTypes.forEach(type => { + const url = props[type]; + if (url) { + if (!activeMat[type] || activeMat[type]!.name !== url) { + if (textureCache.current.has(url)) { + activeMat[type] = textureCache.current.get(url)!; + if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = activeMat[type]; + activeMat.needsUpdate = true; + } else { + const loader = new THREE.TextureLoader(); + loader.load(url, (tex) => { + tex.name = url; + if (type === 'map') tex.colorSpace = THREE.SRGBColorSpace; + tex.wrapS = tex.wrapT = THREE.RepeatWrapping; + textureCache.current.set(url, tex); + activeMat[type] = tex; + if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = tex; + activeMat.needsUpdate = true; + }); + } + } + } else if (url === null && activeMat[type]) { + activeMat[type] = null; + if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = null; + activeMat.needsUpdate = true; + } + }); + + mesh.visible = sceneObj.visible; + mesh.castShadow = true; + mesh.receiveShadow = true; + mesh.frustumCulled = false; + + // Sync individual mesh transform + if (sceneObj.transformProps) { + const { position, rotation, scale } = sceneObj.transformProps; + mesh.position.set(position[0], position[1], position[2]); + mesh.rotation.set(rotation[0], rotation[1], rotation[2]); + mesh.scale.set(scale[0], scale[1], scale[2]); + } + + }, [sceneObj, mesh]); + + return null; +}; + +const FillMesh = ({ fillProps, originalMesh }: { fillProps: any, originalMesh: THREE.Mesh }) => { + const materialRef = useRef(null); + const [clippingPlane] = useState(() => new THREE.Plane(new THREE.Vector3(0, -1, 0), 0)); + const { size: winSize } = useThree(); + + // Clone geometry and fix interpolation + const geometry = useMemo(() => { + const geo = originalMesh.geometry.clone(); + geo.computeVertexNormals(); + return geo; + }, [originalMesh.geometry]); + + // Update clipping plane based on height + useEffect(() => { + geometry.computeBoundingBox(); + const box = geometry.boundingBox || new THREE.Box3().setFromObject(originalMesh); + const minY = box.min.y; + const maxY = box.max.y; + // height is 0 to 1 + const clipY = minY + (maxY - minY) * (fillProps.height ?? 0.5); + // Since this plane is applied in world space by Three.js by default, + // wait, localClippingEnabled = true means the plane is in LOCAL space! + // If it's local space, clipY is exact. + clippingPlane.constant = clipY; + }, [geometry, fillProps.height, originalMesh]); + + useFrame((state) => { + if (materialRef.current && fillProps.type === 'liquid') { + const mat = materialRef.current; + if (mat.uTime !== undefined) mat.uTime = state.clock.elapsedTime; + if (mat.uResolution && mat.uResolution.set) { + mat.uResolution.set(winSize.width || 800, winSize.height || 600); + } + } + }); + + const fillNode = ( + + {fillProps.type === 'liquid' ? ( + // @ts-ignore + + ) : ( + + )} + + ); + + return createPortal(fillNode, originalMesh); +}; + +export const ModelViewer = forwardRef(({ object, sceneObjects }, ref) => { + const internalRef = useRef(null); + const groupRef = (ref as any) || internalRef; + const textureCache = useRef>(new Map()); + const meshCache = useRef>(new Map()); + + useTexturePipeline(object); + + // Cleanup textures on unmount + useEffect(() => { + return () => { + textureCache.current.forEach(tex => tex.dispose()); + textureCache.current.clear(); + meshCache.current.clear(); + }; + }, []); + + // Build mesh cache when object changes + useEffect(() => { + meshCache.current.clear(); + if (object) { + let mIdx = 0; + object.traverse((child) => { + if (child instanceof THREE.Mesh) { + const stableId = `mesh-${mIdx}`; + meshCache.current.set(stableId, child); + mIdx++; + } + }); + } + }, [object]); + + // Main transform update for the core model + useEffect(() => { + if (object && sceneObjects) { + const mainModel = sceneObjects.find(obj => obj.type === 'model'); + if (mainModel && mainModel.transformProps && groupRef.current) { + const { position: p, rotation: r, scale: s } = mainModel.transformProps; + groupRef.current.position.set(p[0], p[1], p[2]); + groupRef.current.rotation.set(r[0], r[1], r[2]); + groupRef.current.scale.set(s[0], s[1], s[2]); + } + } + }, [object, sceneObjects]); + + if (!object) return null; + + const meshObjects = sceneObjects.filter(obj => obj.type === 'mesh'); + + return ( + + + {meshObjects.map(obj => { + const mesh = meshCache.current.get(obj.id); + if (!mesh) return null; + return ( + + + {obj.fillProps?.enabled && ( + + )} + + ); + })} + + ); +}); diff --git a/src/components/RenderModal.tsx b/src/components/RenderModal.tsx new file mode 100644 index 0000000..5ba94b4 --- /dev/null +++ b/src/components/RenderModal.tsx @@ -0,0 +1,252 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Download, Image as ImageIcon, Layers, Settings, Monitor, Smartphone, Maximize2 } from 'lucide-react'; +import { cn } from '../lib/utils'; + +interface RenderModalProps { + isOpen: boolean; + onClose: () => void; + onRender: (settings: RenderSettings) => void; +} + +export interface RenderSettings { + width: number; + height: number; + includeAlpha: boolean; + format: 'png' | 'jpg'; + name: string; + folder?: string; +} + +const RESOLUTION_PRESETS = [ + { label: '7680 x 4800 (8K)', w: 7680, h: 4800 }, + { label: '3840 x 2400 (4K)', w: 3840, h: 2400 }, + { label: '2560 x 1600 (2K)', w: 2560, h: 1600 }, + { label: '1920 x 1200 (Full HD)', w: 1920, h: 1200 }, + { label: '1600 x 1000', w: 1600, h: 1000 }, + { label: '1280 x 800', w: 1280, h: 800 }, + { label: '1024 x 640', w: 1024, h: 640 }, + { label: '800 x 500', w: 800, h: 500 }, + { label: '640 x 400', w: 640, h: 400 }, +]; + +export const RenderModal: React.FC = ({ isOpen, onClose, onRender }) => { + const [settings, setSettings] = useState({ + width: 1920, + height: 1200, + includeAlpha: true, + format: 'png', + name: 'Render_1', + folder: 'C:/Users/Render/Documents' + }); + + const [activeTab, setActiveTab] = useState('Output'); + + if (!isOpen) return null; + + return ( + +
+ + {/* Header */} +
+ Render + +
+ +
+ {/* Sidebar */} +
+ {['Output', 'Options', 'Queue'].map((tab) => ( + + ))} +
+ + {/* Content */} +
+
+ {/* Mode Tabs */} +
+ {['Still Image', 'Animation', 'VIZ3D XR', 'Configurator', 'CMF'].map((mode) => ( + + ))} +
+ + {/* Name & Resolution */} +
+
+ + setSettings(prev => ({ ...prev, name: e.target.value }))} + className="bg-zinc-950 border border-zinc-800 rounded px-3 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-blue-500/50" + /> +
+ +
+ +
+ setSettings(prev => ({ ...prev, folder: e.target.value }))} + className="flex-1 bg-zinc-950 border border-zinc-800 rounded px-3 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-blue-500/50" + /> + +
+
+ +
+ +
+
+ Format + +
+
+ +
+ +
+
+ W: + setSettings(prev => ({ ...prev, width: parseInt(e.target.value) || 0 }))} + className="w-20 bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300" + /> + px +
+
+ H: + setSettings(prev => ({ ...prev, height: parseInt(e.target.value) || 0 }))} + className="w-20 bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300" + /> + px +
+ +
+
+
+ + {/* Collapsible Sections */} +
+
+ Layers and Passes + +
+
+
+
+ Region +
+ +
+
+
+
+
+ + {/* Footer */} +
+ + +
+ +
+ + ); +}; + +const ChevronDown = ({ size, className }: { size: number, className?: string }) => ( + + + +); diff --git a/src/components/UVEditor.tsx b/src/components/UVEditor.tsx new file mode 100644 index 0000000..47d1c73 --- /dev/null +++ b/src/components/UVEditor.tsx @@ -0,0 +1,453 @@ +import React, { useState, useRef, useEffect, Suspense, useMemo, useCallback } from 'react'; +import { X, Upload, Trash2, RotateCw, Eye, EyeOff } from 'lucide-react'; +import { Canvas } from '@react-three/fiber'; +import { OrbitControls, Environment } from '@react-three/drei'; +import * as THREE from 'three'; +import { ModelViewer } from './ModelViewer'; + +interface Layer { + id: string; + url: string; + img?: HTMLImageElement; + x: number; y: number; + scaleX: number; scaleY: number; + rotation: number; + cropL: number; cropT: number; cropR: number; cropB: number; + visible: boolean; +} + +type Handle = 'move' | 'rotate' | 'scaleNW' | 'scaleNE' | 'scaleSW' | 'scaleSE' | 'scaleN' | 'scaleS' | 'scaleE' | 'scaleW'; + +interface UVEditorProps { + onClose: () => void; + onSave: (dataUrl: string, mappingType: string, layers: any[], bgColor: string, bgTransparent: boolean) => void; + targetObject?: any; + loadedModel?: THREE.Group | null; + sceneObjects?: any[]; +} + +const CANVAS_SIZE = 1024; + +function deg2rad(d: number) { return d * Math.PI / 180; } + +export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObjects }: UVEditorProps) { + const initBg = targetObject?.materialProps?.uvBackground ?? '#1a1a2e'; + const initTrans = targetObject?.materialProps?.uvTransparent ?? false; + + const [layers, setLayers] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [mappingType, setMappingType] = useState(targetObject?.materialProps?.mappingType || 'uv'); + const [bgColor, setBgColor] = useState(initBg); + const [bgTransparent, setBgTransparent] = useState(initTrans); + + const containerRef = useRef(null); + const fileInputRef = useRef(null); + const dragRef = useRef<{ handle: Handle; startX: number; startY: number; layer: Layer } | null>(null); + + useEffect(() => { + const saved = targetObject?.materialProps?.uvLayers; + if (!saved?.length) return; + let n = 0; + const init: Layer[] = saved.map((l: any) => ({ ...l })); + init.forEach(layer => { + const img = new Image(); + img.onload = () => { layer.img = img; n++; if (n === init.length) setLayers([...init]); }; + img.src = layer.url; + }); + }, []); + + const renderCanvas = useCallback((lrs: Layer[], transparent: boolean, bg: string): string => { + const c = document.createElement('canvas'); + c.width = c.height = CANVAS_SIZE; + const ctx = c.getContext('2d')!; + if (!transparent) { ctx.fillStyle = bg; ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); } + lrs.forEach(layer => { + if (!layer.img || layer.visible === false) return; + const iw = layer.img.width, ih = layer.img.height; + const sx = layer.cropL * iw, sy = layer.cropT * ih; + const sw = (1 - layer.cropL - layer.cropR) * iw; + const sh = (1 - layer.cropT - layer.cropB) * ih; + ctx.save(); + ctx.translate(layer.x, layer.y); + ctx.rotate(deg2rad(layer.rotation)); + ctx.scale(layer.scaleX, layer.scaleY); + ctx.drawImage(layer.img, sx, sy, sw, sh, -sw / 2, -sh / 2, sw, sh); + ctx.restore(); + }); + return c.toDataURL(transparent ? 'image/png' : 'image/jpeg'); + }, []); + + useEffect(() => { + setPreviewUrl(renderCanvas(layers, bgTransparent, bgColor)); + }, [layers, bgTransparent, bgColor]); + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; if (!file) return; + const reader = new FileReader(); + reader.onload = ev => { + const url = ev.target?.result as string; + const img = new Image(); + img.onload = () => { + const nl: Layer = { id: `l-${Date.now()}`, url, img, x: CANVAS_SIZE / 2, y: CANVAS_SIZE / 2, scaleX: 1, scaleY: 1, rotation: 0, cropL: 0, cropT: 0, cropR: 0, cropB: 0, visible: true }; + setLayers(p => [...p, nl]); + setSelectedId(nl.id); + }; + img.src = url; + }; + reader.readAsDataURL(file); + e.target.value = ''; + }; + + const updateLayer = (id: string, u: Partial) => setLayers(p => p.map(l => l.id === id ? { ...l, ...u } : l)); + const selectedLayer = layers.find(l => l.id === selectedId) ?? null; + + const getSF = () => { + const r = containerRef.current?.getBoundingClientRect(); + return r ? CANVAS_SIZE / r.width : 1; + }; + + const onHandleDown = (e: React.MouseEvent, handle: Handle, layer: Layer) => { + e.stopPropagation(); + setSelectedId(layer.id); + dragRef.current = { handle, startX: e.clientX, startY: e.clientY, layer: { ...layer } }; + e.preventDefault(); + }; + + // Snap helper: snap value to nearest multiple of snapSize + const snap = (v: number, snapSize: number) => Math.round(v / snapSize) * snapSize; + + const onMouseMove = (e: React.MouseEvent) => { + const d = dragRef.current; + if (!d) return; + const sf = getSF(); + const dx = (e.clientX - d.startX) * sf; + const dy = (e.clientY - d.startY) * sf; + const isShift = e.shiftKey; + const SNAP_PX = 16; // grid snap in canvas units + + if (d.handle === 'move') { + let nx = d.layer.x + dx; + let ny = d.layer.y + dy; + // Clamp to canvas bounds with soft margin + nx = Math.max(0, Math.min(CANVAS_SIZE, nx)); + ny = Math.max(0, Math.min(CANVAS_SIZE, ny)); + // Hold Shift to snap to grid + if (isShift) { nx = snap(nx, SNAP_PX); ny = snap(ny, SNAP_PX); } + // Center snapping: snap near canvas center + if (Math.abs(nx - CANVAS_SIZE / 2) < SNAP_PX) nx = CANVAS_SIZE / 2; + if (Math.abs(ny - CANVAS_SIZE / 2) < SNAP_PX) ny = CANVAS_SIZE / 2; + updateLayer(d.layer.id, { x: nx, y: ny }); + } else if (d.handle === 'rotate') { + let rot = d.layer.rotation + dx / 2; + // Snap rotation to 15° increments when Shift held + if (isShift) rot = snap(rot, 15); + updateLayer(d.layer.id, { rotation: rot }); + } else if (d.handle.startsWith('scale')) { + if (isShift) { + // Shift held: stretch X and Y independently + const sx = Math.max(0.05, d.layer.scaleX + dx / 200); + const sy = Math.max(0.05, d.layer.scaleY + dy / 200); + updateLayer(d.layer.id, { scaleX: sx, scaleY: sy }); + } else { + // No shift: uniform scale driven by the larger axis + const delta = (Math.abs(dx) > Math.abs(dy) ? dx : dy) / 200; + const sx = Math.max(0.05, d.layer.scaleX + delta); + const sy = Math.max(0.05, d.layer.scaleY + delta); + updateLayer(d.layer.id, { scaleX: sx, scaleY: sy }); + } + } + }; + + const onMouseUp = () => { dragRef.current = null; }; + + const handleSave = () => { + if (previewUrl) { + const serialized = layers.map(({ img, ...rest }) => rest); + onSave(previewUrl, mappingType, serialized, bgColor, bgTransparent); + } + onClose(); + }; + + const PreviewModel = () => { + const mockObjs = useMemo(() => { + if (!sceneObjects) return []; + return sceneObjects.map(obj => obj.id === targetObject?.id + ? { ...obj, materialProps: { ...obj.materialProps, map: previewUrl, mappingType } } + : obj + ); + }, [sceneObjects, previewUrl, mappingType]); + const cloned = useMemo(() => loadedModel?.clone() ?? null, [loadedModel]); + if (!targetObject) return ; + if (targetObject.type === 'cube') { + const tex = previewUrl ? (() => { const t = new THREE.TextureLoader().load(previewUrl); t.colorSpace = THREE.SRGBColorSpace; t.flipY = false; return t; })() : null; + return ; + } + return ; + }; + + // Bounding box overlay rendered on canvas div + const BoundingBox = ({ layer }: { layer: Layer }) => { + const iw = (layer.img?.width ?? 200) * (1 - layer.cropL - layer.cropR); + const ih = (layer.img?.height ?? 200) * (1 - layer.cropT - layer.cropB); + const rect = containerRef.current?.getBoundingClientRect(); + if (!rect) return null; + const pct = (v: number) => `${(v / CANVAS_SIZE) * 100}%`; + const w = (iw * layer.scaleX / CANVAS_SIZE) * 100; + const h = (ih * layer.scaleY / CANVAS_SIZE) * 100; + const lx = (layer.x / CANVAS_SIZE) * 100; + const ly = (layer.y / CANVAS_SIZE) * 100; + + const cornerStyle = (cx: string, cy: string, cursor: string): React.CSSProperties => ({ + position: 'absolute', left: cx, top: cy, width: 10, height: 10, + background: '#ffffff', border: '2px solid #3b82f6', borderRadius: 2, + transform: 'translate(-50%,-50%)', cursor, zIndex: 10, touchAction: 'none' + }); + const edgeStyle = (cx: string, cy: string, cursor: string): React.CSSProperties => ({ + position: 'absolute', left: cx, top: cy, width: 8, height: 8, + background: '#3b82f6', borderRadius: '50%', + transform: 'translate(-50%,-50%)', cursor, zIndex: 10 + }); + + return ( +
+ {/* Corners - scale */} + {(['NW','NE','SW','SE'] as const).map(c => ( +
onHandleDown(e, `scale${c}` as Handle, layer)} + /> + ))} + {/* Edge midpoints - scale */} + {[ + { id: 'N', cx: '50%', cy: '0%', cur: 'ns-resize' }, + { id: 'S', cx: '50%', cy: '100%', cur: 'ns-resize' }, + { id: 'E', cx: '100%', cy: '50%', cur: 'ew-resize' }, + { id: 'W', cx: '0%', cy: '50%', cur: 'ew-resize' }, + ].map(({ id, cx, cy, cur }) => ( +
onHandleDown(e, `scale${id}` as Handle, layer)} /> + ))} + {/* Rotate handle - top center above box */} +
onHandleDown(e, 'rotate', layer)} title="Rotate" /> + {/* Line from box to rotate handle */} +
+
+ ); + }; + + return ( +
+ {/* Top Bar */} +
+
+ +
+ UV Map +
+
+ + {!bgTransparent && ( +
+ +
+ {/* Left sidebar */} +
+
+ + +
+

Layers

+ {layers.length === 0 && ( +
Upload an image to start
+ )} + {[...layers].reverse().map((layer, i) => ( +
setSelectedId(layer.id)} + className={`flex items-center gap-2 p-2 rounded-lg border cursor-pointer transition-all ${selectedId === layer.id ? 'border-blue-500 bg-blue-500/10' : 'border-zinc-800 hover:border-zinc-600 bg-zinc-800/20'} ${!layer.visible ? 'opacity-40' : ''}`}> + + Layer {layers.length - i} + + +
+ ))} + + {/* Layer props */} + {selectedLayer && ( +
+

Transform

+
+ {(['x', 'y'] as const).map(ax => ( +
+

{ax}

+ updateLayer(selectedLayer.id, { [ax]: +e.target.value })} + className="w-full bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-[11px] text-blue-400 font-mono outline-none focus:border-blue-500" /> +
+ ))} +
+
+ {(['scaleX', 'scaleY'] as const).map(ax => ( +
+

{ax === 'scaleX' ? 'Scl X' : 'Scl Y'}

+ updateLayer(selectedLayer.id, { [ax]: Math.max(0.01, +e.target.value) })} + className="w-full bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-[11px] text-blue-400 font-mono outline-none focus:border-blue-500" /> +
+ ))} +
+
+
+ Rotation{Math.round(selectedLayer.rotation)}° +
+ updateLayer(selectedLayer.id, { rotation: +e.target.value })} + className="w-full h-1 bg-zinc-700 rounded appearance-none cursor-pointer accent-blue-500" /> +
+ +
+

Crop

+ {(['cropL', 'cropT', 'cropR', 'cropB'] as const).map(s => ( +
+ {s.slice(4)} + updateLayer(selectedLayer.id, { [s]: +e.target.value })} + className="flex-1 h-1 bg-zinc-700 rounded appearance-none cursor-pointer accent-orange-500" /> + {Math.round(selectedLayer[s] * 100)}% +
+ ))} +
+ )} +
+
+ + {/* Canvas area */} +
setSelectedId(null)}> +
+ + {/* Layers */} + {layers.map(layer => { + if (layer.visible === false) return null; + const iw = (layer.img?.width ?? 200) * (1 - layer.cropL - layer.cropR); + const ih = (layer.img?.height ?? 200) * (1 - layer.cropT - layer.cropB); + const isSelected = selectedId === layer.id; + return ( +
{ e.stopPropagation(); onHandleDown(e, 'move', layer); }} + className="absolute cursor-move" + style={{ + left: `${(layer.x / CANVAS_SIZE) * 100}%`, + top: `${(layer.y / CANVAS_SIZE) * 100}%`, + transform: `translate(-50%,-50%) rotate(${layer.rotation}deg) scale(${layer.scaleX},${layer.scaleY})`, + zIndex: isSelected ? 4 : 2, + transition: 'transform 0.04s linear', + }}> + {layer.img && ( +
+ +
+ )} +
+ ); + })} + + {/* Bounding box overlays - only for visible selected layer */} + {layers.map(layer => selectedId === layer.id && layer.visible !== false && )} + +
+ {CANVAS_SIZE}×{CANVAS_SIZE} +
+
+
+ + {/* Right panel - preview */} +
+
+
+ + 3D Preview +
+
+ + + + + + + +
+
+ + + +
+

Legend

+
+
Drag corner/edge — uniform scale
+
Shift + drag — free X/Y stretch
+
Drag top handle — rotate
+
Drag layer — move (Shift snaps)
+
+
+
+
+
+ ); +} diff --git a/src/components/UnifiedMappingMaterial.tsx b/src/components/UnifiedMappingMaterial.tsx new file mode 100644 index 0000000..a85d2a9 --- /dev/null +++ b/src/components/UnifiedMappingMaterial.tsx @@ -0,0 +1,176 @@ +import React, { useMemo, useRef, useEffect } from 'react'; +import * as THREE from 'three'; +import { useTexture } from '@react-three/drei'; + +// Mapping Types Enum +export const MAPPING_TYPES = { + UV: 0, + PLANAR: 1, + CYLINDER: 2, + SPHERE: 3, + BOX: 4, +}; + +const vertexShader = ` + varying vec2 vUv; + varying vec3 vLocalPos; + varying vec3 vNormal; + varying vec3 vWorldPos; + + void main() { + vUv = uv; + vLocalPos = position; + vNormal = normalize(normal); + vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; + +const fragmentShader = ` + uniform sampler2D uTexture; + uniform int uMappingType; + uniform float uScale; + uniform vec2 uTiling; + uniform vec2 uOffset; + uniform float uRotation; + uniform vec3 uBBoxMin; + uniform vec3 uBBoxMax; + + varying vec2 vUv; + varying vec3 vLocalPos; + varying vec3 vNormal; + + #ifndef PI + #define PI 3.14159265359 + #endif + + vec2 rotateUV(vec2 uv, float rotation) { + float mid = 0.5; + float cosAngle = cos(rotation); + float sinAngle = sin(rotation); + float dx = uv.x - mid; + float dy = uv.y - mid; + return vec2( + cosAngle * dx - sinAngle * dy + mid, + sinAngle * dx + cosAngle * dy + mid + ); + } + + void main() { + vec2 finalUv = vec2(0.0); + + // Bounding box calculations for dynamic projections + vec3 boundsSize = uBBoxMax - uBBoxMin; + vec3 boundsPos = (vLocalPos - uBBoxMin) / (boundsSize + 0.00001); + vec3 center = (uBBoxMax + uBBoxMin) * 0.5; + vec3 dir = vLocalPos - center; + vec3 normDir = normalize(dir + 0.00001); + + if (uMappingType == 0) { + // UV: Applies native geometry vUv + finalUv = vUv; + } else if (uMappingType == 1) { + // PLANAR (XZ / Top-Down) + finalUv = boundsPos.xz; + } else if (uMappingType == 2) { + // CYLINDER: u = angle around Y, v = normalized height + vec3 localRel = vLocalPos - center; + float u = (atan(localRel.z, localRel.x) / (2.0 * PI)) + 0.5; + float v = boundsPos.y; + finalUv = vec2(u, v); + } else if (uMappingType == 3) { + // SPHERE + float u = (atan(normDir.z, normDir.x) / (2.0 * PI)) + 0.5; + float v = (asin(clamp(normDir.y, -1.0, 1.0)) / PI) + 0.5; + finalUv = vec2(u, v); + } else if (uMappingType == 4) { + // BOX (Triplanar) + vec3 absNormal = abs(vNormal); + if (absNormal.x > absNormal.y && absNormal.x > absNormal.z) { + finalUv = boundsPos.zy; + } else if (absNormal.y > absNormal.x && absNormal.y > absNormal.z) { + finalUv = boundsPos.xz; + } else { + finalUv = boundsPos.xy; + } + } + + // Apply Tiling, Scale, and Offset + finalUv = finalUv * uTiling * uScale + uOffset; + + // Apply Rotation + if (uRotation != 0.0) { + finalUv = rotateUV(finalUv, uRotation); + } + + vec4 texColor = texture2D(uTexture, finalUv); + gl_FragColor = texColor; + } +`; + +export const UnifiedMappingMaterial = ({ + textureUrl, + mappingType = 'UV', + uvScale = 1.0, + uvTiling = [1, 1], + uvOffset = [0, 0], + uvRotation = 0, + geometry +}) => { + const materialRef = useRef(null); + + // Load texture + const texture = useTexture(textureUrl) as THREE.Texture; + + useEffect(() => { + if (texture) { + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + texture.needsUpdate = true; + } + }, [texture]); + + // Initialize Uniforms + const uniforms = useMemo(() => ({ + uTexture: { value: texture }, + uMappingType: { value: MAPPING_TYPES[mappingType as keyof typeof MAPPING_TYPES] || 0 }, + uScale: { value: uvScale }, + uTiling: { value: new THREE.Vector2(uvTiling[0], uvTiling[1]) }, + uOffset: { value: new THREE.Vector2(uvOffset[0], uvOffset[1]) }, + uRotation: { value: uvRotation * (Math.PI / 180) }, + uBBoxMin: { value: new THREE.Vector3(-1, -1, -1) }, + uBBoxMax: { value: new THREE.Vector3(1, 1, 1) } + }), [texture]); + + // Real-time Uniform Updates + useEffect(() => { + if (materialRef.current) { + const u = materialRef.current.uniforms; + u.uMappingType.value = MAPPING_TYPES[mappingType as keyof typeof MAPPING_TYPES] || 0; + u.uScale.value = uvScale; + u.uTiling.value.set(uvTiling[0], uvTiling[1]); + u.uOffset.value.set(uvOffset[0], uvOffset[1]); + u.uRotation.value = uvRotation * (Math.PI / 180); + } + }, [mappingType, uvScale, uvTiling, uvOffset, uvRotation]); + + // Bounding Box Update + useEffect(() => { + if (geometry && materialRef.current) { + geometry.computeBoundingBox(); + const box = geometry.boundingBox || new THREE.Box3().setFromObject(new THREE.Mesh(geometry)); + materialRef.current.uniforms.uBBoxMin.value.copy(box.min); + materialRef.current.uniforms.uBBoxMax.value.copy(box.max); + } + }, [geometry]); + + return ( + + ); +}; diff --git a/src/core/uv/boxProjection.ts b/src/core/uv/boxProjection.ts new file mode 100644 index 0000000..3beb704 --- /dev/null +++ b/src/core/uv/boxProjection.ts @@ -0,0 +1,72 @@ +import { UVGenerationContext, computeBoundingBox, normalizeCoordinate } from './uvUtils'; + +export function generateBoxUVs(context: UVGenerationContext): Float32Array { + const { positions, normals } = context; + const numVertices = positions.length / 3; + const uvs = new Float32Array(numVertices * 2); + const bbox = computeBoundingBox(positions); + + if (!normals) { + // Fallback if no normals provided: use position to determine primary axis from center + for (let i = 0; i < numVertices; i++) { + const x = positions[i * 3]; + const y = positions[i * 3 + 1]; + const z = positions[i * 3 + 2]; + + const dx = Math.abs(x - bbox.center[0]); + const dy = Math.abs(y - bbox.center[1]); + const dz = Math.abs(z - bbox.center[2]); + + let u = 0; + let v = 0; + + if (dx >= dy && dx >= dz) { + u = normalizeCoordinate(z, bbox.min[2], bbox.size[2]); + v = normalizeCoordinate(y, bbox.min[1], bbox.size[1]); + } else if (dy >= dx && dy >= dz) { + u = normalizeCoordinate(x, bbox.min[0], bbox.size[0]); + v = normalizeCoordinate(z, bbox.min[2], bbox.size[2]); + } else { + u = normalizeCoordinate(x, bbox.min[0], bbox.size[0]); + v = normalizeCoordinate(y, bbox.min[1], bbox.size[1]); + } + + uvs[i * 2] = u; + uvs[i * 2 + 1] = v; + } + return uvs; + } + + // Use normals to determine mapping face + for (let i = 0; i < numVertices; i++) { + const x = positions[i * 3]; + const y = positions[i * 3 + 1]; + const z = positions[i * 3 + 2]; + + const nx = Math.abs(normals[i * 3]); + const ny = Math.abs(normals[i * 3 + 1]); + const nz = Math.abs(normals[i * 3 + 2]); + + let u = 0; + let v = 0; + + if (nx >= ny && nx >= nz) { + u = normalizeCoordinate(z, bbox.min[2], bbox.size[2]); + v = normalizeCoordinate(y, bbox.min[1], bbox.size[1]); + if (normals[i * 3] < 0) u = 1 - u; // Flip for negative X + } else if (ny >= nx && ny >= nz) { + u = normalizeCoordinate(x, bbox.min[0], bbox.size[0]); + v = normalizeCoordinate(z, bbox.min[2], bbox.size[2]); + if (normals[i * 3 + 1] < 0) u = 1 - u; // Flip for negative Y + } else { + u = normalizeCoordinate(x, bbox.min[0], bbox.size[0]); + v = normalizeCoordinate(y, bbox.min[1], bbox.size[1]); + if (normals[i * 3 + 2] < 0) u = 1 - u; // Flip for negative Z + } + + uvs[i * 2] = u; + uvs[i * 2 + 1] = v; + } + + return uvs; +} diff --git a/src/core/uv/cylindricalProjection.ts b/src/core/uv/cylindricalProjection.ts new file mode 100644 index 0000000..3535495 --- /dev/null +++ b/src/core/uv/cylindricalProjection.ts @@ -0,0 +1,42 @@ +import { UVGenerationContext, computeBoundingBox, normalizeCoordinate } from './uvUtils'; + +export function generateCylindricalUVs(context: UVGenerationContext, axis: 'x' | 'y' | 'z' = 'y'): Float32Array { + const { positions } = context; + const numVertices = positions.length / 3; + const uvs = new Float32Array(numVertices * 2); + const bbox = computeBoundingBox(positions); + + for (let i = 0; i < numVertices; i++) { + const x = positions[i * 3]; + const y = positions[i * 3 + 1]; + const z = positions[i * 3 + 2]; + + const cx = x - bbox.center[0]; + const cy = y - bbox.center[1]; + const cz = z - bbox.center[2]; + + let u = 0; + let v = 0; + + switch (axis) { + case 'x': + u = (Math.atan2(cy, cz) + Math.PI) / (2 * Math.PI); + v = normalizeCoordinate(x, bbox.min[0], bbox.size[0]); + break; + case 'z': + u = (Math.atan2(cy, cx) + Math.PI) / (2 * Math.PI); + v = normalizeCoordinate(z, bbox.min[2], bbox.size[2]); + break; + case 'y': + default: + u = (Math.atan2(cz, cx) + Math.PI) / (2 * Math.PI); + v = normalizeCoordinate(y, bbox.min[1], bbox.size[1]); + break; + } + + uvs[i * 2] = u; + uvs[i * 2 + 1] = v; + } + + return uvs; +} diff --git a/src/core/uv/planarProjection.ts b/src/core/uv/planarProjection.ts new file mode 100644 index 0000000..d7ae240 --- /dev/null +++ b/src/core/uv/planarProjection.ts @@ -0,0 +1,38 @@ +import { UVGenerationContext, computeBoundingBox, normalizeCoordinate } from './uvUtils'; + +export function generatePlanarUVs(context: UVGenerationContext, axis: 'x' | 'y' | 'z' = 'z'): Float32Array { + const { positions } = context; + const numVertices = positions.length / 3; + const uvs = new Float32Array(numVertices * 2); + const bbox = computeBoundingBox(positions); + + for (let i = 0; i < numVertices; i++) { + const x = positions[i * 3]; + const y = positions[i * 3 + 1]; + const z = positions[i * 3 + 2]; + + let u = 0; + let v = 0; + + switch (axis) { + case 'x': + u = normalizeCoordinate(z, bbox.min[2], bbox.size[2]); + v = normalizeCoordinate(y, bbox.min[1], bbox.size[1]); + break; + case 'y': + u = normalizeCoordinate(x, bbox.min[0], bbox.size[0]); + v = normalizeCoordinate(z, bbox.min[2], bbox.size[2]); + break; + case 'z': + default: + u = normalizeCoordinate(x, bbox.min[0], bbox.size[0]); + v = normalizeCoordinate(y, bbox.min[1], bbox.size[1]); + break; + } + + uvs[i * 2] = u; + uvs[i * 2 + 1] = v; + } + + return uvs; +} diff --git a/src/core/uv/uvUtils.ts b/src/core/uv/uvUtils.ts new file mode 100644 index 0000000..79959fa --- /dev/null +++ b/src/core/uv/uvUtils.ts @@ -0,0 +1,50 @@ +export interface UVGenerationContext { + positions: Float32Array; + normals?: Float32Array; + indices?: Uint32Array | Uint16Array; +} + +export interface BoundingBox { + min: [number, number, number]; + max: [number, number, number]; + size: [number, number, number]; + center: [number, number, number]; +} + +export function computeBoundingBox(positions: Float32Array): BoundingBox { + let minX = Infinity, minY = Infinity, minZ = Infinity; + let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + + for (let i = 0; i < positions.length; i += 3) { + const x = positions[i]; + const y = positions[i + 1]; + const z = positions[i + 2]; + + if (x < minX) minX = x; + if (y < minY) minY = y; + if (z < minZ) minZ = z; + + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + if (z > maxZ) maxZ = z; + } + + const size: [number, number, number] = [maxX - minX, maxY - minY, maxZ - minZ]; + const center: [number, number, number] = [ + minX + size[0] / 2, + minY + size[1] / 2, + minZ + size[2] / 2, + ]; + + return { + min: [minX, minY, minZ], + max: [maxX, maxY, maxZ], + size, + center, + }; +} + +export function normalizeCoordinate(value: number, min: number, size: number): number { + if (size === 0) return 0; + return (value - min) / size; +} diff --git a/src/hooks/useTexturePipeline.ts b/src/hooks/useTexturePipeline.ts new file mode 100644 index 0000000..52f7ad5 --- /dev/null +++ b/src/hooks/useTexturePipeline.ts @@ -0,0 +1,94 @@ +import { useEffect, useRef, useState } from 'react'; +import * as THREE from 'three'; +import { ProjectionType, UVWorkerMessage, UVWorkerResponse } from '../workers/uvWorker'; + +export function useTexturePipeline(scene: THREE.Object3D | null) { + const [isProcessing, setIsProcessing] = useState(false); + const workerRef = useRef(null); + + useEffect(() => { + workerRef.current = new Worker(new URL('../workers/uvWorker.ts', import.meta.url), { + type: 'module', + }); + return () => { + workerRef.current?.terminate(); + }; + }, []); + + useEffect(() => { + if (!scene || !workerRef.current) return; + + const processMesh = async (mesh: THREE.Mesh) => { + const geometry = mesh.geometry; + if (!geometry.attributes.position) return; + + const positions = geometry.attributes.position.array as Float32Array; + const normals = geometry.attributes.normal?.array as Float32Array | undefined; + + const generateUVs = (projection: ProjectionType, axis?: 'x'|'y'|'z'): Promise => { + return new Promise((resolve, reject) => { + if (!workerRef.current) return reject('No worker'); + const id = Math.random().toString(36).substring(7); + + const handleMessage = (e: MessageEvent) => { + if (e.data.id === id) { + workerRef.current?.removeEventListener('message', handleMessage); + if (e.data.error) reject(new Error(e.data.error)); + else resolve(e.data.uvs); + } + }; + + workerRef.current.addEventListener('message', handleMessage); + + workerRef.current.postMessage({ + id, + positions, + normals, + projection, + axis + } as UVWorkerMessage); + }); + }; + + try { + const numVertices = positions.length / 3; + // Add dummy attributes synchronously to prevent shader compile errors + if (!geometry.attributes.uvPlanar) geometry.setAttribute('uvPlanar', new THREE.BufferAttribute(new Float32Array(numVertices * 2), 2)); + if (!geometry.attributes.uvBox) geometry.setAttribute('uvBox', new THREE.BufferAttribute(new Float32Array(numVertices * 2), 2)); + if (!geometry.attributes.uvCylinder) geometry.setAttribute('uvCylinder', new THREE.BufferAttribute(new Float32Array(numVertices * 2), 2)); + + const uvPlanar = await generateUVs('planar', 'z'); + (geometry.attributes.uvPlanar as THREE.BufferAttribute).array.set(uvPlanar); + geometry.attributes.uvPlanar.needsUpdate = true; + + const uvBox = await generateUVs('box'); + (geometry.attributes.uvBox as THREE.BufferAttribute).array.set(uvBox); + geometry.attributes.uvBox.needsUpdate = true; + + const uvCylinder = await generateUVs('cylindrical', 'y'); + (geometry.attributes.uvCylinder as THREE.BufferAttribute).array.set(uvCylinder); + geometry.attributes.uvCylinder.needsUpdate = true; + } catch (err) { + console.error("UV Generation error", err); + } + }; + + const processAllMeshes = async () => { + setIsProcessing(true); + const meshes: THREE.Mesh[] = []; + scene.traverse((child) => { + if ((child as THREE.Mesh).isMesh) { + meshes.push(child as THREE.Mesh); + } + }); + + // Process in parallel + await Promise.all(meshes.map(processMesh)); + setIsProcessing(false); + }; + + processAllMeshes(); + }, [scene]); + + return { isProcessing }; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..3550ba5 --- /dev/null +++ b/src/index.css @@ -0,0 +1,113 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap'); +@import "tailwindcss"; + +@theme { + --font-sans: "Outfit", "Inter", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace; + + /* Theme Colors */ + --color-primary: #4DA3FF; + --color-accent: #FF2E88; + --color-glass-bg: rgba(246, 249, 255, 0.8); + --color-glass-card: rgba(255, 255, 255, 0.7); + + /* Shadows */ + --shadow-glass: 0 8px 32px 0 rgba(31, 38, 135, 0.15); + --shadow-emboss: inset 2px 2px 4px #dbe6ff, inset -2px -2px 4px #ffffff; +} + +@layer base { + body { + @apply bg-[#F6F9FF] text-gray-900 antialiased overflow-hidden transition-colors duration-500; + font-feature-settings: "ss01", "ss02", "cv01", "cv11"; + } + + body.dark { + @apply bg-[#0a0a0a] text-gray-100; + } +} + +@layer components { + .glass-panel { + @apply bg-white/70 backdrop-blur-xl border border-white/40 shadow-glass transition-all duration-300; + } + + .dark .glass-panel { + @apply bg-white/5 border-white/10 shadow-2xl; + } + + .glass-card { + @apply bg-white/50 backdrop-blur-md border border-white/40 hover:bg-white/80 transition-all duration-300 shadow-sm; + } + + .dark .glass-card { + @apply bg-white/5 border-white/10 hover:bg-white/10; + } + + .emboss-btn { + @apply shadow-glass border border-white/40 transition-all duration-200 active:shadow-emboss; + box-shadow: 4px 4px 8px rgba(0,0,0,0.05), -4px -4px 8px rgba(255,255,255,0.8); + } + + .dark .emboss-btn { + @apply border-white/10 shadow-none active:bg-white/10; + box-shadow: none; + } + + .sidebar-item { + @apply flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 cursor-pointer; + } + + .sidebar-item-active { + @apply bg-primary/20 text-primary border border-primary/30; + } + + .sidebar-item:hover:not(.sidebar-item-active) { + @apply bg-white/20; + } + + .dark .sidebar-item:hover:not(.sidebar-item-active) { + @apply bg-white/5; + } + + .neon-glow:hover { + box-shadow: 0 0 15px rgba(255, 46, 136, 0.4); + } + + .custom-scrollbar::-webkit-scrollbar { + width: 4px; + } + + .custom-scrollbar::-webkit-scrollbar-track { + @apply bg-transparent; + } + + .custom-scrollbar::-webkit-scrollbar-thumb { + @apply bg-black/10 rounded-full hover:bg-black/20; + } + + .dark .custom-scrollbar::-webkit-scrollbar-thumb { + @apply bg-white/10 hover:bg-white/20; + } +} + +/* Animation for the background glow */ +@keyframes float { + 0% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(2%, 2%) scale(1.1); } + 66% { transform: translate(-1%, 3%) scale(0.9); } + 100% { transform: translate(0, 0) scale(1); } +} + +.animate-float { + animation: float 20s ease-in-out infinite; +} + +.animate-float-delayed { + animation: float 25s ease-in-out infinite reverse; +} + +/* Hide TransformControls UI when rendering */ +.rendering-hide { + display: none !important; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..080dac3 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import App from './App.tsx'; +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/materials/MaterialFactory.ts b/src/materials/MaterialFactory.ts new file mode 100644 index 0000000..685d218 --- /dev/null +++ b/src/materials/MaterialFactory.ts @@ -0,0 +1,52 @@ +import * as THREE from 'three'; +import { StandardUVMaterial } from './StandardUVMaterial'; +import { TriplanarMaterial } from './TriplanarMaterial'; + +type MaterialCacheKey = string; + +export class MaterialFactory { + private static instance: MaterialFactory; + private cache: Map = new Map(); + + private constructor() {} + + public static getInstance(): MaterialFactory { + if (!MaterialFactory.instance) { + MaterialFactory.instance = new MaterialFactory(); + } + return MaterialFactory.instance; + } + + public getStandardUVMaterial( + parameters: THREE.MeshPhysicalMaterialParameters, + key: string + ): StandardUVMaterial { + const cacheKey = `standard_uv_${key}`; + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey) as StandardUVMaterial; + } + + const material = new StandardUVMaterial(parameters); + this.cache.set(cacheKey, material); + return material; + } + + public getTriplanarMaterial( + parameters: THREE.MeshPhysicalMaterialParameters, + key: string + ): TriplanarMaterial { + const cacheKey = `triplanar_${key}`; + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey) as TriplanarMaterial; + } + + const material = new TriplanarMaterial(parameters); + this.cache.set(cacheKey, material); + return material; + } + + public clearCache() { + this.cache.forEach((material) => material.dispose()); + this.cache.clear(); + } +} diff --git a/src/materials/StandardUVMaterial.ts b/src/materials/StandardUVMaterial.ts new file mode 100644 index 0000000..81ccd98 --- /dev/null +++ b/src/materials/StandardUVMaterial.ts @@ -0,0 +1,95 @@ +import * as THREE from 'three'; + +export class StandardUVMaterial extends THREE.MeshPhysicalMaterial { + public uvUniforms: { + uMappingMode: { value: number }; // 0: imported, 1: planar, 2: box, 3: cylindrical + uUVTransform: { value: THREE.Matrix3 }; + }; + + constructor(parameters?: THREE.MeshPhysicalMaterialParameters) { + super(parameters); + + this.uvUniforms = { + uMappingMode: { value: 0 }, + uUVTransform: { value: new THREE.Matrix3() }, + }; + + this.customProgramCacheKey = () => `standard_uv_v2`; + + this.onBeforeCompile = (shader) => { + shader.uniforms.uMappingMode = this.uvUniforms.uMappingMode; + shader.uniforms.uUVTransform = this.uvUniforms.uUVTransform; + + shader.vertexShader = ` + attribute vec2 uvPlanar; + attribute vec2 uvBox; + attribute vec2 uvCylinder; + + uniform int uMappingMode; + uniform mat3 uUVTransform; + + varying vec2 vCustomUV; + ` + shader.vertexShader; + + shader.vertexShader = shader.vertexShader.replace( + `#include `, + ` + #include + + vec2 selectedUV = vec2(0.0); + #ifdef USE_UV + selectedUV = uv; + #endif + + if (uMappingMode == 1) { + selectedUV = uvPlanar; + } else if (uMappingMode == 2) { + selectedUV = uvBox; + } else if (uMappingMode == 3) { + selectedUV = uvCylinder; + } + + vCustomUV = (uUVTransform * vec3(selectedUV, 1.0)).xy; + ` + ); + + shader.fragmentShader = ` + varying vec2 vCustomUV; + ` + shader.fragmentShader; + + const mainStartIndex = shader.fragmentShader.indexOf('void main() {'); + if (mainStartIndex !== -1) { + const prefix = shader.fragmentShader.substring(0, mainStartIndex); + let mainBody = shader.fragmentShader.substring(mainStartIndex); + + mainBody = mainBody.replace(/\bvUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvNormalMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvRoughnessMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvMetalnessMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvAlphaMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvEmissiveMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvAoMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvClearcoatMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvClearcoatNormalMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvClearcoatRoughnessMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvTransmissionMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvThicknessMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvSpecularMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvSpecularIntensityMapUv\b/g, 'vCustomUV'); + mainBody = mainBody.replace(/\bvSpecularColorMapUv\b/g, 'vCustomUV'); + + shader.fragmentShader = prefix + mainBody; + } + }; + } + + public updateUVTransform(offset: [number, number], repeat: [number, number], rotation: number) { + this.uvUniforms.uUVTransform.value.setUvTransform( + offset[0], offset[1], + repeat[0], repeat[1], + rotation, + 0.5, 0.5 + ); + } +} diff --git a/src/materials/TriplanarMaterial.ts b/src/materials/TriplanarMaterial.ts new file mode 100644 index 0000000..bac87dc --- /dev/null +++ b/src/materials/TriplanarMaterial.ts @@ -0,0 +1,134 @@ +import * as THREE from 'three'; + +export class TriplanarMaterial extends THREE.MeshPhysicalMaterial { + public triplanarUniforms: { + tAlbedo: { value: THREE.Texture | null }; + tNormal: { value: THREE.Texture | null }; + tRoughness: { value: THREE.Texture | null }; + tAo: { value: THREE.Texture | null }; + uTriplanarScale: { value: number }; + uBlendSharpness: { value: number }; + }; + + constructor(parameters?: THREE.MeshPhysicalMaterialParameters) { + super(parameters); + + this.triplanarUniforms = { + tAlbedo: { value: null }, + tNormal: { value: null }, + tRoughness: { value: null }, + tAo: { value: null }, + uTriplanarScale: { value: 1.0 }, + uBlendSharpness: { value: 5.0 }, + }; + + this.customProgramCacheKey = () => `triplanar_mat_v1`; + + this.onBeforeCompile = (shader) => { + // Inject Uniforms + shader.uniforms.tAlbedo = this.triplanarUniforms.tAlbedo; + shader.uniforms.tNormal = this.triplanarUniforms.tNormal; + shader.uniforms.tRoughness = this.triplanarUniforms.tRoughness; + shader.uniforms.tAo = this.triplanarUniforms.tAo; + shader.uniforms.uTriplanarScale = this.triplanarUniforms.uTriplanarScale; + shader.uniforms.uBlendSharpness = this.triplanarUniforms.uBlendSharpness; + + // Vertex Shader Injections + shader.vertexShader = ` + varying vec3 vTriplanarPosition; + varying vec3 vTriplanarNormal; + ` + shader.vertexShader; + + shader.vertexShader = shader.vertexShader.replace( + `#include `, + ` + #include + vTriplanarPosition = position; // Object space + vTriplanarNormal = normal; // Object space normal + ` + ); + + // Fragment Shader Injections + shader.fragmentShader = ` + uniform sampler2D tAlbedo; + uniform sampler2D tNormal; + uniform sampler2D tRoughness; + uniform sampler2D tAo; + uniform float uTriplanarScale; + uniform float uBlendSharpness; + + varying vec3 vTriplanarPosition; + varying vec3 vTriplanarNormal; + + // Triplanar blending logic + vec4 getTriplanarBlend(sampler2D tex, vec3 pos, vec3 norm) { + vec3 blendWeights = abs(norm); + blendWeights = pow(blendWeights, vec3(uBlendSharpness)); + blendWeights /= dot(blendWeights, vec3(1.0)); // Normalize + + vec2 uvX = pos.yz * uTriplanarScale; + vec2 uvY = pos.zx * uTriplanarScale; + vec2 uvZ = pos.xy * uTriplanarScale; + + vec4 colX = texture2D(tex, uvX); + vec4 colY = texture2D(tex, uvY); + vec4 colZ = texture2D(tex, uvZ); + + return colX * blendWeights.x + colY * blendWeights.y + colZ * blendWeights.z; + } + ` + shader.fragmentShader; + + // Replace map chunks + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` + #ifdef USE_MAP + vec4 texelColor = getTriplanarBlend(tAlbedo, vTriplanarPosition, vTriplanarNormal); + texelColor = mapTexelToLinear(texelColor); + diffuseColor *= texelColor; + #endif + ` + ); + + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` + float roughnessFactor = roughness; + #ifdef USE_ROUGHNESSMAP + vec4 texelRoughness = getTriplanarBlend(tRoughness, vTriplanarPosition, vTriplanarNormal); + roughnessFactor *= texelRoughness.g; + #endif + ` + ); + + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` + #ifdef USE_NORMALMAP + vec4 texelNormal = getTriplanarBlend(tNormal, vTriplanarPosition, vTriplanarNormal); + // Simplified triplanar normal calculation + vec3 n = texelNormal.rgb * 2.0 - 1.0; + normal = normalize(normalMatrix * n); + #elif defined( USE_BUMPMAP ) + // Bump map omitted for brevity + #endif + ` + ); + + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` + #ifdef USE_AOMAP + vec4 texelAo = getTriplanarBlend(tAo, vTriplanarPosition, vTriplanarNormal); + float ambientOcclusion = ( texelAo.r - 1.0 ) * aoMapIntensity + 1.0; + reflectedLight.indirectDiffuse *= ambientOcclusion; + #if defined( USE_ENVMAP ) && defined( STANDARD ) + float dotNV = saturate( dot( geometry.normal, geometry.viewDir ) ); + reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness ); + #endif + #endif + ` + ); + }; + } +} diff --git a/src/store/configuratorStore.ts b/src/store/configuratorStore.ts new file mode 100644 index 0000000..5d28fca --- /dev/null +++ b/src/store/configuratorStore.ts @@ -0,0 +1,54 @@ +import { create } from 'zustand'; + +export type MappingMode = 'imported' | 'planar' | 'box' | 'cylindrical' | 'triplanar'; + +export interface UVTransforms { + scale: [number, number]; + repeat: [number, number]; + offset: [number, number]; + rotation: number; +} + +export interface TriplanarTransforms { + scale: number; + blendSharpness: number; + rotation: [number, number, number]; + worldSpace: boolean; +} + +interface ConfiguratorState { + mappingMode: MappingMode; + uvTransforms: UVTransforms; + triplanarTransforms: TriplanarTransforms; + projectionAxis: 'x' | 'y' | 'z'; + + // Actions + setMappingMode: (mode: MappingMode) => void; + setUVTransforms: (transforms: Partial) => void; + setTriplanarTransforms: (transforms: Partial) => void; + setProjectionAxis: (axis: 'x' | 'y' | 'z') => void; +} + +export const useConfiguratorStore = create((set) => ({ + mappingMode: 'imported', + projectionAxis: 'z', + uvTransforms: { + scale: [1, 1], + repeat: [1, 1], + offset: [0, 0], + rotation: 0, + }, + triplanarTransforms: { + scale: 1, + blendSharpness: 5.0, + rotation: [0, 0, 0], + worldSpace: false, + }, + + setMappingMode: (mode) => set({ mappingMode: mode }), + setUVTransforms: (transforms) => + set((state) => ({ uvTransforms: { ...state.uvTransforms, ...transforms } })), + setTriplanarTransforms: (transforms) => + set((state) => ({ triplanarTransforms: { ...state.triplanarTransforms, ...transforms } })), + setProjectionAxis: (axis) => set({ projectionAxis: axis }), +})); diff --git a/src/utils/uvQuality.ts b/src/utils/uvQuality.ts new file mode 100644 index 0000000..696dabe --- /dev/null +++ b/src/utils/uvQuality.ts @@ -0,0 +1,71 @@ +export interface UVQualityReport { + missingUVs: boolean; + extremeStretching: boolean; + overlappingUVs: boolean; + highComplexity: boolean; + recommendTriplanar: boolean; + details: { + vertexCount: number; + uvRange: [number, number]; + }; +} + +export function analyzeUVQuality( + positions: Float32Array, + uvs?: Float32Array, + indices?: Uint32Array | Uint16Array +): UVQualityReport { + const vertexCount = positions.length / 3; + const highComplexity = vertexCount > 50000; // Arbitrary threshold for complex geometry + + if (!uvs || uvs.length === 0) { + return { + missingUVs: true, + extremeStretching: false, + overlappingUVs: false, + highComplexity, + recommendTriplanar: true, + details: { + vertexCount, + uvRange: [0, 0] + } + }; + } + + let minU = Infinity, maxU = -Infinity; + let minV = Infinity, maxV = -Infinity; + + for (let i = 0; i < uvs.length; i += 2) { + const u = uvs[i]; + const v = uvs[i + 1]; + + if (u < minU) minU = u; + if (u > maxU) maxU = u; + if (v < minV) minV = v; + if (v > maxV) maxV = v; + } + + const uRange = maxU - minU; + const vRange = maxV - minV; + + // Basic heuristic: if UVs are heavily clamped or barely cover any area + const extremeStretching = (uRange < 0.01 && uRange > 0) || (vRange < 0.01 && vRange > 0); + + // Overlapping detection is computationally expensive, skipping deep check for now + // A robust check would require comparing triangle areas in UV space vs World space + const overlappingUVs = false; + + const recommendTriplanar = extremeStretching || highComplexity; + + return { + missingUVs: false, + extremeStretching, + overlappingUVs, + highComplexity, + recommendTriplanar, + details: { + vertexCount, + uvRange: [uRange, vRange] + } + }; +} diff --git a/src/workers/uvWorker.ts b/src/workers/uvWorker.ts new file mode 100644 index 0000000..6dfaff2 --- /dev/null +++ b/src/workers/uvWorker.ts @@ -0,0 +1,49 @@ +import { generatePlanarUVs } from '../core/uv/planarProjection'; +import { generateBoxUVs } from '../core/uv/boxProjection'; +import { generateCylindricalUVs } from '../core/uv/cylindricalProjection'; + +export type ProjectionType = 'planar' | 'box' | 'cylindrical'; + +export interface UVWorkerMessage { + id: string; + positions: Float32Array; + normals?: Float32Array; + projection: ProjectionType; + axis?: 'x' | 'y' | 'z'; +} + +export interface UVWorkerResponse { + id: string; + uvs: Float32Array; + error?: string; +} + +self.onmessage = (event: MessageEvent) => { + const { id, positions, normals, projection, axis } = event.data; + + try { + const context = { positions, normals }; + let uvs: Float32Array; + + switch (projection) { + case 'planar': + uvs = generatePlanarUVs(context, axis || 'z'); + break; + case 'box': + uvs = generateBoxUVs(context); + break; + case 'cylindrical': + uvs = generateCylindricalUVs(context, axis || 'y'); + break; + default: + throw new Error(`Unsupported projection: ${projection}`); + } + + // Transfer the ArrayBuffer back to main thread for performance + self.postMessage({ id, uvs } as UVWorkerResponse, { + transfer: [uvs.buffer], + }); + } catch (error) { + self.postMessage({ id, error: String(error) } as UVWorkerResponse); + } +}; diff --git a/test.cjs b/test.cjs new file mode 100644 index 0000000..e11d129 --- /dev/null +++ b/test.cjs @@ -0,0 +1,34 @@ +const puppeteer = require('puppeteer'); +const path = require('path'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + page.on('console', msg => { + if (msg.type() === 'error') { + console.log('PAGE ERROR:', msg.text()); + } else { + console.log('PAGE LOG:', msg.text()); + } + }); + + page.on('pageerror', error => { + console.log('PAGE EXCEPTION:', error.message); + console.log('STACK:', error.stack); + }); + + await page.goto('http://localhost:3000', { waitUntil: 'networkidle2' }); + + // Find the file input for import (usually has an accept=".glb,.gltf...") + const fileInput = await page.$('input[type="file"]'); + if (fileInput) { + console.log('Found file input, uploading...'); + await fileInput.uploadFile(path.resolve('C:/Users/900250.HCUDT299/Downloads/0.glb')); + await new Promise(r => setTimeout(r, 5000)); + } else { + console.log('File input not found'); + } + + await browser.close(); +})(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d88f175 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..f5293da --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,30 @@ +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import {defineConfig, loadEnv} from 'vite'; + +export default defineConfig(({mode}) => { + const env = loadEnv(mode, '.', ''); + return { + plugins: [react(), tailwindcss()], + define: { + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + 'react': path.resolve(__dirname, 'node_modules/react'), + 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), + }, + dedupe: ['react', 'react-dom'], + }, + optimizeDeps: { + include: ['react', 'react-dom', 'three', '@react-three/fiber', '@react-three/drei'], + }, + server: { + // HMR is disabled in AI Studio via DISABLE_HMR env var. + // Do not modify—file watching is disabled to prevent flickering during agent edits. + hmr: process.env.DISABLE_HMR !== 'true', + }, + }; +});