diff --git a/package-lock.json b/package-lock.json index 2ad154f..bfd967a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,8 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.6", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@reactflow/background": "^11.3.14", "@reactflow/controls": "^11.2.14", "@reactflow/minimap": "^11.7.14", @@ -36,6 +38,7 @@ "@sentry/browser": "^10.34.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "fengari-web": "^0.1.4", "framer-motion": "^11.15.0", "immer": "^11.1.3", "lucide-react": "^0.462.0", @@ -52,6 +55,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", + "three": "^0.182.0", "zustand": "^5.0.10" }, "devDependencies": { @@ -61,6 +65,7 @@ "@types/node": "22.19.7", "@types/react": "18.3.27", "@types/react-dom": "^18", + "@types/three": "^0.182.0", "@vitejs/plugin-react": "^5.1.2", "autoprefixer": "^10.4.23", "eslint": "^8", @@ -84,6 +89,7 @@ }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -413,7 +419,6 @@ }, "node_modules/@babel/runtime": { "version": "7.28.6", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -600,6 +605,12 @@ "node": ">=18" } }, + "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/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -1248,6 +1259,7 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1265,6 +1277,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1272,16 +1285,24 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", + "dev": true, "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/@monaco-editor/loader": { "version": "1.7.0", "license": "MIT", @@ -1301,6 +1322,18 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1474,6 +1507,7 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1485,6 +1519,7 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1492,6 +1527,7 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3082,6 +3118,100 @@ "version": "1.1.1", "license": "MIT" }, + "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/fiber/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/@reactflow/background": { "version": "11.3.14", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", @@ -3803,36 +3933,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "dev": true, @@ -3906,6 +4006,12 @@ "@testing-library/dom": ">=7.21.4" } }, + "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/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3917,12 +4023,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -4283,6 +4383,12 @@ "dev": true, "license": "MIT" }, + "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", "dev": true, @@ -4306,14 +4412,20 @@ "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/prop-types": { "version": "15.7.15", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.27", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4322,17 +4434,59 @@ }, "node_modules/@types/react-dom": { "version": "18.3.7", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" } }, + "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/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.182.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz", + "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==", + "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": "~0.22.0" + } + }, + "node_modules/@types/three/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/@types/trusted-types": { "version": "2.0.7", "license": "MIT", "optional": true }, + "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/@typescript-eslint/parser": { "version": "8.53.0", "dev": true, @@ -4822,6 +4976,24 @@ "win32" ] }, + "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.1.2", "dev": true, @@ -4938,6 +5110,12 @@ "url": "https://opencollective.com/vitest" } }, + "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/acorn": { "version": "8.15.0", "dev": true, @@ -5004,10 +5182,12 @@ }, "node_modules/any-promise": { "version": "1.3.0", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -5019,6 +5199,7 @@ }, "node_modules/arg": { "version": "5.0.2", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -5754,6 +5935,26 @@ "dev": true, "license": "MIT" }, + "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.9.15", "dev": true, @@ -5764,7 +5965,6 @@ }, "node_modules/bidi-js": { "version": "1.0.3", - "dev": true, "license": "MIT", "dependencies": { "require-from-string": "^2.0.2" @@ -5772,6 +5972,7 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5791,6 +5992,7 @@ }, "node_modules/braces": { "version": "3.0.3", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -5831,6 +6033,30 @@ "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/busboy": { "version": "1.6.0", "dependencies": { @@ -5894,11 +6120,25 @@ }, "node_modules/camelcase-css": { "version": "2.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" } }, + "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.30001764", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", @@ -5944,6 +6184,7 @@ }, "node_modules/chokidar": { "version": "3.6.0", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -5966,6 +6207,7 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -6019,6 +6261,7 @@ }, "node_modules/commander": { "version": "4.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6045,6 +6288,24 @@ "url": "https://opencollective.com/core-js" } }, + "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", "license": "MIT", @@ -6064,6 +6325,7 @@ }, "node_modules/cssesc": { "version": "3.0.0", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -6110,7 +6372,7 @@ }, "node_modules/csstype": { "version": "3.2.3", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/d3-color": { @@ -6340,13 +6602,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dequal": { - "version": "2.0.3", - "dev": true, + "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", - "peer": true, - "engines": { - "node": ">=6" + "dependencies": { + "webgl-constants": "^1.1.1" } }, "node_modules/detect-node-es": { @@ -6355,10 +6617,12 @@ }, "node_modules/didyoumean": { "version": "1.2.2", + "dev": true, "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", + "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -6372,12 +6636,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/dompurify": { "version": "3.3.1", "license": "(MPL-2.0 OR Apache-2.0)", @@ -6385,6 +6643,12 @@ "@types/trusted-types": "^2.0.7" } }, + "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", "dev": true, @@ -7148,6 +7412,7 @@ }, "node_modules/fast-glob": { "version": "3.3.3", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7162,6 +7427,7 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7182,11 +7448,42 @@ }, "node_modules/fastq": { "version": "1.20.1", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, + "node_modules/fengari": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.5.tgz", + "integrity": "sha512-0DS4Nn4rV8qyFlQCpKK8brT61EUtswynrpfFTcgLErcilBIBskSMQ86fO2WVuybr14ywyKdRjv91FiRZwnEuvQ==", + "license": "MIT", + "dependencies": { + "readline-sync": "^1.4.10", + "sprintf-js": "^1.1.3", + "tmp": "^0.2.5" + } + }, + "node_modules/fengari-interop": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fengari-interop/-/fengari-interop-0.1.4.tgz", + "integrity": "sha512-4/CW/3PJUo3ebD4ACgE1g/3NGEYSq7OQAyETyypsAl/WeySDBbxExikkayNkZzbpgyC9GyJp8v1DU2VOXxNq7Q==", + "license": "MIT", + "peerDependencies": { + "fengari": "^0.1.0" + } + }, + "node_modules/fengari-web": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fengari-web/-/fengari-web-0.1.4.tgz", + "integrity": "sha512-f+W/Csx9VNyKttxYjZnk6290+Pcs7w7noDVhkuPEt0e51GWoD32vSNHFXhZYzTe8Ni/bhbk5VocNV1RBIgO5iA==", + "license": "MIT", + "dependencies": { + "fengari": "^0.1.4", + "fengari-interop": "^0.1" + } + }, "node_modules/fflate": { "version": "0.4.8", "license": "MIT" @@ -7204,6 +7501,7 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -7320,6 +7618,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7332,6 +7631,7 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7472,6 +7772,7 @@ }, "node_modules/glob-parent": { "version": "6.0.2", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7531,6 +7832,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/gopd": { "version": "1.2.0", "dev": true, @@ -7622,6 +7929,7 @@ }, "node_modules/hasown": { "version": "2.0.2", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7630,6 +7938,12 @@ "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/html-encoding-sniffer": { "version": "6.0.0", "dev": true, @@ -7665,6 +7979,26 @@ "node": ">= 14" } }, + "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/ignore": { "version": "5.3.2", "dev": true, @@ -7673,6 +8007,12 @@ "node": ">= 4" } }, + "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/immer": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", @@ -7791,6 +8131,7 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -7846,6 +8187,7 @@ }, "node_modules/is-core-module": { "version": "2.16.1", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -7890,6 +8232,7 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7937,6 +8280,7 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7969,6 +8313,7 @@ }, "node_modules/is-number": { "version": "7.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -8002,6 +8347,12 @@ "dev": true, "license": "MIT" }, + "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/is-regex": { "version": "1.2.1", "dev": true, @@ -8154,6 +8505,18 @@ "node": ">= 0.4" } }, + "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/jackspeak": { "version": "2.3.6", "dev": true, @@ -8173,6 +8536,7 @@ }, "node_modules/jiti": { "version": "1.21.7", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -8320,8 +8684,18 @@ "node": ">= 0.8.0" } }, + "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/lilconfig": { "version": "3.1.3", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -8332,6 +8706,7 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -8384,13 +8759,14 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "dev": true, + "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", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" } }, "node_modules/magic-string": { @@ -8418,13 +8794,30 @@ }, "node_modules/merge2": { "version": "1.4.1", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, + "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": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -8492,6 +8885,7 @@ }, "node_modules/mz": { "version": "2.7.0", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -8625,6 +9019,7 @@ }, "node_modules/normalize-path": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8632,6 +9027,7 @@ }, "node_modules/object-assign": { "version": "4.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8639,6 +9035,7 @@ }, "node_modules/object-hash": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9005,6 +9402,7 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -9038,6 +9436,7 @@ }, "node_modules/picomatch": { "version": "2.3.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9048,6 +9447,7 @@ }, "node_modules/pify": { "version": "2.3.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9055,6 +9455,7 @@ }, "node_modules/pirates": { "version": "4.0.7", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -9070,6 +9471,7 @@ }, "node_modules/postcss": { "version": "8.5.6", + "dev": true, "funding": [ { "type": "opencollective", @@ -9096,6 +9498,7 @@ }, "node_modules/postcss-import": { "version": "15.1.0", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -9111,6 +9514,7 @@ }, "node_modules/postcss-js": { "version": "4.1.0", + "dev": true, "funding": [ { "type": "opencollective", @@ -9134,6 +9538,7 @@ }, "node_modules/postcss-load-config": { "version": "6.0.1", + "dev": true, "funding": [ { "type": "opencollective", @@ -9174,6 +9579,7 @@ }, "node_modules/postcss-nested": { "version": "6.2.0", + "dev": true, "funding": [ { "type": "opencollective", @@ -9197,6 +9603,7 @@ }, "node_modules/postcss-selector-parser": { "version": "6.1.2", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -9208,6 +9615,7 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", + "dev": true, "license": "MIT" }, "node_modules/posthog-js": { @@ -9231,6 +9639,12 @@ "web-vitals": "^4.2.4" } }, + "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/preact": { "version": "10.28.2", "license": "MIT", @@ -9247,30 +9661,14 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "dev": true, - "license": "MIT", - "peer": true, + "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": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "is-promise": "^2.1.0", + "lie": "^3.0.2" } }, "node_modules/prop-types": { @@ -9324,6 +9722,7 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "dev": true, "funding": [ { "type": "github", @@ -9368,12 +9767,6 @@ "react": "^18.0.0 || ^19.0.0" } }, - "node_modules/react-is": { - "version": "17.0.2", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/react-refresh": { "version": "0.18.0", "dev": true, @@ -9453,6 +9846,21 @@ } } }, + "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/reactflow": { "version": "11.11.4", "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", @@ -9473,6 +9881,7 @@ }, "node_modules/read-cache": { "version": "1.0.0", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -9480,6 +9889,7 @@ }, "node_modules/readdirp": { "version": "3.6.0", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -9488,6 +9898,15 @@ "node": ">=8.10.0" } }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/redent": { "version": "3.0.0", "dev": true, @@ -9611,7 +10030,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9619,6 +10037,7 @@ }, "node_modules/resolve": { "version": "1.22.11", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -9653,6 +10072,7 @@ }, "node_modules/reusify": { "version": "1.1.0", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -9737,6 +10157,7 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "dev": true, "funding": [ { "type": "github", @@ -10014,6 +10435,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "dev": true, @@ -10028,6 +10455,32 @@ "version": "1.0.7", "license": "MIT" }, + "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/std-env": { "version": "3.10.0", "dev": true, @@ -10564,6 +11017,7 @@ }, "node_modules/sucrase": { "version": "3.35.1", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -10595,6 +11049,7 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10603,6 +11058,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/symbol-tree": { "version": "3.2.4", "dev": true, @@ -10620,6 +11084,7 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -10667,6 +11132,7 @@ }, "node_modules/thenify": { "version": "3.3.1", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -10674,6 +11140,7 @@ }, "node_modules/thenify-all": { "version": "1.6.0", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -10682,6 +11149,44 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.182.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz", + "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==", + "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/tinybench": { "version": "2.9.0", "dev": true, @@ -10697,6 +11202,7 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -10711,6 +11217,7 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -10726,6 +11233,7 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10758,8 +11266,18 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -10790,6 +11308,36 @@ "node": ">=20" } }, + "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/ts-api-utils": { "version": "2.4.0", "dev": true, @@ -10803,6 +11351,7 @@ }, "node_modules/ts-interface-checker": { "version": "0.1.13", + "dev": true, "license": "Apache-2.0" }, "node_modules/tsconfig-paths": { @@ -10831,6 +11380,43 @@ "version": "2.8.1", "license": "0BSD" }, + "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-check": { "version": "0.4.0", "dev": true, @@ -11076,8 +11662,18 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", + "dev": true, "license": "MIT" }, + "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/vite": { "version": "7.3.1", "dev": true, @@ -11280,6 +11876,17 @@ "version": "4.2.4", "license": "Apache-2.0" }, + "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/webidl-conversions": { "version": "8.0.1", "dev": true, diff --git a/package.json b/package.json index 757d82b..7a0b21e 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.6", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@reactflow/background": "^11.3.14", "@reactflow/controls": "^11.2.14", "@reactflow/minimap": "^11.7.14", @@ -41,6 +43,7 @@ "@sentry/browser": "^10.34.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "fengari-web": "^0.1.4", "framer-motion": "^11.15.0", "immer": "^11.1.3", "lucide-react": "^0.462.0", @@ -57,6 +60,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", + "three": "^0.182.0", "zustand": "^5.0.10" }, "devDependencies": { @@ -66,6 +70,7 @@ "@types/node": "22.19.7", "@types/react": "18.3.27", "@types/react-dom": "^18", + "@types/three": "^0.182.0", "@vitejs/plugin-react": "^5.1.2", "autoprefixer": "^10.4.23", "eslint": "^8", diff --git a/src/App.tsx b/src/App.tsx index 61f8e06..9de25db 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ const TranslationPanel = lazy(() => import('./components/TranslationPanel').then const AvatarToolkit = lazy(() => import('./components/AvatarToolkit')); const VisualScriptingCanvas = lazy(() => import('./components/visual-scripting/VisualScriptingCanvas')); const AssetLibrary = lazy(() => import('./components/assets/AssetLibrary')); +const LivePreview = lazy(() => import('./components/preview/LivePreview')); function App() { const [currentCode, setCurrentCode] = useState(''); @@ -48,6 +49,7 @@ function App() { const [showAvatarToolkit, setShowAvatarToolkit] = useState(false); const [showVisualScripting, setShowVisualScripting] = useState(false); const [showAssetLibrary, setShowAssetLibrary] = useState(false); + const [showLivePreview, setShowLivePreview] = useState(false); const [code, setCode] = useState(''); const [currentPlatform, setCurrentPlatform] = useState('roblox'); const isMobile = useIsMobile(); @@ -486,6 +488,7 @@ end)`, onAvatarToolkitClick={() => setShowAvatarToolkit(true)} onVisualScriptingClick={() => setShowVisualScripting(true)} onAssetLibraryClick={() => setShowAssetLibrary(true)} + onLivePreviewClick={() => setShowLivePreview(true)} /> @@ -639,6 +642,18 @@ end)`, /> )} + }> + {showLivePreview && ( + + + setShowLivePreview(false)} + /> + + + )} + diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx index 7631ecc..7f64de1 100644 --- a/src/components/FileTree.tsx +++ b/src/components/FileTree.tsx @@ -58,38 +58,35 @@ export function FileTree({ if (next.has(id)) { next.delete(id); } else { - return ( - -
- {files.map((node) => ( - - ))} -
-
- ); + next.add(id); + } + return next; + }); + }, []); + + const startRename = useCallback((node: FileNode) => { + setEditingId(node.id); + setEditingName(node.name); + }, []); + + const finishRename = useCallback((id: string) => { + if (editingName.trim()) { + onFileRename(id, editingName.trim()); + } + setEditingId(null); + setEditingName(''); + }, [editingName, onFileRename]); + + const handleDelete = useCallback((node: FileNode) => { + if (confirm(`Are you sure you want to delete "${node.name}"?`)) { + onFileDelete(node.id); + } + }, [onFileDelete]); + + const handleDragStart = useCallback((e: React.DragEvent, node: FileNode) => { + e.stopPropagation(); + setDraggedId(node.id); + e.dataTransfer.effectAllowed = 'move'; }, []); const handleDragOver = useCallback((e: React.DragEvent, node: FileNode) => { diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index dad36e8..d16c8a7 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -7,7 +7,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { Copy, FileCode, Download, Info, Play, FolderPlus, User, SignOut, List, ArrowsLeftRight, UserCircle, GitBranch, Package } from '@phosphor-icons/react'; +import { Copy, FileCode, Download, Info, Play, FolderPlus, User, SignOut, List, ArrowsLeftRight, UserCircle, GitBranch, Package, Cube } from '@phosphor-icons/react'; import { toast } from 'sonner'; import { useState, useEffect, useCallback, memo } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'; @@ -27,9 +27,10 @@ interface ToolbarProps { onAvatarToolkitClick?: () => void; onVisualScriptingClick?: () => void; onAssetLibraryClick?: () => void; + onLivePreviewClick?: () => void; } -export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectClick, currentPlatform, onPlatformChange, onTranslateClick, onAvatarToolkitClick, onVisualScriptingClick, onAssetLibraryClick }: ToolbarProps) { +export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectClick, currentPlatform, onPlatformChange, onTranslateClick, onAvatarToolkitClick, onVisualScriptingClick, onAssetLibraryClick, onLivePreviewClick }: ToolbarProps) { const [showInfo, setShowInfo] = useState(false); const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(null); @@ -158,6 +159,25 @@ export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectCl )} + {/* Live Preview Button */} + {onLivePreviewClick && ( + + + + + Live 3D Preview with Lua Execution + + )} +
@@ -290,6 +310,12 @@ export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectCl Asset Library )} + {onLivePreviewClick && ( + + + 3D Preview + + )} Copy Code diff --git a/src/components/preview/LivePreview.tsx b/src/components/preview/LivePreview.tsx new file mode 100644 index 0000000..f841276 --- /dev/null +++ b/src/components/preview/LivePreview.tsx @@ -0,0 +1,363 @@ +'use client'; + +import { useState, lazy, Suspense } from 'react'; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from '@/components/ui/resizable'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Play, + Square, + Pause, + RotateCcw, + Settings, + Maximize2, + Minimize2, + Eye, + EyeOff, + Grid3X3, + Box, + Sun, + Terminal, + Loader2, +} from 'lucide-react'; + +import { usePreviewStore } from '@/stores/preview-store'; +import PreviewConsole from './PreviewConsole'; + +// Lazy load the 3D viewport +const PreviewViewport = lazy(() => import('./PreviewViewport')); + +interface LivePreviewProps { + code: string; + onClose?: () => void; +} + +export default function LivePreview({ code, onClose }: LivePreviewProps) { + const { + isRunning, + isPaused, + settings, + updateSettings, + runScript, + stopScript, + pauseScript, + resumeScript, + resetScene, + scene, + } = usePreviewStore(); + + const [isFullscreen, setIsFullscreen] = useState(false); + const [showConsole, setShowConsole] = useState(true); + + const handleRun = async () => { + await runScript(code); + }; + + const handleStop = () => { + stopScript(); + }; + + const handleReset = () => { + resetScene(); + }; + + return ( +
+ {/* Toolbar */} +
+
+ Live Preview + + Beta + +
+ +
+ {/* Run controls */} + {!isRunning ? ( + + ) : ( + <> + + + + )} + + + + + + {/* View toggles */} + + + {/* Settings popover */} + + + + + +
+

Preview Settings

+ +
+
+ + + updateSettings({ showGrid: checked }) + } + /> +
+ +
+ + + updateSettings({ showAxes: checked }) + } + /> +
+ +
+ + + updateSettings({ shadowsEnabled: checked }) + } + /> +
+ +
+ + + updateSettings({ showWireframe: checked }) + } + /> +
+ +
+ + + updateSettings({ showStats: checked }) + } + /> +
+ +
+ + + updateSettings({ autoRotate: checked }) + } + /> +
+ + + +
+ + +
+
+
+
+
+ + {/* Fullscreen toggle */} + +
+
+ + {/* Main content */} +
+ + {/* 3D Viewport */} + +
+ +
+ + + Loading 3D viewport... + +
+
+ } + > + + + + {/* Running indicator */} + {isRunning && ( +
+ + + {isPaused ? 'Paused' : 'Running'} + +
+ )} + + {/* Instance count */} +
+ + {scene.instances.length} objects + +
+
+ + + {/* Console */} + {showConsole && ( + <> + + + + + + )} + +
+
+ ); +} diff --git a/src/components/preview/PreviewConsole.tsx b/src/components/preview/PreviewConsole.tsx new file mode 100644 index 0000000..c532002 --- /dev/null +++ b/src/components/preview/PreviewConsole.tsx @@ -0,0 +1,263 @@ +'use client'; + +import { useRef, useEffect, useState } from 'react'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { + Trash2, + Search, + AlertTriangle, + AlertCircle, + Info, + MessageSquare, + Filter, + ChevronDown, +} from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuCheckboxItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +import { usePreviewStore } from '@/stores/preview-store'; +import { ConsoleOutput } from '@/lib/preview/types'; + +// Icons for different output types +const OUTPUT_ICONS: Record = { + log: , + warn: , + error: , + info: , +}; + +// Colors for different output types +const OUTPUT_COLORS: Record = { + log: 'text-foreground', + warn: 'text-yellow-500', + error: 'text-red-500', + info: 'text-blue-500', +}; + +const OUTPUT_BG: Record = { + log: 'bg-transparent', + warn: 'bg-yellow-500/5', + error: 'bg-red-500/5', + info: 'bg-blue-500/5', +}; + +interface PreviewConsoleProps { + className?: string; +} + +export default function PreviewConsole({ className }: PreviewConsoleProps) { + const { consoleOutputs, clearConsole } = usePreviewStore(); + const scrollRef = useRef(null); + const [autoScroll, setAutoScroll] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [filters, setFilters] = useState({ + log: true, + warn: true, + error: true, + info: true, + }); + + // Auto-scroll to bottom when new output arrives + useEffect(() => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [consoleOutputs, autoScroll]); + + // Filter outputs + const filteredOutputs = consoleOutputs.filter((output) => { + // Type filter + if (!filters[output.type]) return false; + + // Search filter + if (searchQuery) { + return output.message.toLowerCase().includes(searchQuery.toLowerCase()); + } + + return true; + }); + + // Count by type + const counts = consoleOutputs.reduce( + (acc, output) => { + acc[output.type] = (acc[output.type] || 0) + 1; + return acc; + }, + { log: 0, warn: 0, error: 0, info: 0 } as Record + ); + + // Format timestamp + const formatTime = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3, + }); + }; + + return ( +
+ {/* Header */} +
+
+ Console + {counts.error > 0 && ( + + {counts.error} + + )} + {counts.warn > 0 && ( + + {counts.warn} + + )} +
+ +
+ {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="h-7 w-32 pl-7 text-xs" + /> +
+ + {/* Filter dropdown */} + + + + + + + setFilters((f) => ({ ...f, log: checked })) + } + > + + Logs ({counts.log}) + + + setFilters((f) => ({ ...f, info: checked })) + } + > + + Info ({counts.info}) + + + setFilters((f) => ({ ...f, warn: checked })) + } + > + + Warnings ({counts.warn}) + + + setFilters((f) => ({ ...f, error: checked })) + } + > + + Errors ({counts.error}) + + + + + {/* Clear button */} + +
+
+ + {/* Output list */} + { + const target = e.target as HTMLDivElement; + const isAtBottom = + target.scrollHeight - target.scrollTop - target.clientHeight < 50; + setAutoScroll(isAtBottom); + }} + > +
+ {filteredOutputs.length === 0 ? ( +
+ {consoleOutputs.length === 0 + ? 'No output yet. Run a script to see results.' + : 'No matching results.'} +
+ ) : ( + filteredOutputs.map((output) => ( +
+ + {OUTPUT_ICONS[output.type]} + + + {formatTime(output.timestamp)} + + + {output.message} + +
+ )) + )} +
+
+ + {/* Auto-scroll indicator */} + {!autoScroll && filteredOutputs.length > 0 && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/preview/PreviewViewport.tsx b/src/components/preview/PreviewViewport.tsx new file mode 100644 index 0000000..c407057 --- /dev/null +++ b/src/components/preview/PreviewViewport.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useRef, useMemo, Suspense } from 'react'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import { + OrbitControls, + Grid, + GizmoHelper, + GizmoViewport, + Environment, + Stats, + PerspectiveCamera, +} from '@react-three/drei'; +import * as THREE from 'three'; + +import { usePreviewStore } from '@/stores/preview-store'; +import { PreviewInstance, MATERIAL_PROPERTIES } from '@/lib/preview/types'; + +// Single instance mesh component +function InstanceMesh({ instance }: { instance: PreviewInstance }) { + const meshRef = useRef(null); + + // Get material properties + const matProps = MATERIAL_PROPERTIES[instance.material] || MATERIAL_PROPERTIES.Plastic; + + // Create geometry based on shape + const geometry = useMemo(() => { + switch (instance.shape) { + case 'Ball': + return new THREE.SphereGeometry(0.5, 32, 32); + case 'Cylinder': + return new THREE.CylinderGeometry(0.5, 0.5, 1, 32); + case 'Wedge': + // Create a wedge shape + const shape = new THREE.Shape(); + shape.moveTo(0, 0); + shape.lineTo(1, 0); + shape.lineTo(0, 1); + shape.closePath(); + const extrudeSettings = { depth: 1, bevelEnabled: false }; + return new THREE.ExtrudeGeometry(shape, extrudeSettings); + case 'Block': + default: + return new THREE.BoxGeometry(1, 1, 1); + } + }, [instance.shape]); + + // Create material + const material = useMemo(() => { + const color = new THREE.Color(instance.color.r, instance.color.g, instance.color.b); + + if (matProps.emissive) { + return new THREE.MeshStandardMaterial({ + color, + emissive: color, + emissiveIntensity: 0.5, + roughness: matProps.roughness, + metalness: matProps.metalness, + transparent: instance.transparency > 0, + opacity: 1 - instance.transparency, + }); + } + + return new THREE.MeshStandardMaterial({ + color, + roughness: matProps.roughness, + metalness: matProps.metalness, + transparent: instance.transparency > 0, + opacity: 1 - instance.transparency, + }); + }, [instance.color, instance.transparency, instance.material, matProps]); + + if (!instance.visible) return null; + + return ( + + ); +} + +// Scene content +function SceneContent() { + const { scene, settings } = usePreviewStore(); + + return ( + <> + {/* Lighting */} + {scene.lights.map((light) => { + const color = new THREE.Color(light.color.r, light.color.g, light.color.b); + + switch (light.type) { + case 'directional': + return ( + + ); + case 'point': + return ( + + ); + case 'spot': + return ( + + ); + case 'ambient': + return ( + + ); + default: + return null; + } + })} + + {/* Instances */} + {scene.instances.map((instance) => ( + + ))} + + {/* Grid */} + {settings.showGrid && ( + + )} + + {/* Axes Helper */} + {settings.showAxes && ( + + + + )} + + ); +} + +// Camera controller +function CameraController() { + const { scene, settings } = usePreviewStore(); + const controlsRef = useRef(null); + + useFrame(() => { + if (controlsRef.current && settings.autoRotate) { + controlsRef.current.autoRotate = true; + controlsRef.current.autoRotateSpeed = 1; + } else if (controlsRef.current) { + controlsRef.current.autoRotate = false; + } + }); + + return ( + <> + + + + ); +} + +// Loading fallback +function LoadingFallback() { + return ( + + + + + ); +} + +export default function PreviewViewport() { + const { settings } = usePreviewStore(); + + return ( +
+ + }> + + + + + {settings.showStats && } + +
+ ); +} diff --git a/src/components/preview/index.ts b/src/components/preview/index.ts new file mode 100644 index 0000000..503f9bd --- /dev/null +++ b/src/components/preview/index.ts @@ -0,0 +1,3 @@ +export { default as LivePreview } from './LivePreview'; +export { default as PreviewViewport } from './PreviewViewport'; +export { default as PreviewConsole } from './PreviewConsole'; diff --git a/src/lib/preview/lua-runtime.ts b/src/lib/preview/lua-runtime.ts new file mode 100644 index 0000000..dcbca7b --- /dev/null +++ b/src/lib/preview/lua-runtime.ts @@ -0,0 +1,425 @@ +/** + * Lua Runtime using Fengari + * Executes Lua code with a mock Roblox API + */ + +import { createRobloxAPI, Instance } from './roblox-api'; + +export interface LuaRuntimeOutput { + type: 'log' | 'warn' | 'error' | 'info'; + message: string; + timestamp: number; +} + +export interface LuaRuntimeState { + running: boolean; + outputs: LuaRuntimeOutput[]; + error: string | null; + instances: Instance[]; +} + +export interface LuaRuntime { + execute: (code: string) => Promise; + stop: () => void; + getState: () => LuaRuntimeState; + getInstances: () => Instance[]; + onOutput: (callback: (output: LuaRuntimeOutput) => void) => void; + onInstanceUpdate: (callback: (instances: Instance[]) => void) => void; +} + +/** + * Creates a new Lua runtime instance + * Uses JavaScript-based Lua transpilation for simplicity + */ +export function createLuaRuntime(): LuaRuntime { + let state: LuaRuntimeState = { + running: false, + outputs: [], + error: null, + instances: [], + }; + + let outputCallbacks: ((output: LuaRuntimeOutput) => void)[] = []; + let instanceCallbacks: ((instances: Instance[]) => void)[] = []; + let stopFlag = false; + let api = createRobloxAPI(); + + const addOutput = (type: LuaRuntimeOutput['type'], message: string) => { + const output: LuaRuntimeOutput = { + type, + message, + timestamp: Date.now(), + }; + state.outputs.push(output); + outputCallbacks.forEach(cb => cb(output)); + }; + + const notifyInstanceUpdate = () => { + instanceCallbacks.forEach(cb => cb(state.instances)); + }; + + /** + * Simple Lua to JavaScript transpiler for basic Roblox scripts + * This handles common patterns but isn't a full Lua implementation + */ + const transpileLuaToJS = (luaCode: string): string => { + let js = luaCode; + + // Remove comments + js = js.replace(/--\[\[[\s\S]*?\]\]/g, ''); + js = js.replace(/--.*$/gm, ''); + + // Replace local keyword + js = js.replace(/\blocal\s+/g, 'let '); + + // Replace function definitions + js = js.replace(/function\s+(\w+)\s*\((.*?)\)/g, 'async function $1($2)'); + js = js.replace(/function\s*\((.*?)\)/g, 'async function($1)'); + + // Replace end keywords + js = js.replace(/\bend\b/g, '}'); + + // Replace then/do with { + js = js.replace(/\bthen\b/g, '{'); + js = js.replace(/\bdo\b/g, '{'); + + // Replace elseif with else if + js = js.replace(/\belseif\b/g, '} else if'); + + // Replace else (but not else if) + js = js.replace(/\belse\b(?!\s*if)/g, '} else {'); + + // Replace repeat/until + js = js.replace(/\brepeat\b/g, 'do {'); + js = js.replace(/\buntil\s+(.*?)$/gm, '} while (!($1));'); + + // Replace for loops - numeric + js = js.replace( + /for\s+(\w+)\s*=\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d+))?\s*\{/g, + (_, var_, start, end, step) => { + const stepVal = step || '1'; + return `for (let ${var_} = ${start}; ${var_} <= ${end}; ${var_} += ${stepVal}) {`; + } + ); + + // Replace for...in loops (pairs) + js = js.replace( + /for\s+(\w+)\s*,\s*(\w+)\s+in\s+pairs\s*\((.*?)\)\s*\{/g, + 'for (let [$1, $2] of Object.entries($3)) {' + ); + + // Replace for...in loops (ipairs) + js = js.replace( + /for\s+(\w+)\s*,\s*(\w+)\s+in\s+ipairs\s*\((.*?)\)\s*\{/g, + 'for (let [$1, $2] of $3.entries()) {' + ); + + // Replace while loops + js = js.replace(/while\s+(.*?)\s*\{/g, 'while ($1) {'); + + // Replace if statements + js = js.replace(/if\s+(.*?)\s*\{/g, 'if ($1) {'); + + // Replace logical operators + js = js.replace(/\band\b/g, '&&'); + js = js.replace(/\bor\b/g, '||'); + js = js.replace(/\bnot\b/g, '!'); + + // Replace nil with null + js = js.replace(/\bnil\b/g, 'null'); + + // Replace ~= with !== + js = js.replace(/~=/g, '!=='); + + // Replace string concatenation + js = js.replace(/\.\./g, '+'); + + // Replace # length operator + js = js.replace(/#(\w+)/g, '$1.length'); + + // Replace table constructors + js = js.replace(/\{([^{}]*?)\}/g, (match, content) => { + // Check if it looks like an array + if (!/\w+\s*=/.test(content)) { + return `[${content}]`; + } + // It's an object + const converted = content.replace(/\[["']?(\w+)["']?\]\s*=/g, '$1:').replace(/(\w+)\s*=/g, '$1:'); + return `{${converted}}`; + }); + + // Replace print with our output function + js = js.replace(/\bprint\s*\(/g, '__print('); + + // Replace warn + js = js.replace(/\bwarn\s*\(/g, '__warn('); + + // Replace wait + js = js.replace(/\bwait\s*\(/g, 'await __wait('); + js = js.replace(/\btask\.wait\s*\(/g, 'await __wait('); + + // Replace game:GetService + js = js.replace(/game:GetService\s*\(\s*["'](\w+)["']\s*\)/g, '__game.GetService("$1")'); + js = js.replace(/game\.(\w+)/g, '__game.$1'); + + // Replace workspace + js = js.replace(/\bworkspace\b/g, '__workspace'); + + // Replace Instance.new + js = js.replace(/Instance\.new\s*\(\s*["'](\w+)["']\s*(?:,\s*(.*?))?\)/g, (_, className, parent) => { + if (parent) { + return `__Instance.new("${className}", ${parent})`; + } + return `__Instance.new("${className}")`; + }); + + // Replace Vector3.new + js = js.replace(/Vector3\.new\s*\((.*?)\)/g, '__Vector3.new($1)'); + js = js.replace(/Vector3\.zero/g, '__Vector3.zero'); + js = js.replace(/Vector3\.one/g, '__Vector3.one'); + + // Replace Color3 + js = js.replace(/Color3\.new\s*\((.*?)\)/g, '__Color3.new($1)'); + js = js.replace(/Color3\.fromRGB\s*\((.*?)\)/g, '__Color3.fromRGB($1)'); + + // Replace CFrame.new + js = js.replace(/CFrame\.new\s*\((.*?)\)/g, '__CFrame.new($1)'); + + // Replace TweenInfo.new + js = js.replace(/TweenInfo\.new\s*\((.*?)\)/g, '__TweenInfo.new($1)'); + + // Replace Enum references + js = js.replace(/Enum\.(\w+)\.(\w+)/g, '__Enum.$1.$2'); + + // Replace method calls with : to . + js = js.replace(/:(\w+)\s*\(/g, '.$1('); + + // Replace self with this + js = js.replace(/\bself\b/g, 'this'); + + // Replace true/false (they're the same in JS) + + // Replace math functions + js = js.replace(/math\.(\w+)/g, 'Math.$1'); + + // Replace string functions + js = js.replace(/string\.(\w+)/g, (_, fn) => { + const mapping: Record = { + len: 'length', + sub: 'substring', + lower: 'toLowerCase', + upper: 'toUpperCase', + find: 'indexOf', + format: '__stringFormat', + }; + return mapping[fn] || `String.prototype.${fn}`; + }); + + // Replace table functions + js = js.replace(/table\.insert\s*\((.*?),\s*(.*?)\)/g, '$1.push($2)'); + js = js.replace(/table\.remove\s*\((.*?),\s*(.*?)\)/g, '$1.splice($2 - 1, 1)'); + js = js.replace(/table\.concat\s*\((.*?)\)/g, '$1.join("")'); + + return js; + }; + + const execute = async (code: string): Promise => { + // Reset state + state = { + running: true, + outputs: [], + error: null, + instances: [], + }; + stopFlag = false; + api = createRobloxAPI(); + + addOutput('info', 'Starting script execution...'); + + try { + // Transpile Lua to JavaScript + const jsCode = transpileLuaToJS(code); + + // Create execution context + const context = { + __print: (...args: any[]) => { + addOutput('log', args.map(a => String(a)).join(' ')); + }, + __warn: (...args: any[]) => { + addOutput('warn', args.map(a => String(a)).join(' ')); + }, + __wait: async (seconds: number = 0) => { + if (stopFlag) throw new Error('Script stopped'); + await new Promise(resolve => setTimeout(resolve, (seconds || 0.03) * 1000)); + return seconds; + }, + __game: api.game, + __workspace: api.workspace, + __Instance: { + new: (className: string, parent?: Instance) => { + const instance = api.Instance.new(className, parent); + state.instances.push(instance); + notifyInstanceUpdate(); + return instance; + }, + }, + __Vector3: api.Vector3, + __Color3: api.Color3, + __CFrame: api.CFrame, + __TweenInfo: api.TweenInfo, + __Enum: api.Enum, + __stringFormat: (format: string, ...args: any[]) => { + let result = format; + args.forEach((arg, i) => { + result = result.replace(/%[dsf]/, String(arg)); + }); + return result; + }, + console: { + log: (...args: any[]) => addOutput('log', args.map(a => String(a)).join(' ')), + warn: (...args: any[]) => addOutput('warn', args.map(a => String(a)).join(' ')), + error: (...args: any[]) => addOutput('error', args.map(a => String(a)).join(' ')), + }, + setTimeout, + setInterval, + clearTimeout, + clearInterval, + Math, + String, + Number, + Array, + Object, + JSON, + Date, + Promise, + }; + + // Wrap the code in an async IIFE + const wrappedCode = ` + (async () => { + ${jsCode} + })(); + `; + + // Create function with context + const contextKeys = Object.keys(context); + const contextValues = Object.values(context); + + // Use Function constructor to create isolated execution + const fn = new Function(...contextKeys, wrappedCode); + + // Execute + await fn(...contextValues); + + // Start RunService if we have connections + api.runService.start(); + + addOutput('info', 'Script executed successfully'); + } catch (error: any) { + state.error = error.message || 'Unknown error'; + addOutput('error', `Error: ${state.error}`); + } + + state.running = false; + return state; + }; + + const stop = () => { + stopFlag = true; + state.running = false; + api.runService.stop(); + addOutput('info', 'Script stopped'); + }; + + return { + execute, + stop, + getState: () => state, + getInstances: () => state.instances, + onOutput: (callback) => { + outputCallbacks.push(callback); + }, + onInstanceUpdate: (callback) => { + instanceCallbacks.push(callback); + }, + }; +} + +/** + * Parse Lua code to extract instance creations for preview + */ +export function parseScriptForInstances(code: string): Array<{ + className: string; + name?: string; + properties: Record; +}> { + const instances: Array<{ + className: string; + name?: string; + properties: Record; + }> = []; + + // Match Instance.new calls + const instancePattern = /Instance\.new\s*\(\s*["'](\w+)["']/g; + let match; + + while ((match = instancePattern.exec(code)) !== null) { + instances.push({ + className: match[1], + properties: {}, + }); + } + + return instances; +} + +/** + * Validate Lua syntax (basic check) + */ +export function validateLuaSyntax(code: string): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + // Check for balanced keywords + const keywords = { + 'function': 'end', + 'if': 'end', + 'for': 'end', + 'while': 'end', + 'repeat': 'until', + 'do': 'end', + }; + + // Remove strings and comments for analysis + const cleaned = code + .replace(/--\[\[[\s\S]*?\]\]/g, '') + .replace(/--.*$/gm, '') + .replace(/"[^"]*"/g, '""') + .replace(/'[^']*'/g, "''"); + + // Count keyword pairs + for (const [start, end] of Object.entries(keywords)) { + const startRegex = new RegExp(`\\b${start}\\b`, 'g'); + const endRegex = new RegExp(`\\b${end}\\b`, 'g'); + + const startCount = (cleaned.match(startRegex) || []).length; + const endCount = (cleaned.match(endRegex) || []).length; + + if (startCount !== endCount) { + errors.push(`Unmatched '${start}' - found ${startCount} '${start}' but ${endCount} '${end}'`); + } + } + + // Check for common syntax errors + if (/=\s*=\s*=/.test(cleaned)) { + errors.push("Invalid operator '===' - use '==' for equality"); + } + + if (/!\s*=/.test(cleaned)) { + errors.push("Invalid operator '!=' - use '~=' for not equal"); + } + + return { + valid: errors.length === 0, + errors, + }; +} diff --git a/src/lib/preview/roblox-api.ts b/src/lib/preview/roblox-api.ts new file mode 100644 index 0000000..7c1b9ed --- /dev/null +++ b/src/lib/preview/roblox-api.ts @@ -0,0 +1,664 @@ +/** + * Mock Roblox API for Live Preview + * Provides a simplified simulation of Roblox's API surface + */ + +import { Vector3, Color, Euler, Quaternion } from 'three'; + +// Type definitions for the mock API +export interface Instance { + ClassName: string; + Name: string; + Parent: Instance | null; + children: Instance[]; + properties: Record; + + // Methods + Destroy: () => void; + Clone: () => Instance; + FindFirstChild: (name: string) => Instance | null; + GetChildren: () => Instance[]; + IsA: (className: string) => boolean; + WaitForChild: (name: string, timeout?: number) => Promise; +} + +export interface Vector3Value { + X: number; + Y: number; + Z: number; + add: (other: Vector3Value) => Vector3Value; + sub: (other: Vector3Value) => Vector3Value; + mul: (scalar: number) => Vector3Value; + div: (scalar: number) => Vector3Value; + Magnitude: number; + Unit: Vector3Value; + Dot: (other: Vector3Value) => number; + Cross: (other: Vector3Value) => Vector3Value; +} + +export interface Color3Value { + R: number; + G: number; + B: number; +} + +export interface CFrameValue { + Position: Vector3Value; + LookVector: Vector3Value; + RightVector: Vector3Value; + UpVector: Vector3Value; + Rotation: Vector3Value; +} + +// Create Vector3 constructor +export function createVector3(x: number = 0, y: number = 0, z: number = 0): Vector3Value { + const vec: Vector3Value = { + X: x, + Y: y, + Z: z, + add: (other) => createVector3(x + other.X, y + other.Y, z + other.Z), + sub: (other) => createVector3(x - other.X, y - other.Y, z - other.Z), + mul: (scalar) => createVector3(x * scalar, y * scalar, z * scalar), + div: (scalar) => createVector3(x / scalar, y / scalar, z / scalar), + get Magnitude() { + return Math.sqrt(x * x + y * y + z * z); + }, + get Unit() { + const mag = Math.sqrt(x * x + y * y + z * z); + return mag > 0 ? createVector3(x / mag, y / mag, z / mag) : createVector3(); + }, + Dot: (other) => x * other.X + y * other.Y + z * other.Z, + Cross: (other) => createVector3( + y * other.Z - z * other.Y, + z * other.X - x * other.Z, + x * other.Y - y * other.X + ), + }; + return vec; +} + +// Create Color3 constructor +export function createColor3(r: number = 0, g: number = 0, b: number = 0): Color3Value { + return { R: r, G: g, B: b }; +} + +export function createColor3FromRGB(r: number, g: number, b: number): Color3Value { + return { R: r / 255, G: g / 255, B: b / 255 }; +} + +// Create CFrame constructor +export function createCFrame(x: number = 0, y: number = 0, z: number = 0): CFrameValue { + return { + Position: createVector3(x, y, z), + LookVector: createVector3(0, 0, -1), + RightVector: createVector3(1, 0, 0), + UpVector: createVector3(0, 1, 0), + Rotation: createVector3(0, 0, 0), + }; +} + +// Instance factory +let instanceIdCounter = 0; + +export function createInstance(className: string, parent?: Instance | null): Instance { + const instance: Instance = { + ClassName: className, + Name: className + instanceIdCounter++, + Parent: parent || null, + children: [], + properties: getDefaultProperties(className), + + Destroy() { + if (this.Parent) { + const idx = this.Parent.children.indexOf(this); + if (idx > -1) this.Parent.children.splice(idx, 1); + } + this.children.forEach(child => child.Destroy()); + }, + + Clone() { + const clone = createInstance(this.ClassName); + clone.Name = this.Name; + clone.properties = { ...this.properties }; + this.children.forEach(child => { + const childClone = child.Clone(); + childClone.Parent = clone; + clone.children.push(childClone); + }); + return clone; + }, + + FindFirstChild(name: string) { + return this.children.find(c => c.Name === name) || null; + }, + + GetChildren() { + return [...this.children]; + }, + + IsA(className: string) { + return this.ClassName === className || getClassHierarchy(this.ClassName).includes(className); + }, + + async WaitForChild(name: string, timeout: number = 5) { + const existing = this.FindFirstChild(name); + if (existing) return existing; + + return new Promise((resolve, reject) => { + const start = Date.now(); + const check = () => { + const child = this.FindFirstChild(name); + if (child) { + resolve(child); + } else if (Date.now() - start > timeout * 1000) { + reject(new Error(`WaitForChild timeout: ${name}`)); + } else { + setTimeout(check, 100); + } + }; + check(); + }); + }, + }; + + if (parent) { + parent.children.push(instance); + } + + return instance; +} + +// Default properties for common classes +function getDefaultProperties(className: string): Record { + const defaults: Record> = { + Part: { + Position: createVector3(0, 0, 0), + Size: createVector3(4, 1, 2), + Color: createColor3(0.6, 0.6, 0.6), + BrickColor: 'Medium stone grey', + Transparency: 0, + Anchored: false, + CanCollide: true, + Material: 'Plastic', + Shape: 'Block', + }, + SpawnLocation: { + Position: createVector3(0, 1, 0), + Size: createVector3(6, 1, 6), + Color: createColor3(0.05, 0.5, 0.05), + Anchored: true, + CanCollide: true, + }, + Model: { + PrimaryPart: null, + }, + Script: { + Source: '', + Enabled: true, + }, + LocalScript: { + Source: '', + Enabled: true, + }, + ModuleScript: { + Source: '', + }, + PointLight: { + Brightness: 1, + Color: createColor3(1, 1, 1), + Range: 8, + Shadows: false, + }, + SpotLight: { + Brightness: 1, + Color: createColor3(1, 1, 1), + Range: 16, + Angle: 90, + Shadows: true, + }, + SurfaceGui: { + Face: 'Front', + Enabled: true, + }, + TextLabel: { + Text: 'Label', + TextColor3: createColor3(0, 0, 0), + TextSize: 14, + BackgroundTransparency: 0, + BackgroundColor3: createColor3(1, 1, 1), + }, + Sound: { + SoundId: '', + Volume: 0.5, + Playing: false, + Looped: false, + }, + ParticleEmitter: { + Enabled: true, + Rate: 20, + Lifetime: { Min: 1, Max: 2 }, + Speed: { Min: 5, Max: 5 }, + Color: createColor3(1, 1, 1), + }, + Humanoid: { + Health: 100, + MaxHealth: 100, + WalkSpeed: 16, + JumpPower: 50, + }, + Camera: { + CFrame: createCFrame(0, 5, 10), + FieldOfView: 70, + CameraType: 'Custom', + }, + }; + + return defaults[className] || {}; +} + +// Class hierarchy for IsA checks +function getClassHierarchy(className: string): string[] { + const hierarchies: Record = { + Part: ['BasePart', 'PVInstance', 'Instance'], + SpawnLocation: ['Part', 'BasePart', 'PVInstance', 'Instance'], + Model: ['PVInstance', 'Instance'], + Script: ['BaseScript', 'LuaSourceContainer', 'Instance'], + LocalScript: ['BaseScript', 'LuaSourceContainer', 'Instance'], + ModuleScript: ['LuaSourceContainer', 'Instance'], + Humanoid: ['Instance'], + Sound: ['Instance'], + PointLight: ['Light', 'Instance'], + SpotLight: ['Light', 'Instance'], + Camera: ['Instance'], + }; + + return hierarchies[className] || ['Instance']; +} + +// Services +export interface GameService { + Name: string; + instances: Record; +} + +export function createGameServices() { + const workspace = createInstance('Workspace'); + workspace.Name = 'Workspace'; + + const replicatedStorage = createInstance('ReplicatedStorage'); + replicatedStorage.Name = 'ReplicatedStorage'; + + const players = createInstance('Players'); + players.Name = 'Players'; + + const lighting = createInstance('Lighting'); + lighting.Name = 'Lighting'; + lighting.properties = { + Ambient: createColor3(0.5, 0.5, 0.5), + Brightness: 2, + ClockTime: 14, + GeographicLatitude: 41.7, + GlobalShadows: true, + OutdoorAmbient: createColor3(0.5, 0.5, 0.5), + }; + + const serverStorage = createInstance('ServerStorage'); + serverStorage.Name = 'ServerStorage'; + + const starterGui = createInstance('StarterGui'); + starterGui.Name = 'StarterGui'; + + const starterPack = createInstance('StarterPack'); + starterPack.Name = 'StarterPack'; + + const starterPlayer = createInstance('StarterPlayer'); + starterPlayer.Name = 'StarterPlayer'; + + return { + Workspace: workspace, + ReplicatedStorage: replicatedStorage, + Players: players, + Lighting: lighting, + ServerStorage: serverStorage, + StarterGui: starterGui, + StarterPack: starterPack, + StarterPlayer: starterPlayer, + }; +} + +// Events system +export type EventCallback = (...args: any[]) => void; + +export interface RBXScriptSignal { + Connect: (callback: EventCallback) => { Disconnect: () => void }; + Wait: () => Promise; + callbacks: EventCallback[]; + fire: (...args: any[]) => void; +} + +export function createSignal(): RBXScriptSignal { + const callbacks: EventCallback[] = []; + + return { + callbacks, + Connect(callback: EventCallback) { + callbacks.push(callback); + return { + Disconnect: () => { + const idx = callbacks.indexOf(callback); + if (idx > -1) callbacks.splice(idx, 1); + }, + }; + }, + Wait() { + return new Promise((resolve) => { + const handler = (...args: any[]) => { + const idx = callbacks.indexOf(handler); + if (idx > -1) callbacks.splice(idx, 1); + resolve(args); + }; + callbacks.push(handler); + }); + }, + fire(...args: any[]) { + callbacks.forEach(cb => { + try { + cb(...args); + } catch (e) { + console.error('Event callback error:', e); + } + }); + }, + }; +} + +// TweenService mock +export interface TweenInfo { + Time: number; + EasingStyle: string; + EasingDirection: string; + RepeatCount: number; + Reverses: boolean; + DelayTime: number; +} + +export function createTweenInfo( + time: number = 1, + easingStyle: string = 'Linear', + easingDirection: string = 'Out', + repeatCount: number = 0, + reverses: boolean = false, + delayTime: number = 0 +): TweenInfo { + return { Time: time, EasingStyle: easingStyle, EasingDirection: easingDirection, RepeatCount: repeatCount, Reverses: reverses, DelayTime: delayTime }; +} + +export interface Tween { + Play: () => void; + Pause: () => void; + Cancel: () => void; + Completed: RBXScriptSignal; +} + +export function createTweenService() { + return { + Create(instance: Instance, tweenInfo: TweenInfo, properties: Record): Tween { + const completed = createSignal(); + let animationId: number | null = null; + let isPaused = false; + + return { + Completed: completed, + Play() { + if (isPaused) { + isPaused = false; + return; + } + + const startValues: Record = {}; + const endValues = properties; + + for (const key in endValues) { + startValues[key] = instance.properties[key]; + } + + const duration = tweenInfo.Time * 1000; + const startTime = Date.now(); + + const animate = () => { + if (isPaused) return; + + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + + // Apply easing + const easedProgress = applyEasing(progress, tweenInfo.EasingStyle, tweenInfo.EasingDirection); + + // Interpolate values + for (const key in endValues) { + const start = startValues[key]; + const end = endValues[key]; + + if (typeof start === 'number' && typeof end === 'number') { + instance.properties[key] = start + (end - start) * easedProgress; + } else if (start && typeof start === 'object' && 'X' in start) { + // Vector3 + instance.properties[key] = createVector3( + start.X + (end.X - start.X) * easedProgress, + start.Y + (end.Y - start.Y) * easedProgress, + start.Z + (end.Z - start.Z) * easedProgress + ); + } else if (start && typeof start === 'object' && 'R' in start) { + // Color3 + instance.properties[key] = createColor3( + start.R + (end.R - start.R) * easedProgress, + start.G + (end.G - start.G) * easedProgress, + start.B + (end.B - start.B) * easedProgress + ); + } + } + + if (progress < 1) { + animationId = requestAnimationFrame(animate); + } else { + completed.fire(); + } + }; + + animate(); + }, + Pause() { + isPaused = true; + }, + Cancel() { + if (animationId) { + cancelAnimationFrame(animationId); + } + }, + }; + }, + }; +} + +function applyEasing(t: number, style: string, direction: string): number { + // Simplified easing functions + const easingFunctions: Record number> = { + Linear: (t) => t, + Quad: (t) => t * t, + Cubic: (t) => t * t * t, + Sine: (t) => 1 - Math.cos((t * Math.PI) / 2), + Bounce: (t) => { + if (t < 1 / 2.75) return 7.5625 * t * t; + if (t < 2 / 2.75) return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75; + if (t < 2.5 / 2.75) return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375; + return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375; + }, + Elastic: (t) => t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * (2 * Math.PI) / 3), + }; + + const ease = easingFunctions[style] || easingFunctions.Linear; + + if (direction === 'In') { + return ease(t); + } else if (direction === 'Out') { + return 1 - ease(1 - t); + } else if (direction === 'InOut') { + return t < 0.5 ? ease(t * 2) / 2 : 1 - ease((1 - t) * 2) / 2; + } + + return t; +} + +// RunService mock +export function createRunService() { + const heartbeat = createSignal(); + const renderStepped = createSignal(); + const stepped = createSignal(); + + let lastTime = Date.now(); + let isRunning = false; + + const tick = () => { + if (!isRunning) return; + + const now = Date.now(); + const dt = (now - lastTime) / 1000; + lastTime = now; + + stepped.fire(now / 1000, dt); + heartbeat.fire(dt); + renderStepped.fire(dt); + + requestAnimationFrame(tick); + }; + + return { + Heartbeat: heartbeat, + RenderStepped: renderStepped, + Stepped: stepped, + start() { + isRunning = true; + lastTime = Date.now(); + requestAnimationFrame(tick); + }, + stop() { + isRunning = false; + }, + }; +} + +// UserInputService mock +export function createUserInputService() { + const inputBegan = createSignal(); + const inputEnded = createSignal(); + const inputChanged = createSignal(); + + return { + InputBegan: inputBegan, + InputEnded: inputEnded, + InputChanged: inputChanged, + GetMouseLocation: () => ({ X: 0, Y: 0 }), + IsKeyDown: (keyCode: string) => false, + IsMouseButtonPressed: (button: number) => false, + }; +} + +// Export combined API +export function createRobloxAPI() { + const services = createGameServices(); + const runService = createRunService(); + const tweenService = createTweenService(); + const userInputService = createUserInputService(); + + return { + game: { + GetService: (serviceName: string) => { + switch (serviceName) { + case 'Workspace': return services.Workspace; + case 'ReplicatedStorage': return services.ReplicatedStorage; + case 'Players': return services.Players; + case 'Lighting': return services.Lighting; + case 'ServerStorage': return services.ServerStorage; + case 'StarterGui': return services.StarterGui; + case 'StarterPack': return services.StarterPack; + case 'StarterPlayer': return services.StarterPlayer; + case 'TweenService': return tweenService; + case 'RunService': return runService; + case 'UserInputService': return userInputService; + default: throw new Error(`Unknown service: ${serviceName}`); + } + }, + Workspace: services.Workspace, + }, + workspace: services.Workspace, + Vector3: { + new: createVector3, + zero: createVector3(0, 0, 0), + one: createVector3(1, 1, 1), + xAxis: createVector3(1, 0, 0), + yAxis: createVector3(0, 1, 0), + zAxis: createVector3(0, 0, 1), + }, + Color3: { + new: createColor3, + fromRGB: createColor3FromRGB, + }, + CFrame: { + new: createCFrame, + }, + TweenInfo: { + new: createTweenInfo, + }, + Instance: { + new: createInstance, + }, + Enum: { + Material: { + Plastic: 'Plastic', + Wood: 'Wood', + Metal: 'Metal', + Glass: 'Glass', + Neon: 'Neon', + Grass: 'Grass', + Sand: 'Sand', + Brick: 'Brick', + Concrete: 'Concrete', + Ice: 'Ice', + Marble: 'Marble', + Granite: 'Granite', + SmoothPlastic: 'SmoothPlastic', + ForceField: 'ForceField', + }, + PartType: { + Block: 'Block', + Ball: 'Ball', + Cylinder: 'Cylinder', + Wedge: 'Wedge', + }, + KeyCode: { + W: 'W', A: 'A', S: 'S', D: 'D', + E: 'E', F: 'F', G: 'G', Q: 'Q', R: 'R', + Space: 'Space', LeftShift: 'LeftShift', LeftControl: 'LeftControl', + One: 'One', Two: 'Two', Three: 'Three', + }, + EasingStyle: { + Linear: 'Linear', Quad: 'Quad', Cubic: 'Cubic', + Sine: 'Sine', Bounce: 'Bounce', Elastic: 'Elastic', + }, + EasingDirection: { + In: 'In', Out: 'Out', InOut: 'InOut', + }, + }, + print: (...args: any[]) => console.log('[Roblox]', ...args), + warn: (...args: any[]) => console.warn('[Roblox]', ...args), + error: (message: string) => { throw new Error(message); }, + wait: (seconds: number = 0) => new Promise(resolve => setTimeout(resolve, seconds * 1000)), + task: { + wait: (seconds: number = 0) => new Promise(resolve => setTimeout(resolve, seconds * 1000)), + spawn: (fn: () => void) => setTimeout(fn, 0), + delay: (seconds: number, fn: () => void) => setTimeout(fn, seconds * 1000), + }, + services, + runService, + }; +} diff --git a/src/lib/preview/types.ts b/src/lib/preview/types.ts new file mode 100644 index 0000000..495ddd5 --- /dev/null +++ b/src/lib/preview/types.ts @@ -0,0 +1,220 @@ +/** + * Live Preview Types + */ + +export interface PreviewInstance { + id: string; + className: string; + name: string; + position: { x: number; y: number; z: number }; + rotation: { x: number; y: number; z: number }; + scale: { x: number; y: number; z: number }; + color: { r: number; g: number; b: number }; + transparency: number; + material: string; + shape: 'Block' | 'Ball' | 'Cylinder' | 'Wedge'; + visible: boolean; + children: PreviewInstance[]; + properties: Record; +} + +export interface PreviewLight { + id: string; + type: 'point' | 'spot' | 'directional' | 'ambient'; + position: { x: number; y: number; z: number }; + color: { r: number; g: number; b: number }; + intensity: number; + range?: number; + angle?: number; + castShadow: boolean; +} + +export interface PreviewCamera { + position: { x: number; y: number; z: number }; + target: { x: number; y: number; z: number }; + fov: number; + near: number; + far: number; +} + +export interface PreviewScene { + instances: PreviewInstance[]; + lights: PreviewLight[]; + camera: PreviewCamera; + skybox: string | null; + ambientColor: { r: number; g: number; b: number }; + ambientIntensity: number; + fogEnabled: boolean; + fogColor: { r: number; g: number; b: number }; + fogNear: number; + fogFar: number; +} + +export interface ConsoleOutput { + id: string; + type: 'log' | 'warn' | 'error' | 'info'; + message: string; + timestamp: number; + source?: string; + line?: number; +} + +export interface PreviewSettings { + showGrid: boolean; + showAxes: boolean; + showStats: boolean; + showWireframe: boolean; + shadowsEnabled: boolean; + antialias: boolean; + autoRotate: boolean; + backgroundColor: string; +} + +export const DEFAULT_PREVIEW_SETTINGS: PreviewSettings = { + showGrid: true, + showAxes: true, + showStats: false, + showWireframe: false, + shadowsEnabled: true, + antialias: true, + autoRotate: false, + backgroundColor: '#1a1a2e', +}; + +export const DEFAULT_CAMERA: PreviewCamera = { + position: { x: 10, y: 8, z: 10 }, + target: { x: 0, y: 0, z: 0 }, + fov: 70, + near: 0.1, + far: 1000, +}; + +export function createDefaultScene(): PreviewScene { + return { + instances: [ + // Default baseplate + { + id: 'baseplate', + className: 'Part', + name: 'Baseplate', + position: { x: 0, y: -0.5, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 100, y: 1, z: 100 }, + color: { r: 0.3, g: 0.5, b: 0.3 }, + transparency: 0, + material: 'Grass', + shape: 'Block', + visible: true, + children: [], + properties: {}, + }, + // Default spawn + { + id: 'spawnlocation', + className: 'SpawnLocation', + name: 'SpawnLocation', + position: { x: 0, y: 0.5, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 6, y: 1, z: 6 }, + color: { r: 0.2, g: 0.6, b: 0.2 }, + transparency: 0, + material: 'Plastic', + shape: 'Block', + visible: true, + children: [], + properties: {}, + }, + ], + lights: [ + { + id: 'sunlight', + type: 'directional', + position: { x: 50, y: 100, z: 50 }, + color: { r: 1, g: 0.98, b: 0.9 }, + intensity: 1.5, + castShadow: true, + }, + { + id: 'ambient', + type: 'ambient', + position: { x: 0, y: 0, z: 0 }, + color: { r: 0.4, g: 0.4, b: 0.5 }, + intensity: 0.5, + castShadow: false, + }, + ], + camera: DEFAULT_CAMERA, + skybox: null, + ambientColor: { r: 0.5, g: 0.5, b: 0.6 }, + ambientIntensity: 0.5, + fogEnabled: false, + fogColor: { r: 0.8, g: 0.85, b: 0.9 }, + fogNear: 100, + fogFar: 500, + }; +} + +/** + * Convert Roblox API instance to preview instance + */ +export function convertToPreviewInstance(instance: any, id: string): PreviewInstance { + const props = instance.properties || {}; + + const position = props.Position || { X: 0, Y: 0, Z: 0 }; + const size = props.Size || { X: 4, Y: 1, Z: 2 }; + const color = props.Color || { R: 0.6, G: 0.6, B: 0.6 }; + + return { + id, + className: instance.ClassName, + name: instance.Name || instance.ClassName, + position: { + x: position.X || 0, + y: position.Y || 0, + z: position.Z || 0, + }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { + x: size.X || 4, + y: size.Y || 1, + z: size.Z || 2, + }, + color: { + r: color.R || 0.6, + g: color.G || 0.6, + b: color.B || 0.6, + }, + transparency: props.Transparency || 0, + material: props.Material || 'Plastic', + shape: props.Shape || 'Block', + visible: true, + children: (instance.children || []).map((child: any, i: number) => + convertToPreviewInstance(child, `${id}-${i}`) + ), + properties: props, + }; +} + +/** + * Material to Three.js mapping + */ +export const MATERIAL_PROPERTIES: Record = { + Plastic: { roughness: 0.5, metalness: 0.0 }, + SmoothPlastic: { roughness: 0.2, metalness: 0.0 }, + Wood: { roughness: 0.8, metalness: 0.0 }, + Metal: { roughness: 0.3, metalness: 0.9 }, + Glass: { roughness: 0.1, metalness: 0.0 }, + Neon: { roughness: 0.0, metalness: 0.0, emissive: true }, + Grass: { roughness: 0.9, metalness: 0.0 }, + Sand: { roughness: 1.0, metalness: 0.0 }, + Brick: { roughness: 0.9, metalness: 0.0 }, + Concrete: { roughness: 0.95, metalness: 0.0 }, + Ice: { roughness: 0.1, metalness: 0.0 }, + Marble: { roughness: 0.2, metalness: 0.1 }, + Granite: { roughness: 0.85, metalness: 0.05 }, + ForceField: { roughness: 0.0, metalness: 0.0, emissive: true }, +}; diff --git a/src/stores/preview-store.ts b/src/stores/preview-store.ts new file mode 100644 index 0000000..5d71547 --- /dev/null +++ b/src/stores/preview-store.ts @@ -0,0 +1,220 @@ +/** + * Live Preview Store + * Manages preview state, console output, and runtime + */ + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { + PreviewScene, + PreviewInstance, + ConsoleOutput, + PreviewSettings, + DEFAULT_PREVIEW_SETTINGS, + createDefaultScene, + convertToPreviewInstance, +} from '@/lib/preview/types'; +import { createLuaRuntime, LuaRuntime, LuaRuntimeOutput } from '@/lib/preview/lua-runtime'; + +interface PreviewState { + // Scene + scene: PreviewScene; + + // Runtime + isRunning: boolean; + isPaused: boolean; + runtime: LuaRuntime | null; + + // Console + consoleOutputs: ConsoleOutput[]; + maxConsoleOutputs: number; + + // Settings + settings: PreviewSettings; + + // Code + currentCode: string; + + // Actions + setScene: (scene: PreviewScene) => void; + addInstance: (instance: PreviewInstance) => void; + removeInstance: (id: string) => void; + updateInstance: (id: string, updates: Partial) => void; + clearInstances: () => void; + + // Runtime actions + runScript: (code: string) => Promise; + stopScript: () => void; + pauseScript: () => void; + resumeScript: () => void; + resetScene: () => void; + + // Console actions + addConsoleOutput: (output: Omit) => void; + clearConsole: () => void; + + // Settings actions + updateSettings: (settings: Partial) => void; +} + +let outputIdCounter = 0; + +export const usePreviewStore = create()( + persist( + (set, get) => ({ + // Initial state + scene: createDefaultScene(), + isRunning: false, + isPaused: false, + runtime: null, + consoleOutputs: [], + maxConsoleOutputs: 1000, + settings: DEFAULT_PREVIEW_SETTINGS, + currentCode: '', + + // Scene actions + setScene: (scene) => set({ scene }), + + addInstance: (instance) => + set((state) => ({ + scene: { + ...state.scene, + instances: [...state.scene.instances, instance], + }, + })), + + removeInstance: (id) => + set((state) => ({ + scene: { + ...state.scene, + instances: state.scene.instances.filter((i) => i.id !== id), + }, + })), + + updateInstance: (id, updates) => + set((state) => ({ + scene: { + ...state.scene, + instances: state.scene.instances.map((i) => + i.id === id ? { ...i, ...updates } : i + ), + }, + })), + + clearInstances: () => + set((state) => ({ + scene: { + ...state.scene, + instances: state.scene.instances.filter( + (i) => i.id === 'baseplate' || i.id === 'spawnlocation' + ), + }, + })), + + // Runtime actions + runScript: async (code: string) => { + const { stopScript, addConsoleOutput } = get(); + + // Stop any existing script + stopScript(); + + // Create new runtime + const runtime = createLuaRuntime(); + + // Set up output listener + runtime.onOutput((output: LuaRuntimeOutput) => { + addConsoleOutput({ + type: output.type, + message: output.message, + timestamp: output.timestamp, + }); + }); + + // Set up instance listener + runtime.onInstanceUpdate((instances) => { + set((state) => { + const baseInstances = state.scene.instances.filter( + (i) => i.id === 'baseplate' || i.id === 'spawnlocation' + ); + + const newInstances = instances.map((inst, idx) => + convertToPreviewInstance(inst, `runtime-${idx}`) + ); + + return { + scene: { + ...state.scene, + instances: [...baseInstances, ...newInstances], + }, + }; + }); + }); + + set({ runtime, isRunning: true, isPaused: false, currentCode: code }); + + // Execute script + await runtime.execute(code); + + set({ isRunning: false }); + }, + + stopScript: () => { + const { runtime } = get(); + if (runtime) { + runtime.stop(); + } + set({ runtime: null, isRunning: false, isPaused: false }); + }, + + pauseScript: () => { + set({ isPaused: true }); + }, + + resumeScript: () => { + set({ isPaused: false }); + }, + + resetScene: () => { + const { stopScript, clearConsole } = get(); + stopScript(); + clearConsole(); + set({ + scene: createDefaultScene(), + currentCode: '', + }); + }, + + // Console actions + addConsoleOutput: (output) => + set((state) => { + const newOutput: ConsoleOutput = { + ...output, + id: `output-${outputIdCounter++}`, + }; + + let outputs = [...state.consoleOutputs, newOutput]; + + // Trim if over limit + if (outputs.length > state.maxConsoleOutputs) { + outputs = outputs.slice(-state.maxConsoleOutputs); + } + + return { consoleOutputs: outputs }; + }), + + clearConsole: () => set({ consoleOutputs: [] }), + + // Settings actions + updateSettings: (newSettings) => + set((state) => ({ + settings: { ...state.settings, ...newSettings }, + })), + }), + { + name: 'aethex-preview', + partialize: (state) => ({ + settings: state.settings, + }), + } + ) +);