UNPKG

threed-garden

Version:

ThreeD Garden: WebGL 3D Environment Interface for Next.JS React TypeScript Three.JS React-Three Physics, 2D Paper.JS; APIs: Apollo GraphQL, WordPress; CSS: Tailwind, Radix-UI; Libraries: FarmBot 3D; AI: OpenAI, DeepSeek

329 lines (290 loc) 11.1 kB
import * as THREE from 'three'; import WebGL from 'three/addons/capabilities/WebGL.js'; import ThreeMeshUI from 'three-mesh-ui'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // Set up and attach to DOM const controlsPopup = document.getElementById( 'controls-popup' ); const scene = new THREE.Scene(); scene.background = new THREE.Color(0xf4f4f4); const camera = new THREE.PerspectiveCamera( 40, controlsPopup.clientWidth / controlsPopup.clientHeight, 0.1, 1000 ); camera.position.set( 0, -100, 180 ); camera.lookAt( 0, 0, 0 ); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize( controlsPopup.clientWidth, controlsPopup.clientHeight ) renderer.setPixelRatio(window.devicePixelRatio); controlsPopup.appendChild( renderer.domElement ); // Electronics box const loader = new GLTFLoader(); loader.load( '/electronics-box-optimized.gltf', function ( gltf ) { const electronicsBox = gltf.scene; electronicsBox.scale.set( 1000, 1000, 1000 ); electronicsBox.rotation.z = Math.PI / 2; electronicsBox.position.set( 0, 50, -130 ); scene.add(electronicsBox); } ); // Button bases loader.load( '/push-button.gltf', function ( gltf ) { const button1Base = gltf.scene; button1Base.scale.set( 1000, 1000, 1000 ); button1Base.rotation.z = Math.PI / 2; // Button 1 (E-Stop) button1Base.position.set( 60, 20, 0 ); scene.add(button1Base); // Button 2 (Unlock) const button2Base = button1Base.clone(); button2Base.position.set(30, 20, 0); scene.add(button2Base); // Button 3 const button3Base = button1Base.clone(); button3Base.position.set(0, 20, 0); scene.add(button3Base); // Button 4 const button4Base = button1Base.clone(); button4Base.position.set(-30, 20, 0); scene.add(button4Base); // Button 5 const button5Base = button1Base.clone(); button5Base.position.set(-60, 20, 0); scene.add(button5Base); }); // Buttons // Button 1 (E-Stop) // Center const button1CenterGeometry = new THREE.CylinderGeometry( 6.75, 6.75, 1 ); const button1CenterMaterial = new THREE.MeshStandardMaterial( { color: 0xd4d4d4 } ); let button1Center; button1Center = new THREE.Mesh( button1CenterGeometry, button1CenterMaterial ); button1Center.position.set(60, 20, 2.1); button1Center.rotateX( Math.PI / 2 ); scene.add(button1Center); // Color const button1ColorGeometry = new THREE.CylinderGeometry( 9, 9, 1 ); const button1ColorMaterial = new THREE.MeshStandardMaterial( { color: 0xef6666 } ); let button1Color; button1Color = new THREE.Mesh( button1ColorGeometry, button1ColorMaterial ); button1Color.position.set(60, 20, 2); button1Color.rotateX( Math.PI / 2 ); // Group var button1 = new THREE.Group(); button1.add(button1Center); button1.add(button1Color); // Label const button1Label = new ThreeMeshUI.Block({ width: 20, height: 8, padding: 1, borderRadius: 2, justifyContent: 'center', textAlign: 'center', fontFamily: '/Roboto-msdf.json', fontTexture: '/Roboto-msdf.png', fontColor: new THREE.Color(0xf4f4f4), backgroundOpacity: 0.75, }); const button1LabelText = new ThreeMeshUI.Text({ content: 'E-Stop', fontSize: 4 }); button1Label.add( button1LabelText ); button1Label.position.set( 60, 35, 10 ); button1Label.rotateX( Math.PI / 6 ); scene.add(button1Label); // Button 2 (Unlock) // Center const button2CenterGeometry = new THREE.CylinderGeometry( 6.75, 6.75, 1 ); const button2CenterMaterial = new THREE.MeshStandardMaterial( { color: 0xd4d4d4 } ); let button2Center; button2Center = new THREE.Mesh( button2CenterGeometry, button2CenterMaterial ); button2Center.position.set(30, 20, 2.1); button2Center.rotateX( Math.PI / 2 ); scene.add(button2Center); // Color let offColor = new THREE.Color(0xffdd66); let onColor = new THREE.Color(0xffff00); const button2ColorGeometry = new THREE.CylinderGeometry( 9, 9, 1 ); const button2ColorMaterial = new THREE.MeshStandardMaterial( { color: offColor } ); let button2Color; button2Color = new THREE.Mesh( button2ColorGeometry, button2ColorMaterial ); button2Color.position.set(30, 20, 2); button2Color.rotateX( Math.PI / 2 ); // Blinking let isOffColor = true; setInterval(function() { if (isOffColor) { button2Color.material.color.set(onColor); } else { button2Color.material.color.set(offColor); } isOffColor = !isOffColor; }, 1000); // Group var button2 = new THREE.Group(); button2.add(button2Center); button2.add(button2Color); // Label const button2Label = new ThreeMeshUI.Block({ width: 20, height: 8, padding: 1, borderRadius: 2, justifyContent: 'center', textAlign: 'center', fontFamily: '/Roboto-msdf.json', fontTexture: '/Roboto-msdf.png', fontColor: new THREE.Color(0xf4f4f4), backgroundOpacity: 0.75, }); const button2LabelText = new ThreeMeshUI.Text({ content: 'Unlock', fontSize: 4 }); button2Label.add( button2LabelText ); button2Label.position.set( 30, 35, 10 ); button2Label.rotateX( Math.PI / 6 ); scene.add(button2Label); // All buttons var allButtons = new THREE.Group(); allButtons.add(button1); allButtons.add(button2); scene.add(allButtons); // LED bases loader.load( '/led-indicator.gltf', function ( gltf ) { const led1Base = gltf.scene; led1Base.scale.set( 1000, 1000, 1000 ); led1Base.rotation.z = Math.PI / 2; // LED 1 (Sync) led1Base.position.set( 45, 0, -1 ); scene.add(led1Base); // Color const led1ColorGeometry = new THREE.CylinderGeometry( 5, 5, 1 ); const led1ColorMaterial = new THREE.MeshStandardMaterial( { color: 0x66aa44 } ); let led1Color; led1Color = new THREE.Mesh( led1ColorGeometry, led1ColorMaterial ); led1Color.position.set(45, 0, 0); led1Color.rotateX( Math.PI / 2 ); scene.add(led1Color); // LED 2 (Connectivity) const led2Base = led1Base.clone(); led2Base.position.set(15, 0, -1); scene.add(led2Base); // Color const led2ColorGeometry = new THREE.CylinderGeometry( 5, 5, 1 ); const led2ColorMaterial = new THREE.MeshStandardMaterial( { color: 0x3377dd } ); let led2Color; led2Color = new THREE.Mesh( led2ColorGeometry, led2ColorMaterial ); led2Color.position.set(15, 0, 0); led2Color.rotateX( Math.PI / 2 ); scene.add(led2Color); // LED 3 const led3Base = led1Base.clone(); led3Base.position.set(-15, 0, -1); scene.add(led3Base); // Color const led3ColorGeometry = new THREE.CylinderGeometry( 5, 5, 1 ); const led3ColorMaterial = new THREE.MeshStandardMaterial( { color: 0xf4f4f4 } ); let led3Color; led3Color = new THREE.Mesh( led3ColorGeometry, led3ColorMaterial ); led3Color.position.set(-15, 0, 0); led3Color.rotateX( Math.PI / 2 ); scene.add(led3Color); // LED 4 const led4Base = led1Base.clone(); led4Base.position.set(-45, 0, -1); scene.add(led4Base); // Color const led4ColorGeometry = new THREE.CylinderGeometry( 5, 5, 1 ); const led4ColorMaterial = new THREE.MeshStandardMaterial( { color: 0xf4f4f4 } ); let led4Color; led4Color = new THREE.Mesh( led4ColorGeometry, led4ColorMaterial ); led4Color.position.set(-45, 0, 0); led4Color.rotateX( Math.PI / 2 ); scene.add(led4Color); }); // Lighting var pointLight = new THREE.PointLight(0xFFFFFF, 1); pointLight.position.set(0, 0, 200); scene.add(pointLight); var ambientLight = new THREE.AmbientLight(0x404040, 0.5); scene.add(ambientLight); // Fog to limit the view distance scene.fog = new THREE.Fog(0xf4f4f4, 225, 325); // Mouse raycasting const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); let rect; function updateRect() { rect = renderer.domElement.getBoundingClientRect(); } window.addEventListener('resize', updateRect); updateRect(); // Button hover effects window.addEventListener( 'mousemove', onMouseMove, false ); function onMouseMove( event ) { // calculate mouse position in normalized device coordinates // (-1 to +1) for both components mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // update the picking ray with the camera and mouse position raycaster.setFromCamera( mouse, camera ); // calculate objects intersecting the picking ray const intersects = raycaster.intersectObjects( allButtons.children ); if ( intersects.length > 0 ) { // Change the cursor to a pointer renderer.domElement.style.cursor = 'pointer'; if ( intersects[0].object === button1Center || intersects[0].object === button1Color ) { // change the color of button1Center button1Center.material.color.set( 0xefefef ); } else if ( intersects[0].object === button2Center || intersects[0].object === button2Color ) { // change the color of button2Center button2Center.material.color.set( 0xefefef ); } } else { // restore original colors button1Center.material.color.set( 0xdddddd ); button2Center.material.color.set( 0xdddddd ); // Change the cursor back to default renderer.domElement.style.cursor = 'default'; } } // Button press effects window.addEventListener( 'mousedown', onMouseDown, false ); function onMouseDown( event ) { // calculate mouse position in normalized device coordinates // (-1 to +1) for both components mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // update the picking ray with the camera and mouse position raycaster.setFromCamera( mouse, camera ); // calculate objects intersecting the picking ray const intersects = raycaster.intersectObjects( allButtons.children ); if ( intersects.length > 0 ) { if ( intersects[0].object === button1Center || intersects[0].object === button1Color ) { // press button1 button1.position.z -= 3; console.log('E-Stop pressed'); } else if ( intersects[0].object === button2Center || intersects[0].object === button2Color ) { // press button2 button2.position.z -= 3; console.log('Unlock pressed'); } } } window.addEventListener( 'mouseup', onMouseUp, false ); function onMouseUp( event ) { button1.position.z = 0; button2.position.z = 0; } // Render loop function animate() { requestAnimationFrame( animate ); ThreeMeshUI.update(); renderer.render( scene, camera ); } // Compatibility check if ( WebGL.isWebGLAvailable() ) { animate(); } else { const warning = WebGL.getWebGLErrorMessage(); document.getElementById( 'container' ).appendChild( warning ); }