diff --git a/pkgs/webview-ui/app/eslint.config.mjs b/pkgs/webview-ui/app/eslint.config.mjs
index 6b35c3e35..c837e0825 100644
--- a/pkgs/webview-ui/app/eslint.config.mjs
+++ b/pkgs/webview-ui/app/eslint.config.mjs
@@ -27,6 +27,7 @@ const config = tseslint.config(
// TODO: make this more strict by removing later
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
},
},
);
diff --git a/pkgs/webview-ui/app/package-lock.json b/pkgs/webview-ui/app/package-lock.json
index c1b6ca50d..0b19c0ea4 100644
--- a/pkgs/webview-ui/app/package-lock.json
+++ b/pkgs/webview-ui/app/package-lock.json
@@ -20,7 +20,8 @@
"nanoid": "^5.0.7",
"solid-js": "^1.8.11",
"solid-markdown": "^2.0.13",
- "solid-toast": "^0.5.0"
+ "solid-toast": "^0.5.0",
+ "three": "^0.176.0"
},
"devDependencies": {
"@babel/plugin-syntax-import-attributes": "^7.27.1",
@@ -28,6 +29,7 @@
"@tailwindcss/typography": "^0.5.13",
"@types/json-schema": "^7.0.15",
"@types/node": "^20.12.12",
+ "@types/three": "^0.176.0",
"@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19",
"classnames": "^2.5.1",
@@ -678,6 +680,13 @@
"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==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -2063,6 +2072,13 @@
"node": ">=10.13.0"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2165,12 +2181,42 @@
"undici-types": "~6.19.2"
}
},
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.176.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.176.0.tgz",
+ "integrity": "sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "^0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.18.1"
+ }
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
+ "node_modules/@types/webxr": {
+ "version": "0.5.22",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz",
+ "integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
@@ -2616,6 +2662,13 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@webgpu/types": {
+ "version": "0.1.60",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.60.tgz",
+ "integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
@@ -4000,6 +4053,13 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5060,6 +5120,13 @@
"node": ">= 8"
}
},
+ "node_modules/meshoptimizer": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
+ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@@ -7172,6 +7239,12 @@
"node": ">=0.8"
}
},
+ "node_modules/three": {
+ "version": "0.176.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.176.0.tgz",
+ "integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==",
+ "license": "MIT"
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
diff --git a/pkgs/webview-ui/app/package.json b/pkgs/webview-ui/app/package.json
index 32298f48c..f7f561603 100644
--- a/pkgs/webview-ui/app/package.json
+++ b/pkgs/webview-ui/app/package.json
@@ -14,9 +14,12 @@
},
"license": "MIT",
"devDependencies": {
+ "@babel/plugin-syntax-import-attributes": "^7.27.1",
"@eslint/js": "^9.3.0",
"@tailwindcss/typography": "^0.5.13",
+ "@types/json-schema": "^7.0.15",
"@types/node": "^20.12.12",
+ "@types/three": "^0.176.0",
"@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19",
"classnames": "^2.5.1",
@@ -32,10 +35,8 @@
"typescript-eslint": "^7.10.0",
"vite": "^5.0.11",
"vite-plugin-solid": "^2.8.2",
- "vitest": "^1.6.0",
- "@types/json-schema": "^7.0.15",
- "@babel/plugin-syntax-import-attributes": "^7.27.1",
- "vite-plugin-solid-svg": "^0.8.1"
+ "vite-plugin-solid-svg": "^0.8.1",
+ "vitest": "^1.6.0"
},
"dependencies": {
"@floating-ui/dom": "^1.6.8",
@@ -49,6 +50,7 @@
"nanoid": "^5.0.7",
"solid-js": "^1.8.11",
"solid-markdown": "^2.0.13",
- "solid-toast": "^0.5.0"
+ "solid-toast": "^0.5.0",
+ "three": "^0.176.0"
}
}
diff --git a/pkgs/webview-ui/app/src/index.tsx b/pkgs/webview-ui/app/src/index.tsx
index 94b6ba375..d9a863abb 100644
--- a/pkgs/webview-ui/app/src/index.tsx
+++ b/pkgs/webview-ui/app/src/index.tsx
@@ -23,6 +23,7 @@ import { IconVariant } from "./components/icon";
import { Components } from "./routes/components";
import { activeURI } from "./App";
import { VarsForMachine, VarsStep } from "./routes/machines/install/vars-step";
+import { ThreePlayground } from "./three";
export const client = new QueryClient();
@@ -157,6 +158,11 @@ export const routes: AppRoute[] = [
label: "Local Hosts",
component: () =>