Feat(UI/3d): init 3d prototype
This commit is contained in:
@@ -27,6 +27,7 @@ const config = tseslint.config(
|
|||||||
// TODO: make this more strict by removing later
|
// TODO: make this more strict by removing later
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
75
pkgs/webview-ui/app/package-lock.json
generated
75
pkgs/webview-ui/app/package-lock.json
generated
@@ -20,7 +20,8 @@
|
|||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"solid-js": "^1.8.11",
|
"solid-js": "^1.8.11",
|
||||||
"solid-markdown": "^2.0.13",
|
"solid-markdown": "^2.0.13",
|
||||||
"solid-toast": "^0.5.0"
|
"solid-toast": "^0.5.0",
|
||||||
|
"three": "^0.176.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^20.12.12",
|
||||||
|
"@types/three": "^0.176.0",
|
||||||
"@typescript-eslint/parser": "^7.10.0",
|
"@typescript-eslint/parser": "^7.10.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -678,6 +680,13 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
@@ -2063,6 +2072,13 @@
|
|||||||
"node": ">=10.13.0"
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -2165,12 +2181,42 @@
|
|||||||
"undici-types": "~6.19.2"
|
"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": {
|
"node_modules/@types/unist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.18.0",
|
"version": "7.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
|
||||||
@@ -2616,6 +2662,13 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"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": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
@@ -4000,6 +4053,13 @@
|
|||||||
"reusify": "^1.0.4"
|
"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": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@@ -5060,6 +5120,13 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/micromark": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
||||||
@@ -7172,6 +7239,12 @@
|
|||||||
"node": ">=0.8"
|
"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": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|||||||
@@ -14,9 +14,12 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||||
"@eslint/js": "^9.3.0",
|
"@eslint/js": "^9.3.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^20.12.12",
|
||||||
|
"@types/three": "^0.176.0",
|
||||||
"@typescript-eslint/parser": "^7.10.0",
|
"@typescript-eslint/parser": "^7.10.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -32,10 +35,8 @@
|
|||||||
"typescript-eslint": "^7.10.0",
|
"typescript-eslint": "^7.10.0",
|
||||||
"vite": "^5.0.11",
|
"vite": "^5.0.11",
|
||||||
"vite-plugin-solid": "^2.8.2",
|
"vite-plugin-solid": "^2.8.2",
|
||||||
"vitest": "^1.6.0",
|
"vite-plugin-solid-svg": "^0.8.1",
|
||||||
"@types/json-schema": "^7.0.15",
|
"vitest": "^1.6.0"
|
||||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
|
||||||
"vite-plugin-solid-svg": "^0.8.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.6.8",
|
"@floating-ui/dom": "^1.6.8",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"solid-js": "^1.8.11",
|
"solid-js": "^1.8.11",
|
||||||
"solid-markdown": "^2.0.13",
|
"solid-markdown": "^2.0.13",
|
||||||
"solid-toast": "^0.5.0"
|
"solid-toast": "^0.5.0",
|
||||||
|
"three": "^0.176.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { IconVariant } from "./components/icon";
|
|||||||
import { Components } from "./routes/components";
|
import { Components } from "./routes/components";
|
||||||
import { activeURI } from "./App";
|
import { activeURI } from "./App";
|
||||||
import { VarsForMachine, VarsStep } from "./routes/machines/install/vars-step";
|
import { VarsForMachine, VarsStep } from "./routes/machines/install/vars-step";
|
||||||
|
import { ThreePlayground } from "./three";
|
||||||
|
|
||||||
export const client = new QueryClient();
|
export const client = new QueryClient();
|
||||||
|
|
||||||
@@ -157,6 +158,11 @@ export const routes: AppRoute[] = [
|
|||||||
label: "Local Hosts",
|
label: "Local Hosts",
|
||||||
component: () => <HostList />,
|
component: () => <HostList />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/3d",
|
||||||
|
label: "3D-Playground",
|
||||||
|
component: () => <ThreePlayground />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/api_testing",
|
path: "/api_testing",
|
||||||
label: "api_testing",
|
label: "api_testing",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const ModuleItem = (props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={cx(
|
class={cx(
|
||||||
"col-span-1 flex flex-col gap-3 border-b border-secondary-200 pb-4 gap-2",
|
"col-span-1 flex flex-col border-b border-secondary-200 pb-4 gap-2",
|
||||||
props.class,
|
props.class,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
271
pkgs/webview-ui/app/src/three.tsx
Normal file
271
pkgs/webview-ui/app/src/three.tsx
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { Button } from "./components/button";
|
||||||
|
import Icon from "./components/icon";
|
||||||
|
|
||||||
|
function addCubesSpiral({
|
||||||
|
scene,
|
||||||
|
count,
|
||||||
|
gap,
|
||||||
|
selected,
|
||||||
|
}: {
|
||||||
|
scene: THREE.Scene;
|
||||||
|
count: number;
|
||||||
|
gap: number;
|
||||||
|
selected?: string;
|
||||||
|
}) {
|
||||||
|
const cubeSize = 1;
|
||||||
|
const baseSize = 1.4;
|
||||||
|
|
||||||
|
const cubeGeometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
|
||||||
|
const baseGeometry = new THREE.BoxGeometry(baseSize, 0.05, baseSize);
|
||||||
|
|
||||||
|
const cubeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xe0e0e0,
|
||||||
|
roughness: 0.6,
|
||||||
|
metalness: 0.1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
roughness: 0.8,
|
||||||
|
metalness: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let placed = 0;
|
||||||
|
const visited = new Set<string>();
|
||||||
|
|
||||||
|
let x = 0;
|
||||||
|
let z = 0;
|
||||||
|
|
||||||
|
let dx = 1;
|
||||||
|
let dz = 0;
|
||||||
|
|
||||||
|
let segmentLength = 1;
|
||||||
|
let segmentPassed = 0;
|
||||||
|
let stepsTaken = 0;
|
||||||
|
let turnCounter = 0;
|
||||||
|
|
||||||
|
while (placed < count) {
|
||||||
|
const key = `${x},${z}`;
|
||||||
|
if (!visited.has(key)) {
|
||||||
|
if ((x + z) % 2 === 0) {
|
||||||
|
// Place base
|
||||||
|
const base = new THREE.Mesh(baseGeometry, baseMaterial);
|
||||||
|
base.position.set(x * gap, 0, z * gap);
|
||||||
|
base.receiveShadow = true;
|
||||||
|
base.castShadow = true;
|
||||||
|
scene.add(base);
|
||||||
|
|
||||||
|
// Place cube
|
||||||
|
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
|
||||||
|
if (selected && +selected === placed) {
|
||||||
|
console.log("Selected", placed);
|
||||||
|
cube.material = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x99e0ff,
|
||||||
|
roughness: 0.6,
|
||||||
|
metalness: 0.1,
|
||||||
|
});
|
||||||
|
base.material = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x99e0ff,
|
||||||
|
roughness: 0.6,
|
||||||
|
metalness: 0.1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Store
|
||||||
|
cube.userData = { id: placed };
|
||||||
|
|
||||||
|
cube.position.set(x * gap, 0.55, z * gap);
|
||||||
|
cube.castShadow = true;
|
||||||
|
scene.add(cube);
|
||||||
|
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
visited.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
x += dx;
|
||||||
|
z += dz;
|
||||||
|
segmentPassed++;
|
||||||
|
stepsTaken++;
|
||||||
|
|
||||||
|
if (segmentPassed === segmentLength) {
|
||||||
|
segmentPassed = 0;
|
||||||
|
|
||||||
|
// Turn right: [1,0] → [0,1] → [-1,0] → [0,-1]
|
||||||
|
const temp = dx;
|
||||||
|
dx = -dz;
|
||||||
|
dz = temp;
|
||||||
|
|
||||||
|
turnCounter++;
|
||||||
|
if (turnCounter % 2 === 0) {
|
||||||
|
segmentLength++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail-safe to prevent infinite loops
|
||||||
|
if (stepsTaken > count * 20) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up geometry
|
||||||
|
cubeGeometry.dispose();
|
||||||
|
baseGeometry.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewProps {
|
||||||
|
count: number;
|
||||||
|
onCubeClick: (id: number) => void;
|
||||||
|
selected?: string;
|
||||||
|
}
|
||||||
|
const View = (props: ViewProps) => {
|
||||||
|
let container: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
|
||||||
|
const camera = new THREE.PerspectiveCamera(
|
||||||
|
75,
|
||||||
|
container!.clientWidth / container!.clientHeight,
|
||||||
|
0.1,
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transparent renderer
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
|
renderer.setSize(container!.clientWidth, container!.clientHeight);
|
||||||
|
renderer.setClearColor(0x000000, 0); // Transparent background
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
container!.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
// Cube (casts shadow)
|
||||||
|
const cubeGeometry = new THREE.BoxGeometry();
|
||||||
|
const cubeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xb0c0c2,
|
||||||
|
roughness: 0.4,
|
||||||
|
metalness: 0.1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
|
||||||
|
cube.castShadow = true;
|
||||||
|
cube.position.y = 1;
|
||||||
|
// scene.add(cube);
|
||||||
|
addCubesSpiral({
|
||||||
|
scene,
|
||||||
|
count: props.count,
|
||||||
|
gap: 1.5,
|
||||||
|
selected: props.selected,
|
||||||
|
});
|
||||||
|
|
||||||
|
const factor = Math.log10(props.count) / 10 + 1;
|
||||||
|
camera.position.set(5 * factor, 6 * factor, 5 * factor); // from above and to the side
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
|
// Floor (receives shadow)
|
||||||
|
const floorGeometry = new THREE.PlaneGeometry(100, 100);
|
||||||
|
const floorMaterial = new THREE.ShadowMaterial({ opacity: 0.1 });
|
||||||
|
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
||||||
|
floor.rotation.x = -Math.PI / 2;
|
||||||
|
floor.position.y = -0.1;
|
||||||
|
floor.receiveShadow = true;
|
||||||
|
scene.add(floor);
|
||||||
|
|
||||||
|
// Light (casts shadow)
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||||
|
directionalLight.position.set(-20, 30, 20); // above & behind the cube
|
||||||
|
directionalLight.castShadow = true;
|
||||||
|
directionalLight.shadow.mapSize.width = 2048; // higher res = smoother shadow
|
||||||
|
directionalLight.shadow.mapSize.height = 2048;
|
||||||
|
directionalLight.shadow.radius = 6;
|
||||||
|
// directionalLight.shadow.radius
|
||||||
|
|
||||||
|
scene.add(directionalLight);
|
||||||
|
// Optional ambient light for slight scene illumination
|
||||||
|
scene.add(new THREE.AmbientLight(0xffffff, 0.2));
|
||||||
|
|
||||||
|
// Animate
|
||||||
|
// const animate = () => {
|
||||||
|
// animationId = requestAnimationFrame(animate);
|
||||||
|
// };
|
||||||
|
|
||||||
|
const raycaster = new THREE.Raycaster();
|
||||||
|
const mouse = new THREE.Vector2();
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
const intersects = raycaster.intersectObjects(scene.children, true);
|
||||||
|
const cube = intersects.find((i) => i.object.userData?.id !== undefined);
|
||||||
|
|
||||||
|
if (cube) {
|
||||||
|
props.onCubeClick(cube.object.userData.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener("click", handleClick);
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
// let animationId = requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
// cancelAnimationFrame(animationId);
|
||||||
|
renderer.dispose();
|
||||||
|
cubeGeometry.dispose();
|
||||||
|
cubeMaterial.dispose();
|
||||||
|
floorGeometry.dispose();
|
||||||
|
floorMaterial.dispose();
|
||||||
|
container?.removeChild(renderer.domElement);
|
||||||
|
renderer.domElement.removeEventListener("click", handleClick);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={container}
|
||||||
|
style={{ width: "100%", height: "100%", overflow: "hidden" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThreePlayground = () => {
|
||||||
|
const [count, setCount] = createSignal(1);
|
||||||
|
const [selected, setSelected] = createSignal<string>("");
|
||||||
|
|
||||||
|
const onCubeClick = (id: number) => {
|
||||||
|
console.log(`Cube ${id} clicked`);
|
||||||
|
setSelected(`${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative size-full">
|
||||||
|
<Show when={selected() || !selected()} keyed>
|
||||||
|
<Show when={count()} keyed>
|
||||||
|
{(c) => (
|
||||||
|
<View count={c} onCubeClick={onCubeClick} selected={selected()} />
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
<div class="absolute bottom-4 right-0 z-10 flex w-full items-center justify-center">
|
||||||
|
<div class="flex w-fit items-center justify-between gap-4 rounded-xl border px-8 py-2 text-white shadow-2xl bg-inv-1 border-inv-1">
|
||||||
|
<Button startIcon={<Icon icon="Edit" />}></Button>
|
||||||
|
<Button startIcon={<Icon icon="Grid" />}></Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<Icon icon="Plus" />}
|
||||||
|
onClick={() => {
|
||||||
|
setCount((c) => c + 1);
|
||||||
|
}}
|
||||||
|
></Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<Icon icon="Trash" />}
|
||||||
|
onClick={() => {
|
||||||
|
setCount((c) => c - 1);
|
||||||
|
}}
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user