Răsfoiți Sursa

Examples: More clean up. (#21796)

Michael Herzog 4 ani în urmă
părinte
comite
18c7a6a731

+ 500 - 408
examples/webxr_vr_handinput_pointerclick.html

@@ -2,418 +2,510 @@
 <html lang="en">
 <html lang="en">
 
 
 <head>
 <head>
-  <title>three.js webxr hands - point and click</title>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-  <link type="text/css" rel="stylesheet" href="main.css">
+	<title>three.js vr - handinput - point and click</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+	<link type="text/css" rel="stylesheet" href="main.css">
 </head>
 </head>
 
 
 <body>
 <body>
 
 
-  <div id="info">
-    <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>  vr - handinput - point and click<br />
-    (Oculus Browser 15.1+)
-  </div>
-
-  <script type="module">
-
-    import * as THREE from '../build/three.module.js';
-    import { VRButton } from './jsm/webxr/VRButton.js';
-    import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
-    import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
-    import { OculusHandPointerModel } from './jsm/webxr/OculusHandPointerModel.js';
-    import { createText } from './jsm/webxr/Text2D.js';
-
-    import { World, System, Component, TagComponent, Types } from './jsm/libs/ecsy.module.js';
-
-    class Object3D extends Component { }
-
-    Object3D.schema = {
-      object: { type: Types.Ref }
-    };
-
-    class Button extends Component { }
-
-    Button.schema = {
-      // button states: [none, hovered, pressed]
-      currState: { type: Types.String, default: 'none' },
-      prevState: { type: Types.String, default: 'none' },
-      action: { type: Types.Ref, default: () => { } }
-    }
-
-    class ButtonSystem extends System {
-      execute(delta, time) {
-        this.queries.buttons.results.forEach(entity => {
-          var button = entity.getMutableComponent(Button);
-          var buttonMesh = entity.getComponent(Object3D).object;
-          if (button.currState == 'none') {
-            buttonMesh.scale.set(1, 1, 1);
-          } else {
-            buttonMesh.scale.set(1.1, 1.1, 1.1);
-          }
-          if (button.currState == "pressed" && button.prevState != "pressed") {
-            button.action();
-          }
-
-          // preserve prevState, clear currState
-          // HandRaySystem will update currState
-          button.prevState = button.currState;
-          button.currState = 'none';
-        });
-      }
-    }
-
-    ButtonSystem.queries = {
-      buttons: {
-        components: [Button]
-      }
-    }
-
-    class Intersectable extends TagComponent { }
-
-    class HandRaySystem extends System {
-      init(attributes) {
-        this.handPointers = attributes.handPointers;
-      }
-
-      execute(delta, time) {
-        this.handPointers.forEach(hp => {
-          var distance = null;
-          var intersectingEntity = null;
-          this.queries.intersectable.results.forEach(entity => {
-            var object = entity.getComponent(Object3D).object;
-            let intersections = hp.intersectObject(object);
-            if (intersections && intersections.length > 0) {
-              if (distance == null || intersections[0].distance < distance) {
-                distance = intersections[0].distance;
-                intersectingEntity = entity;
-              }
-            }
-          });
-          if (distance) {
-            hp.setCursor(distance);
-            if (intersectingEntity.hasComponent(Button)) {
-              let button = intersectingEntity.getMutableComponent(Button);
-              if (hp.isPinched()) {
-                button.currState = 'pressed';
-              } else if (button.currState != 'pressed') {
-                button.currState = 'hovered';
-              }
-            }
-          } else {
-            hp.setCursor(1.5);
-          }
-        });
-      }
-    }
-
-    HandRaySystem.queries = {
-      intersectable: {
-        components: [Intersectable]
-      }
-    };
-
-    class Rotating extends TagComponent { }
-
-    class RotatingSystem extends System {
-      execute(delta, time) {
-        this.queries.rotatingObjects.results.forEach(entity => {
-          var object = entity.getComponent(Object3D).object;
-          object.rotation.x += 0.4 * delta;
-          object.rotation.y += 0.4 * delta;
-        });
-      }
-    }
-
-    RotatingSystem.queries = {
-      rotatingObjects: {
-        components: [Rotating]
-      }
-    }
-
-    class HandsInstructionText extends TagComponent { }
-
-    class InstructionSystem extends System {
-      init(attributes) {
-        this.controllers = attributes.controllers;
-      }
-
-      execute(delta, time) {
-        let visible = false;
-        this.controllers.forEach(controller => {
-          if (controller.visible) {
-            visible = true;
-          }
-        })
-        this.queries.instructionTexts.results.forEach(entity => {
-          var object = entity.getComponent(Object3D).object;
-          object.visible = visible;
-        });
-      }
-    }
-
-    InstructionSystem.queries = {
-      instructionTexts: {
-        components: [HandsInstructionText]
-      }
-    }
-
-    class OffsetFromCamera extends Component { }
-
-    OffsetFromCamera.schema = {
-      x: { type: Types.Number, default: 0 },
-      y: { type: Types.Number, default: 0 },
-      z: { type: Types.Number, default: 0 },
-    }
-
-    class NeedCalibration extends TagComponent { }
-
-    class CalibrationSystem extends System {
-      init(attributes) {
-        this.camera = attributes.camera;
-        this.renderer = attributes.renderer;
-      }
-
-      execute(delta, time) {
-        this.queries.needCalibration.results.forEach(entity => {
-          if (this.renderer.xr.getSession()) {
-            let offset = entity.getComponent(OffsetFromCamera);
-            let object = entity.getComponent(Object3D).object;
-            let xrCamera = renderer.xr.getCamera(this.camera);
-            object.position.x = xrCamera.position.x + offset.x;
-            object.position.y = xrCamera.position.y + offset.y;
-            object.position.z = xrCamera.position.z + offset.z;
-            entity.removeComponent(NeedCalibration);
-          }
-        });
-      }
-    }
-
-    CalibrationSystem.queries = {
-      needCalibration: {
-        components: [NeedCalibration]
-      }
-    }
-
-    let world = new World();
-    var clock = new THREE.Clock();
-    let camera, scene, renderer;
-
-    init();
-    animate();
-
-    function makeButtonMesh(x, y, z, color) {
-      const geometry = new THREE.BoxGeometry(x, y, z);
-      const material = new THREE.MeshPhongMaterial({ color: color });
-      const buttonMesh = new THREE.Mesh(geometry, material);
-      return buttonMesh;
-    }
-
-
-    function init() {
-
-      let container = document.createElement('div');
-      document.body.appendChild(container);
-
-      scene = new THREE.Scene();
-      scene.background = new THREE.Color(0x444444);
-
-      camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
-      camera.position.set(0, 1.2, 0.3);
-
-      scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
-
-      const light = new THREE.DirectionalLight(0xffffff);
-      light.position.set(0, 6, 0);
-      light.castShadow = true;
-      light.shadow.camera.top = 2;
-      light.shadow.camera.bottom = - 2;
-      light.shadow.camera.right = 2;
-      light.shadow.camera.left = - 2;
-      light.shadow.mapSize.set(4096, 4096);
-      scene.add(light);
-
-      renderer = new THREE.WebGLRenderer({ antialias: true });
-      renderer.setPixelRatio(window.devicePixelRatio);
-      renderer.setSize(window.innerWidth, window.innerHeight);
-      renderer.outputEncoding = THREE.sRGBEncoding;
-      renderer.shadowMap.enabled = true;
-      renderer.xr.enabled = true;
-
-      container.appendChild(renderer.domElement);
-
-      document.body.appendChild(VRButton.createButton(renderer));
-
-      // controllers
-      let controller1 = renderer.xr.getController(0);
-      scene.add(controller1);
-
-      let controller2 = renderer.xr.getController(1);
-      scene.add(controller2);
-
-      const controllerModelFactory = new XRControllerModelFactory();
-
-      // Hand 1
-      let controllerGrip1 = renderer.xr.getControllerGrip(0);
-      controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
-      scene.add(controllerGrip1);
-
-      let hand1 = renderer.xr.getHand(0);
-      hand1.add(new OculusHandModel(hand1));
-      let handPointer1 = new OculusHandPointerModel(hand1, controller1);
-      hand1.add(handPointer1);
-
-      scene.add(hand1);
-
-      // Hand 2
-      let controllerGrip2 = renderer.xr.getControllerGrip(1);
-      controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
-      scene.add(controllerGrip2);
-
-      let hand2 = renderer.xr.getHand(1);
-      hand2.add(new OculusHandModel(hand2));
-      let handPointer2 = new OculusHandPointerModel(hand2, controller2);
-      hand2.add(handPointer2);
-      scene.add(hand2);
-
-      // setup objects in scene and entities
-      const floorGeometry = new THREE.PlaneGeometry(4, 4);
-      const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 });
-      const floor = new THREE.Mesh(floorGeometry, floorMaterial);
-      floor.rotation.x = - Math.PI / 2;
-      scene.add(floor);
-
-      const menuGeometry = new THREE.PlaneGeometry(0.24, 0.5);
-      const menuMaterial = new THREE.MeshPhongMaterial({
-        opacity: 0,
-        transparent: true,
-      });
-      let menuMesh = new THREE.Mesh(menuGeometry, menuMaterial);
-      menuMesh.position.set(0.4, 1, -1);
-      menuMesh.rotation.y = - Math.PI / 12;
-      scene.add(menuMesh);
-
-      let orangeButton = makeButtonMesh(0.2, 0.1, 0.01, 0xffd3b5);
-      orangeButton.position.set(0, 0.18, 0);
-      menuMesh.add(orangeButton);
-
-      let pinkButton = makeButtonMesh(0.2, 0.1, 0.01, 0xe84a5f);
-      pinkButton.position.set(0, 0.06, 0);
-      menuMesh.add(pinkButton);
-
-      let resetButton = makeButtonMesh(0.2, 0.1, 0.01, 0x355c7d);
-      let resetButtonText = createText("reset", 0.06);
-      resetButton.add(resetButtonText);
-      resetButtonText.position.set(0, 0, 0.0051);
-      resetButton.position.set(0, -0.06, 0);
-      menuMesh.add(resetButton);
-
-      let exitButton = makeButtonMesh(0.2, 0.1, 0.01, 0xff0000);
-      let exitButtonText = createText("exit", 0.06);
-      exitButton.add(exitButtonText);
-      exitButtonText.position.set(0, 0, 0.0051);
-      exitButton.position.set(0, -0.18, 0);
-      menuMesh.add(exitButton);
-
-      let tkGeometry = new THREE.TorusKnotGeometry(0.5, 0.2, 200, 32);
-      let tkMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
-      tkMaterial.metalness = 0.8;
-      let torusKnot = new THREE.Mesh(tkGeometry, tkMaterial);
-      torusKnot.position.set(0, 1, -5);
-      scene.add(torusKnot);
-
-      let instructionText = createText("This is a WebXR Hands demo, please explore with hands.", 0.04);
-      instructionText.position.set(0, 1.6, -0.6);
-      scene.add(instructionText);
-
-      let exitText = createText("Exiting session...", 0.04);
-      exitText.position.set(0, 1.5, -0.6);
-      exitText.visible = false;
-      scene.add(exitText);
-
-      world
-        .registerComponent(Object3D)
-        .registerComponent(Button)
-        .registerComponent(Intersectable)
-        .registerComponent(Rotating)
-        .registerComponent(HandsInstructionText)
-        .registerComponent(OffsetFromCamera)
-        .registerComponent(NeedCalibration);
-
-      world
-        .registerSystem(RotatingSystem)
-        .registerSystem(InstructionSystem, { controllers: [controllerGrip1, controllerGrip2] })
-        .registerSystem(CalibrationSystem, { renderer: renderer, camera: camera })
-        .registerSystem(ButtonSystem)
-        .registerSystem(HandRaySystem, { handPointers: [handPointer1, handPointer2] });
-
-      var menuEntity = world.createEntity();
-      menuEntity.addComponent(Intersectable);
-      menuEntity.addComponent(OffsetFromCamera, { x: 0.4, y: 0, z: -1 });
-      menuEntity.addComponent(NeedCalibration);
-      menuEntity.addComponent(Object3D, { object: menuMesh });
-
-      var obEntity = world.createEntity();
-      obEntity.addComponent(Intersectable);
-      obEntity.addComponent(Object3D, { object: orangeButton });
-      let obAction = function () { torusKnot.material.color.setHex(0xffd3b5); };
-      obEntity.addComponent(Button, { action: obAction });
-
-      var pbEntity = world.createEntity();
-      pbEntity.addComponent(Intersectable);
-      pbEntity.addComponent(Object3D, { object: pinkButton });
-      let pbAction = function () { torusKnot.material.color.setHex(0xe84a5f); };
-      pbEntity.addComponent(Button, { action: pbAction });
-
-      var rbEntity = world.createEntity();
-      rbEntity.addComponent(Intersectable);
-      rbEntity.addComponent(Object3D, { object: resetButton });
-      let rbAction = function () { torusKnot.material.color.setHex(0xffffff); };
-      rbEntity.addComponent(Button, { action: rbAction });
-
-      var ebEntity = world.createEntity();
-      ebEntity.addComponent(Intersectable);
-      ebEntity.addComponent(Object3D, { object: exitButton });
-      let ebAction = function () {
-        exitText.visible = true;
-        setTimeout(function () { exitText.visible = false; renderer.xr.getSession().end(); }, 2000);
-      };
-      ebEntity.addComponent(Button, { action: ebAction });
-
-      var tkEntity = world.createEntity();
-      tkEntity.addComponent(Rotating);
-      tkEntity.addComponent(Object3D, { object: torusKnot });
-
-      var itEntity = world.createEntity();
-      itEntity.addComponent(HandsInstructionText);
-      itEntity.addComponent(Object3D, { object: instructionText });
-
-      window.addEventListener('resize', onWindowResize);
-    }
-
-    function onWindowResize() {
-
-      camera.aspect = window.innerWidth / window.innerHeight;
-      camera.updateProjectionMatrix();
-
-      renderer.setSize(window.innerWidth, window.innerHeight);
-
-    }
-
-    function animate() {
-
-      renderer.setAnimationLoop(render);
-
-    }
-
-    function render() {
-      var delta = clock.getDelta();
-      var elapsedTime = clock.elapsedTime;
-      world.execute(delta, elapsedTime);
-      renderer.render(scene, camera);
-    }
-
-  </script>
+	<div id="info">
+		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>	vr - handinput - point and click<br />
+		(Oculus Browser 15.1+)
+	</div>
+
+	<script type="module">
+
+		import * as THREE from '../build/three.module.js';
+		import { VRButton } from './jsm/webxr/VRButton.js';
+		import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
+		import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
+		import { OculusHandPointerModel } from './jsm/webxr/OculusHandPointerModel.js';
+		import { createText } from './jsm/webxr/Text2D.js';
+
+		import { World, System, Component, TagComponent, Types } from './jsm/libs/ecsy.module.js';
+
+		class Object3D extends Component { }
+
+		Object3D.schema = {
+			object: { type: Types.Ref }
+		};
+
+		class Button extends Component { }
+
+		Button.schema = {
+			// button states: [none, hovered, pressed]
+			currState: { type: Types.String, default: 'none' },
+			prevState: { type: Types.String, default: 'none' },
+			action: { type: Types.Ref, default: () => { } }
+		};
+
+		class ButtonSystem extends System {
+
+			execute( /* delta, time */ ) {
+
+				this.queries.buttons.results.forEach( entity => {
+
+					const button = entity.getMutableComponent( Button );
+					const buttonMesh = entity.getComponent( Object3D ).object;
+					if ( button.currState == 'none' ) {
+
+						buttonMesh.scale.set( 1, 1, 1 );
+
+					} else {
+
+						buttonMesh.scale.set( 1.1, 1.1, 1.1 );
+
+					}
+
+					if ( button.currState == 'pressed' && button.prevState != 'pressed' ) {
+
+						button.action();
+
+					}
+
+					// preserve prevState, clear currState
+					// HandRaySystem will update currState
+					button.prevState = button.currState;
+					button.currState = 'none';
+
+				} );
+
+			}
+
+		}
+
+		ButtonSystem.queries = {
+			buttons: {
+				components: [ Button ]
+			}
+		};
+
+		class Intersectable extends TagComponent { }
+
+		class HandRaySystem extends System {
+
+			init( attributes ) {
+
+				this.handPointers = attributes.handPointers;
+
+			}
+
+			execute( /* delta, time */ ) {
+
+				this.handPointers.forEach( hp => {
+
+					let distance = null;
+					let intersectingEntity = null;
+					this.queries.intersectable.results.forEach( entity => {
+
+						const object = entity.getComponent( Object3D ).object;
+						const intersections = hp.intersectObject( object );
+						if ( intersections && intersections.length > 0 ) {
+
+							if ( distance == null || intersections[ 0 ].distance < distance ) {
+
+								distance = intersections[ 0 ].distance;
+								intersectingEntity = entity;
+
+							}
+
+						}
+
+					} );
+					if ( distance ) {
+
+						hp.setCursor( distance );
+						if ( intersectingEntity.hasComponent( Button ) ) {
+
+							const button = intersectingEntity.getMutableComponent( Button );
+							if ( hp.isPinched() ) {
+
+								button.currState = 'pressed';
+
+							} else if ( button.currState != 'pressed' ) {
+
+								button.currState = 'hovered';
+
+							}
+
+						}
+
+					} else {
+
+						hp.setCursor( 1.5 );
+
+					}
+
+				} );
+
+			}
+
+		}
+
+		HandRaySystem.queries = {
+			intersectable: {
+				components: [ Intersectable ]
+			}
+		};
+
+		class Rotating extends TagComponent { }
+
+		class RotatingSystem extends System {
+
+			execute( delta/*, time*/ ) {
+
+				this.queries.rotatingObjects.results.forEach( entity => {
+
+					const object = entity.getComponent( Object3D ).object;
+					object.rotation.x += 0.4 * delta;
+					object.rotation.y += 0.4 * delta;
+
+				} );
+
+			}
+
+		}
+
+		RotatingSystem.queries = {
+			rotatingObjects: {
+				components: [ Rotating ]
+			}
+		};
+
+		class HandsInstructionText extends TagComponent { }
+
+		class InstructionSystem extends System {
+
+			init( attributes ) {
+
+				this.controllers = attributes.controllers;
+
+			}
+
+			execute( /* delta, time */ ) {
+
+				let visible = false;
+				this.controllers.forEach( controller => {
+
+					if ( controller.visible ) {
+
+						visible = true;
+
+					}
+
+				} );
+
+				this.queries.instructionTexts.results.forEach( entity => {
+
+					const object = entity.getComponent( Object3D ).object;
+					object.visible = visible;
+
+				} );
+
+			}
+
+		}
+
+		InstructionSystem.queries = {
+			instructionTexts: {
+				components: [ HandsInstructionText ]
+			}
+		};
+
+		class OffsetFromCamera extends Component { }
+
+		OffsetFromCamera.schema = {
+			x: { type: Types.Number, default: 0 },
+			y: { type: Types.Number, default: 0 },
+			z: { type: Types.Number, default: 0 },
+		};
+
+		class NeedCalibration extends TagComponent { }
+
+		class CalibrationSystem extends System {
+
+			init( attributes ) {
+
+				this.camera = attributes.camera;
+				this.renderer = attributes.renderer;
+
+			}
+
+			execute( /* delta, time */ ) {
+
+				this.queries.needCalibration.results.forEach( entity => {
+
+					if ( this.renderer.xr.getSession() ) {
+
+						const offset = entity.getComponent( OffsetFromCamera );
+						const object = entity.getComponent( Object3D ).object;
+						const xrCamera = renderer.xr.getCamera( this.camera );
+						object.position.x = xrCamera.position.x + offset.x;
+						object.position.y = xrCamera.position.y + offset.y;
+						object.position.z = xrCamera.position.z + offset.z;
+						entity.removeComponent( NeedCalibration );
+
+					}
+
+				} );
+
+			}
+
+		}
+
+		CalibrationSystem.queries = {
+			needCalibration: {
+				components: [ NeedCalibration ]
+			}
+		};
+
+		const world = new World();
+		const clock = new THREE.Clock();
+		let camera, scene, renderer;
+
+		init();
+		animate();
+
+		function makeButtonMesh( x, y, z, color ) {
+
+			const geometry = new THREE.BoxGeometry( x, y, z );
+			const material = new THREE.MeshPhongMaterial( { color: color } );
+			const buttonMesh = new THREE.Mesh( geometry, material );
+			return buttonMesh;
+
+		}
+
+		function init() {
+
+			const container = document.createElement( 'div' );
+			document.body.appendChild( container );
+
+			scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0x444444 );
+
+			camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 10 );
+			camera.position.set( 0, 1.2, 0.3 );
+
+			scene.add( new THREE.HemisphereLight( 0x808080, 0x606060 ) );
+
+			const light = new THREE.DirectionalLight( 0xffffff );
+			light.position.set( 0, 6, 0 );
+			light.castShadow = true;
+			light.shadow.camera.top = 2;
+			light.shadow.camera.bottom = - 2;
+			light.shadow.camera.right = 2;
+			light.shadow.camera.left = - 2;
+			light.shadow.mapSize.set( 4096, 4096 );
+			scene.add( light );
+
+			renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			renderer.outputEncoding = THREE.sRGBEncoding;
+			renderer.shadowMap.enabled = true;
+			renderer.xr.enabled = true;
+
+			container.appendChild( renderer.domElement );
+
+			document.body.appendChild( VRButton.createButton( renderer ) );
+
+			// controllers
+			const controller1 = renderer.xr.getController( 0 );
+			scene.add( controller1 );
+
+			const controller2 = renderer.xr.getController( 1 );
+			scene.add( controller2 );
+
+			const controllerModelFactory = new XRControllerModelFactory();
+
+			// Hand 1
+			const controllerGrip1 = renderer.xr.getControllerGrip( 0 );
+			controllerGrip1.add( controllerModelFactory.createControllerModel( controllerGrip1 ) );
+			scene.add( controllerGrip1 );
+
+			const hand1 = renderer.xr.getHand( 0 );
+			hand1.add( new OculusHandModel( hand1 ) );
+			const handPointer1 = new OculusHandPointerModel( hand1, controller1 );
+			hand1.add( handPointer1 );
+
+			scene.add( hand1 );
+
+			// Hand 2
+			const controllerGrip2 = renderer.xr.getControllerGrip( 1 );
+			controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
+			scene.add( controllerGrip2 );
+
+			const hand2 = renderer.xr.getHand( 1 );
+			hand2.add( new OculusHandModel( hand2 ) );
+			const handPointer2 = new OculusHandPointerModel( hand2, controller2 );
+			hand2.add( handPointer2 );
+			scene.add( hand2 );
+
+			// setup objects in scene and entities
+			const floorGeometry = new THREE.PlaneGeometry( 4, 4 );
+			const floorMaterial = new THREE.MeshPhongMaterial( { color: 0x222222 } );
+			const floor = new THREE.Mesh( floorGeometry, floorMaterial );
+			floor.rotation.x = - Math.PI / 2;
+			scene.add( floor );
+
+			const menuGeometry = new THREE.PlaneGeometry( 0.24, 0.5 );
+			const menuMaterial = new THREE.MeshPhongMaterial( {
+				opacity: 0,
+				transparent: true,
+			} );
+			const menuMesh = new THREE.Mesh( menuGeometry, menuMaterial );
+			menuMesh.position.set( 0.4, 1, - 1 );
+			menuMesh.rotation.y = - Math.PI / 12;
+			scene.add( menuMesh );
+
+			const orangeButton = makeButtonMesh( 0.2, 0.1, 0.01, 0xffd3b5 );
+			orangeButton.position.set( 0, 0.18, 0 );
+			menuMesh.add( orangeButton );
+
+			const pinkButton = makeButtonMesh( 0.2, 0.1, 0.01, 0xe84a5f );
+			pinkButton.position.set( 0, 0.06, 0 );
+			menuMesh.add( pinkButton );
+
+			const resetButton = makeButtonMesh( 0.2, 0.1, 0.01, 0x355c7d );
+			const resetButtonText = createText( 'reset', 0.06 );
+			resetButton.add( resetButtonText );
+			resetButtonText.position.set( 0, 0, 0.0051 );
+			resetButton.position.set( 0, - 0.06, 0 );
+			menuMesh.add( resetButton );
+
+			const exitButton = makeButtonMesh( 0.2, 0.1, 0.01, 0xff0000 );
+			const exitButtonText = createText( 'exit', 0.06 );
+			exitButton.add( exitButtonText );
+			exitButtonText.position.set( 0, 0, 0.0051 );
+			exitButton.position.set( 0, - 0.18, 0 );
+			menuMesh.add( exitButton );
+
+			const tkGeometry = new THREE.TorusKnotGeometry( 0.5, 0.2, 200, 32 );
+			const tkMaterial = new THREE.MeshPhongMaterial( { color: 0xffffff } );
+			tkMaterial.metalness = 0.8;
+			const torusKnot = new THREE.Mesh( tkGeometry, tkMaterial );
+			torusKnot.position.set( 0, 1, - 5 );
+			scene.add( torusKnot );
+
+			const instructionText = createText( 'This is a WebXR Hands demo, please explore with hands.', 0.04 );
+			instructionText.position.set( 0, 1.6, - 0.6 );
+			scene.add( instructionText );
+
+			const exitText = createText( 'Exiting session...', 0.04 );
+			exitText.position.set( 0, 1.5, - 0.6 );
+			exitText.visible = false;
+			scene.add( exitText );
+
+			world
+				.registerComponent( Object3D )
+				.registerComponent( Button )
+				.registerComponent( Intersectable )
+				.registerComponent( Rotating )
+				.registerComponent( HandsInstructionText )
+				.registerComponent( OffsetFromCamera )
+				.registerComponent( NeedCalibration );
+
+			world
+				.registerSystem( RotatingSystem )
+				.registerSystem( InstructionSystem, { controllers: [ controllerGrip1, controllerGrip2 ] } )
+				.registerSystem( CalibrationSystem, { renderer: renderer, camera: camera } )
+				.registerSystem( ButtonSystem )
+				.registerSystem( HandRaySystem, { handPointers: [ handPointer1, handPointer2 ] } );
+
+			const menuEntity = world.createEntity();
+			menuEntity.addComponent( Intersectable );
+			menuEntity.addComponent( OffsetFromCamera, { x: 0.4, y: 0, z: - 1 } );
+			menuEntity.addComponent( NeedCalibration );
+			menuEntity.addComponent( Object3D, { object: menuMesh } );
+
+			const obEntity = world.createEntity();
+			obEntity.addComponent( Intersectable );
+			obEntity.addComponent( Object3D, { object: orangeButton } );
+			const obAction = function () {
+
+				torusKnot.material.color.setHex( 0xffd3b5 );
+
+			};
+
+			obEntity.addComponent( Button, { action: obAction } );
+
+			const pbEntity = world.createEntity();
+			pbEntity.addComponent( Intersectable );
+			pbEntity.addComponent( Object3D, { object: pinkButton } );
+			const pbAction = function () {
+
+				torusKnot.material.color.setHex( 0xe84a5f );
+
+			};
+
+			pbEntity.addComponent( Button, { action: pbAction } );
+
+			const rbEntity = world.createEntity();
+			rbEntity.addComponent( Intersectable );
+			rbEntity.addComponent( Object3D, { object: resetButton } );
+			const rbAction = function () {
+
+				torusKnot.material.color.setHex( 0xffffff );
+
+			};
+
+			rbEntity.addComponent( Button, { action: rbAction } );
+
+			const ebEntity = world.createEntity();
+			ebEntity.addComponent( Intersectable );
+			ebEntity.addComponent( Object3D, { object: exitButton } );
+			const ebAction = function () {
+
+				exitText.visible = true;
+				setTimeout( function () {
+
+					exitText.visible = false; renderer.xr.getSession().end();
+
+				}, 2000 );
+
+			};
+
+			ebEntity.addComponent( Button, { action: ebAction } );
+
+			const tkEntity = world.createEntity();
+			tkEntity.addComponent( Rotating );
+			tkEntity.addComponent( Object3D, { object: torusKnot } );
+
+			const itEntity = world.createEntity();
+			itEntity.addComponent( HandsInstructionText );
+			itEntity.addComponent( Object3D, { object: instructionText } );
+
+			window.addEventListener( 'resize', onWindowResize );
+
+		}
+
+		function onWindowResize() {
+
+			camera.aspect = window.innerWidth / window.innerHeight;
+			camera.updateProjectionMatrix();
+
+			renderer.setSize( window.innerWidth, window.innerHeight );
+
+		}
+
+		function animate() {
+
+			renderer.setAnimationLoop( render );
+
+		}
+
+		function render() {
+
+			const delta = clock.getDelta();
+			const elapsedTime = clock.elapsedTime;
+			world.execute( delta, elapsedTime );
+			renderer.render( scene, camera );
+
+		}
+
+	</script>
 </body>
 </body>
 
 
 </html>
 </html>

+ 580 - 468
examples/webxr_vr_handinput_pointerdrag.html

@@ -2,478 +2,590 @@
 <html lang="en">
 <html lang="en">
 
 
 <head>
 <head>
-  <title>three.js webxr hands - point and drag</title>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-  <link type="text/css" rel="stylesheet" href="main.css">
+	<title>three.js ve - handinput - point and drag</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+	<link type="text/css" rel="stylesheet" href="main.css">
 </head>
 </head>
 
 
 <body>
 <body>
 
 
-  <div id="info">
-    <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> vr - handinput - point and drag<br />
-    (Oculus Browser 15.1+)
-  </div>
-
-  <script type="module">
-
-    import * as THREE from '../build/three.module.js';
-    import { VRButton } from './jsm/webxr/VRButton.js';
-    import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
-    import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
-    import { OculusHandPointerModel } from './jsm/webxr/OculusHandPointerModel.js';
-    import { createText } from './jsm/webxr/Text2D.js';
-
-    import { World, System, Component, TagComponent, Types } from './jsm/libs/ecsy.module.js';
-
-    class Object3D extends Component { }
-
-    Object3D.schema = {
-      object: { type: Types.Ref }
-    };
-
-    class Button extends Component { }
-
-    Button.schema = {
-      // button states: [none, hovered, pressed]
-      currState: { type: Types.String, default: 'none' },
-      prevState: { type: Types.String, default: 'none' },
-      action: { type: Types.Ref, default: () => { } }
-    }
-
-    class ButtonSystem extends System {
-      execute(delta, time) {
-        this.queries.buttons.results.forEach(entity => {
-          var button = entity.getMutableComponent(Button);
-          var buttonMesh = entity.getComponent(Object3D).object;
-          if (button.currState == 'none') {
-            buttonMesh.scale.set(1, 1, 1);
-          } else {
-            buttonMesh.scale.set(1.1, 1.1, 1.1);
-          }
-          if (button.currState == "pressed" && button.prevState != "pressed") {
-            button.action();
-          }
-
-          // preserve prevState, clear currState
-          // HandRaySystem will update currState
-          button.prevState = button.currState;
-          button.currState = 'none';
-        });
-      }
-    }
-
-    ButtonSystem.queries = {
-      buttons: {
-        components: [Button]
-      }
-    }
-
-    class Draggable extends Component { }
-
-    Draggable.schema = {
-      // draggable states: [detached, hovered, to-be-attached, attached, to-be-detached]
-      state: { type: Types.String, default: 'none' },
-      originalParent: { type: Types.Ref, default: null },
-      attachedPointer: { type: Types.Ref, default: null }
-    }
-
-    class DraggableSystem extends System {
-      execute(delta, time) {
-        this.queries.draggable.results.forEach(entity => {
-          let draggable = entity.getMutableComponent(Draggable);
-          let object = entity.getComponent(Object3D).object;
-          if (draggable.originalParent == null) {
-            draggable.originalParent = object.parent;
-          }
-          switch (draggable.state) {
-            case 'to-be-attached':
-              draggable.attachedPointer.children[0].attach(object);
-              draggable.state = 'attached';
-              break;
-            case 'to-be-detached':
-              draggable.originalParent.attach(object);
-              draggable.state = 'detached';
-              break;
-            default:
-              object.scale.set(1, 1, 1);
-          }
-        });
-      }
-    }
-
-    DraggableSystem.queries = {
-      draggable: {
-        components: [Draggable]
-      }
-    }
-
-    class Intersectable extends TagComponent { }
-
-    class HandRaySystem extends System {
-      init(attributes) {
-        this.handPointers = attributes.handPointers;
-      }
-
-      execute(delta, time) {
-        this.handPointers.forEach(hp => {
-          var distance = null;
-          var intersectingEntity = null;
-          this.queries.intersectable.results.forEach(entity => {
-            let object = entity.getComponent(Object3D).object;
-            let intersections = hp.intersectObject(object);
-            if (intersections && intersections.length > 0) {
-              if (distance == null || intersections[0].distance < distance) {
-                distance = intersections[0].distance;
-                intersectingEntity = entity;
-              }
-            }
-          });
-          if (distance) {
-            hp.setCursor(distance);
-            if (intersectingEntity.hasComponent(Button)) {
-              let button = intersectingEntity.getMutableComponent(Button);
-              if (hp.isPinched()) {
-                button.currState = 'pressed';
-              } else if (button.currState != 'pressed') {
-                button.currState = 'hovered';
-              }
-            }
-            if (intersectingEntity.hasComponent(Draggable)) {
-              let draggable = intersectingEntity.getMutableComponent(Draggable);
-              let object = intersectingEntity.getComponent(Object3D).object;
-              object.scale.set(1.1, 1.1, 1.1);
-              if (hp.isPinched()) {
-                if (!hp.isAttached() && draggable.state != 'attached') {
-                  draggable.state = 'to-be-attached';
-                  draggable.attachedPointer = hp;
-                  hp.setAttached(true);
-                }
-              } else {
-                if (hp.isAttached() && draggable.state == 'attached') {
-                  console.log('hello');
-                  draggable.state = 'to-be-detached';
-                  draggable.attachedPointer = null;
-                  hp.setAttached(false);
-                }
-              }
-            }
-          } else {
-            hp.setCursor(1.5);
-          }
-        });
-      }
-    }
-
-    HandRaySystem.queries = {
-      intersectable: {
-        components: [Intersectable]
-      }
-    };
-
-    class HandsInstructionText extends TagComponent { }
-
-    class InstructionSystem extends System {
-      init(attributes) {
-        this.controllers = attributes.controllers;
-      }
-
-      execute(delta, time) {
-        let visible = false;
-        this.controllers.forEach(controller => {
-          if (controller.visible) {
-            visible = true;
-          }
-        })
-        this.queries.instructionTexts.results.forEach(entity => {
-          var object = entity.getComponent(Object3D).object;
-          object.visible = visible;
-        });
-      }
-    }
-
-    InstructionSystem.queries = {
-      instructionTexts: {
-        components: [HandsInstructionText]
-      }
-    }
-
-    class OffsetFromCamera extends Component { }
-
-    OffsetFromCamera.schema = {
-      x: { type: Types.Number, default: 0 },
-      y: { type: Types.Number, default: 0 },
-      z: { type: Types.Number, default: 0 },
-    }
-
-    class NeedCalibration extends TagComponent { }
-
-    class CalibrationSystem extends System {
-      init(attributes) {
-        this.camera = attributes.camera;
-        this.renderer = attributes.renderer;
-      }
-
-      execute(delta, time) {
-        this.queries.needCalibration.results.forEach(entity => {
-          if (this.renderer.xr.getSession()) {
-            let offset = entity.getComponent(OffsetFromCamera);
-            let object = entity.getComponent(Object3D).object;
-            let xrCamera = renderer.xr.getCamera(this.camera);
-            object.position.x = xrCamera.position.x + offset.x;
-            object.position.y = xrCamera.position.y + offset.y;
-            object.position.z = xrCamera.position.z + offset.z;
-            entity.removeComponent(NeedCalibration);
-          }
-        });
-      }
-    }
-
-    CalibrationSystem.queries = {
-      needCalibration: {
-        components: [NeedCalibration]
-      }
-    }
-
-    class Randomizable extends TagComponent { }
-
-    class RandomizerSystem extends System {
-      init(attributes) {
-        this.needRandomizing = true;
-      }
-
-      execute(delta, time) {
-        if (!this.needRandomizing) { return; }
-        this.queries.randomizable.results.forEach(entity => {
-          let object = entity.getComponent(Object3D).object;
-
-          object.material.color.setHex(Math.random() * 0xffffff);
-
-          object.position.x = Math.random() * 2 - 1;
-          object.position.y = Math.random() * 2;
-          object.position.z = Math.random() * 2 - 1;
-
-          object.rotation.x = Math.random() * 2 * Math.PI;
-          object.rotation.y = Math.random() * 2 * Math.PI;
-          object.rotation.z = Math.random() * 2 * Math.PI;
-
-          object.scale.x = Math.random() + 0.5;
-          object.scale.y = Math.random() + 0.5;
-          object.scale.z = Math.random() + 0.5;
-          this.needRandomizing = false;
-        });
-      }
-    }
-
-    RandomizerSystem.queries = {
-      randomizable: {
-        components: [Randomizable]
-      }
-    }
-
-    let world = new World();
-    var clock = new THREE.Clock();
-    let camera, scene, renderer;
-
-    init();
-    animate();
-
-    function makeButtonMesh(x, y, z, color) {
-      const geometry = new THREE.BoxGeometry(x, y, z);
-      const material = new THREE.MeshPhongMaterial({ color: color });
-      const buttonMesh = new THREE.Mesh(geometry, material);
-      return buttonMesh;
-    }
-
-
-    function init() {
-
-      let container = document.createElement('div');
-      document.body.appendChild(container);
-
-      scene = new THREE.Scene();
-      scene.background = new THREE.Color(0x444444);
-
-      camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
-      camera.position.set(0, 1.2, 0.3);
-
-      scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
-
-      const light = new THREE.DirectionalLight(0xffffff);
-      light.position.set(0, 6, 0);
-      light.castShadow = true;
-      light.shadow.camera.top = 2;
-      light.shadow.camera.bottom = - 2;
-      light.shadow.camera.right = 2;
-      light.shadow.camera.left = - 2;
-      light.shadow.mapSize.set(4096, 4096);
-      scene.add(light);
-
-      renderer = new THREE.WebGLRenderer({ antialias: true });
-      renderer.setPixelRatio(window.devicePixelRatio);
-      renderer.setSize(window.innerWidth, window.innerHeight);
-      renderer.outputEncoding = THREE.sRGBEncoding;
-      renderer.shadowMap.enabled = true;
-      renderer.xr.enabled = true;
-
-      container.appendChild(renderer.domElement);
-
-      document.body.appendChild(VRButton.createButton(renderer));
-
-      // controllers
-      let controller1 = renderer.xr.getController(0);
-      scene.add(controller1);
-
-      let controller2 = renderer.xr.getController(1);
-      scene.add(controller2);
-
-      const controllerModelFactory = new XRControllerModelFactory();
-
-      // Hand 1
-      let controllerGrip1 = renderer.xr.getControllerGrip(0);
-      controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
-      scene.add(controllerGrip1);
-
-      let hand1 = renderer.xr.getHand(0);
-      hand1.add(new OculusHandModel(hand1));
-      let handPointer1 = new OculusHandPointerModel(hand1, controller1);
-      hand1.add(handPointer1);
-
-      scene.add(hand1);
-
-      // Hand 2
-      let controllerGrip2 = renderer.xr.getControllerGrip(1);
-      controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
-      scene.add(controllerGrip2);
-
-      let hand2 = renderer.xr.getHand(1);
-      hand2.add(new OculusHandModel(hand2));
-      let handPointer2 = new OculusHandPointerModel(hand2, controller2);
-      hand2.add(handPointer2);
-      scene.add(hand2);
-
-
-      // setup objects in scene and entities
-      const floorGeometry = new THREE.PlaneGeometry(4, 4);
-      const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 });
-      let floor = new THREE.Mesh(floorGeometry, floorMaterial);
-      floor.rotation.x = - Math.PI / 2;
-      scene.add(floor);
-
-      const menuGeometry = new THREE.PlaneGeometry(0.24, 0.5);
-      const menuMaterial = new THREE.MeshPhongMaterial({
-        opacity: 0,
-        transparent: true,
-      });
-      let menuMesh = new THREE.Mesh(menuGeometry, menuMaterial);
-      menuMesh.position.set(0.4, 1, -1);
-      menuMesh.rotation.y = - Math.PI / 12;
-      scene.add(menuMesh);
-
-      let resetButton = makeButtonMesh(0.2, 0.1, 0.01, 0x355c7d);
-      let resetButtonText = createText("reset", 0.06);
-      resetButton.add(resetButtonText);
-      resetButtonText.position.set(0, 0, 0.0051);
-      resetButton.position.set(0, -0.06, 0);
-      menuMesh.add(resetButton);
-
-      let exitButton = makeButtonMesh(0.2, 0.1, 0.01, 0xff0000);
-      let exitButtonText = createText("exit", 0.06);
-      exitButton.add(exitButtonText);
-      exitButtonText.position.set(0, 0, 0.0051);
-      exitButton.position.set(0, -0.18, 0);
-      menuMesh.add(exitButton);
-
-      let instructionText = createText("This is a WebXR Hands demo, please explore with hands.", 0.04);
-      instructionText.position.set(0, 1.6, -0.6);
-      scene.add(instructionText);
-
-      let exitText = createText("Exiting session...", 0.04);
-      exitText.position.set(0, 1.5, -0.6);
-      exitText.visible = false;
-      scene.add(exitText);
-
-      world
-        .registerComponent(Object3D)
-        .registerComponent(Button)
-        .registerComponent(Intersectable)
-        .registerComponent(HandsInstructionText)
-        .registerComponent(OffsetFromCamera)
-        .registerComponent(NeedCalibration)
-        .registerComponent(Randomizable)
-        .registerComponent(Draggable);
-
-      world
-        .registerSystem(RandomizerSystem)
-        .registerSystem(InstructionSystem, { controllers: [controllerGrip1, controllerGrip2] })
-        .registerSystem(CalibrationSystem, { renderer: renderer, camera: camera })
-        .registerSystem(ButtonSystem)
-        .registerSystem(DraggableSystem)
-        .registerSystem(HandRaySystem, { handPointers: [handPointer1, handPointer2] });
-
-      for (let i = 0; i < 20; i++) {
-        const object = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.15, 0.15), new THREE.MeshLambertMaterial({ color: 0xffffff }));
-        scene.add(object);
-
-        let entity = world.createEntity();
-        entity.addComponent(Intersectable);
-        entity.addComponent(Randomizable);
-        entity.addComponent(Object3D, { object: object });
-        entity.addComponent(Draggable);
-      }
-
-      var menuEntity = world.createEntity();
-      menuEntity.addComponent(Intersectable);
-      menuEntity.addComponent(OffsetFromCamera, { x: 0.4, y: 0, z: -1 });
-      menuEntity.addComponent(NeedCalibration);
-      menuEntity.addComponent(Object3D, { object: menuMesh });
-
-      var rbEntity = world.createEntity();
-      rbEntity.addComponent(Intersectable);
-      rbEntity.addComponent(Object3D, { object: resetButton });
-      let rbAction = function () { world.getSystem(RandomizerSystem).needRandomizing = true; };
-      rbEntity.addComponent(Button, { action: rbAction });
-
-      var ebEntity = world.createEntity();
-      ebEntity.addComponent(Intersectable);
-      ebEntity.addComponent(Object3D, { object: exitButton });
-      let ebAction = function () {
-        exitText.visible = true;
-        setTimeout(function () { exitText.visible = false; renderer.xr.getSession().end(); }, 2000);
-      };
-      ebEntity.addComponent(Button, { action: ebAction });
-
-      var itEntity = world.createEntity();
-      itEntity.addComponent(HandsInstructionText);
-      itEntity.addComponent(Object3D, { object: instructionText });
-
-      window.addEventListener('resize', onWindowResize);
-
-    }
-
-    function onWindowResize() {
-
-      camera.aspect = window.innerWidth / window.innerHeight;
-      camera.updateProjectionMatrix();
-
-      renderer.setSize(window.innerWidth, window.innerHeight);
-
-    }
-
-    function animate() {
-
-      renderer.setAnimationLoop(render);
-
-    }
-
-    function render() {
-      var delta = clock.getDelta();
-      var elapsedTime = clock.elapsedTime;
-      world.execute(delta, elapsedTime);
-      renderer.render(scene, camera);
-    }
-
-  </script>
+	<div id="info">
+		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> vr - handinput - point and drag<br />
+		(Oculus Browser 15.1+)
+	</div>
+
+	<script type="module">
+
+		import * as THREE from '../build/three.module.js';
+		import { VRButton } from './jsm/webxr/VRButton.js';
+		import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
+		import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
+		import { OculusHandPointerModel } from './jsm/webxr/OculusHandPointerModel.js';
+		import { createText } from './jsm/webxr/Text2D.js';
+
+		import { World, System, Component, TagComponent, Types } from './jsm/libs/ecsy.module.js';
+
+		class Object3D extends Component { }
+
+		Object3D.schema = {
+			object: { type: Types.Ref }
+		};
+
+		class Button extends Component { }
+
+		Button.schema = {
+			// button states: [none, hovered, pressed]
+			currState: { type: Types.String, default: 'none' },
+			prevState: { type: Types.String, default: 'none' },
+			action: { type: Types.Ref, default: () => { } }
+		};
+
+		class ButtonSystem extends System {
+
+			execute( /*delta, time*/ ) {
+
+				this.queries.buttons.results.forEach( entity => {
+
+					const button = entity.getMutableComponent( Button );
+					const buttonMesh = entity.getComponent( Object3D ).object;
+					if ( button.currState == 'none' ) {
+
+						buttonMesh.scale.set( 1, 1, 1 );
+
+					} else {
+
+						buttonMesh.scale.set( 1.1, 1.1, 1.1 );
+
+					}
+
+					if ( button.currState == 'pressed' && button.prevState != 'pressed' ) {
+
+						button.action();
+
+					}
+
+					// preserve prevState, clear currState
+					// HandRaySystem will update currState
+					button.prevState = button.currState;
+					button.currState = 'none';
+
+				} );
+
+			}
+
+		}
+
+		ButtonSystem.queries = {
+			buttons: {
+				components: [ Button ]
+			}
+		};
+
+		class Draggable extends Component { }
+
+		Draggable.schema = {
+			// draggable states: [detached, hovered, to-be-attached, attached, to-be-detached]
+			state: { type: Types.String, default: 'none' },
+			originalParent: { type: Types.Ref, default: null },
+			attachedPointer: { type: Types.Ref, default: null }
+		};
+
+		class DraggableSystem extends System {
+
+			execute( /*delta, time*/ ) {
+
+				this.queries.draggable.results.forEach( entity => {
+
+					const draggable = entity.getMutableComponent( Draggable );
+					const object = entity.getComponent( Object3D ).object;
+					if ( draggable.originalParent == null ) {
+
+						draggable.originalParent = object.parent;
+
+					}
+
+					switch ( draggable.state ) {
+
+						case 'to-be-attached':
+							draggable.attachedPointer.children[ 0 ].attach( object );
+							draggable.state = 'attached';
+							break;
+						case 'to-be-detached':
+							draggable.originalParent.attach( object );
+							draggable.state = 'detached';
+							break;
+						default:
+							object.scale.set( 1, 1, 1 );
+
+					}
+
+				} );
+
+			}
+
+		}
+
+		DraggableSystem.queries = {
+			draggable: {
+				components: [ Draggable ]
+			}
+		};
+
+		class Intersectable extends TagComponent { }
+
+		class HandRaySystem extends System {
+
+			init( attributes ) {
+
+				this.handPointers = attributes.handPointers;
+
+			}
+
+			execute( /*delta, time*/ ) {
+
+				this.handPointers.forEach( hp => {
+
+					let distance = null;
+					let intersectingEntity = null;
+					this.queries.intersectable.results.forEach( entity => {
+
+						const object = entity.getComponent( Object3D ).object;
+						const intersections = hp.intersectObject( object );
+						if ( intersections && intersections.length > 0 ) {
+
+							if ( distance == null || intersections[ 0 ].distance < distance ) {
+
+								distance = intersections[ 0 ].distance;
+								intersectingEntity = entity;
+
+							}
+
+						}
+
+					} );
+					if ( distance ) {
+
+						hp.setCursor( distance );
+						if ( intersectingEntity.hasComponent( Button ) ) {
+
+							const button = intersectingEntity.getMutableComponent( Button );
+							if ( hp.isPinched() ) {
+
+								button.currState = 'pressed';
+
+							} else if ( button.currState != 'pressed' ) {
+
+								button.currState = 'hovered';
+
+							}
+
+						}
+
+						if ( intersectingEntity.hasComponent( Draggable ) ) {
+
+							const draggable = intersectingEntity.getMutableComponent( Draggable );
+							const object = intersectingEntity.getComponent( Object3D ).object;
+							object.scale.set( 1.1, 1.1, 1.1 );
+							if ( hp.isPinched() ) {
+
+								if ( ! hp.isAttached() && draggable.state != 'attached' ) {
+
+									draggable.state = 'to-be-attached';
+									draggable.attachedPointer = hp;
+									hp.setAttached( true );
+
+								}
+
+							} else {
+
+								if ( hp.isAttached() && draggable.state == 'attached' ) {
+
+									console.log( 'hello' );
+									draggable.state = 'to-be-detached';
+									draggable.attachedPointer = null;
+									hp.setAttached( false );
+
+								}
+
+							}
+
+						}
+
+					} else {
+
+						hp.setCursor( 1.5 );
+
+					}
+
+				} );
+
+			}
+
+		}
+
+		HandRaySystem.queries = {
+			intersectable: {
+				components: [ Intersectable ]
+			}
+		};
+
+		class HandsInstructionText extends TagComponent { }
+
+		class InstructionSystem extends System {
+
+			init( attributes ) {
+
+				this.controllers = attributes.controllers;
+
+			}
+
+			execute( /*delta, time*/ ) {
+
+				let visible = false;
+				this.controllers.forEach( controller => {
+
+					if ( controller.visible ) {
+
+						visible = true;
+
+					}
+
+				} );
+
+				this.queries.instructionTexts.results.forEach( entity => {
+
+					const object = entity.getComponent( Object3D ).object;
+					object.visible = visible;
+
+				} );
+
+			}
+
+		}
+
+		InstructionSystem.queries = {
+			instructionTexts: {
+				components: [ HandsInstructionText ]
+			}
+		};
+
+		class OffsetFromCamera extends Component { }
+
+		OffsetFromCamera.schema = {
+			x: { type: Types.Number, default: 0 },
+			y: { type: Types.Number, default: 0 },
+			z: { type: Types.Number, default: 0 },
+		};
+
+		class NeedCalibration extends TagComponent { }
+
+		class CalibrationSystem extends System {
+
+			init( attributes ) {
+
+				this.camera = attributes.camera;
+				this.renderer = attributes.renderer;
+
+	}
+
+			execute( /*delta, time*/ ) {
+
+				this.queries.needCalibration.results.forEach( entity => {
+
+					if ( this.renderer.xr.getSession() ) {
+
+						const offset = entity.getComponent( OffsetFromCamera );
+						const object = entity.getComponent( Object3D ).object;
+						const xrCamera = renderer.xr.getCamera( this.camera );
+						object.position.x = xrCamera.position.x + offset.x;
+						object.position.y = xrCamera.position.y + offset.y;
+						object.position.z = xrCamera.position.z + offset.z;
+						entity.removeComponent( NeedCalibration );
+
+					}
+
+				} );
+
+			}
+
+		}
+
+		CalibrationSystem.queries = {
+			needCalibration: {
+				components: [ NeedCalibration ]
+			}
+		};
+
+		class Randomizable extends TagComponent { }
+
+		class RandomizerSystem extends System {
+
+			init( /*attributes*/ ) {
+
+				this.needRandomizing = true;
+
+			}
+
+			execute( /*delta, time*/ ) {
+
+				if ( ! this.needRandomizing ) {
+
+					return;
+
+				}
+
+				this.queries.randomizable.results.forEach( entity => {
+
+					const object = entity.getComponent( Object3D ).object;
+
+					object.material.color.setHex( Math.random() * 0xffffff );
+
+					object.position.x = Math.random() * 2 - 1;
+					object.position.y = Math.random() * 2;
+					object.position.z = Math.random() * 2 - 1;
+
+					object.rotation.x = Math.random() * 2 * Math.PI;
+					object.rotation.y = Math.random() * 2 * Math.PI;
+					object.rotation.z = Math.random() * 2 * Math.PI;
+
+					object.scale.x = Math.random() + 0.5;
+					object.scale.y = Math.random() + 0.5;
+					object.scale.z = Math.random() + 0.5;
+					this.needRandomizing = false;
+
+				} );
+
+			}
+
+		}
+
+		RandomizerSystem.queries = {
+			randomizable: {
+				components: [ Randomizable ]
+			}
+		};
+
+		const world = new World();
+		const clock = new THREE.Clock();
+		let camera, scene, renderer;
+
+		init();
+		animate();
+
+		function makeButtonMesh( x, y, z, color ) {
+
+			const geometry = new THREE.BoxGeometry( x, y, z );
+			const material = new THREE.MeshPhongMaterial( { color: color } );
+			const buttonMesh = new THREE.Mesh( geometry, material );
+			return buttonMesh;
+
+		}
+
+		function init() {
+
+			const container = document.createElement( 'div' );
+			document.body.appendChild( container );
+
+			scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0x444444 );
+
+			camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 10 );
+			camera.position.set( 0, 1.2, 0.3 );
+
+			scene.add( new THREE.HemisphereLight( 0x808080, 0x606060 ) );
+
+			const light = new THREE.DirectionalLight( 0xffffff );
+			light.position.set( 0, 6, 0 );
+			light.castShadow = true;
+			light.shadow.camera.top = 2;
+			light.shadow.camera.bottom = - 2;
+			light.shadow.camera.right = 2;
+			light.shadow.camera.left = - 2;
+			light.shadow.mapSize.set( 4096, 4096 );
+			scene.add( light );
+
+			renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			renderer.outputEncoding = THREE.sRGBEncoding;
+			renderer.shadowMap.enabled = true;
+			renderer.xr.enabled = true;
+
+			container.appendChild( renderer.domElement );
+
+			document.body.appendChild( VRButton.createButton( renderer ) );
+
+			// controllers
+			const controller1 = renderer.xr.getController( 0 );
+			scene.add( controller1 );
+
+			const controller2 = renderer.xr.getController( 1 );
+			scene.add( controller2 );
+
+			const controllerModelFactory = new XRControllerModelFactory();
+
+			// Hand 1
+			const controllerGrip1 = renderer.xr.getControllerGrip( 0 );
+			controllerGrip1.add( controllerModelFactory.createControllerModel( controllerGrip1 ) );
+			scene.add( controllerGrip1 );
+
+			const hand1 = renderer.xr.getHand( 0 );
+			hand1.add( new OculusHandModel( hand1 ) );
+			const handPointer1 = new OculusHandPointerModel( hand1, controller1 );
+			hand1.add( handPointer1 );
+
+			scene.add( hand1 );
+
+			// Hand 2
+			const controllerGrip2 = renderer.xr.getControllerGrip( 1 );
+			controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
+			scene.add( controllerGrip2 );
+
+			const hand2 = renderer.xr.getHand( 1 );
+			hand2.add( new OculusHandModel( hand2 ) );
+			const handPointer2 = new OculusHandPointerModel( hand2, controller2 );
+			hand2.add( handPointer2 );
+			scene.add( hand2 );
+
+
+			// setup objects in scene and entities
+			const floorGeometry = new THREE.PlaneGeometry( 4, 4 );
+			const floorMaterial = new THREE.MeshPhongMaterial( { color: 0x222222 } );
+			const floor = new THREE.Mesh( floorGeometry, floorMaterial );
+			floor.rotation.x = - Math.PI / 2;
+			scene.add( floor );
+
+			const menuGeometry = new THREE.PlaneGeometry( 0.24, 0.5 );
+			const menuMaterial = new THREE.MeshPhongMaterial( {
+				opacity: 0,
+				transparent: true,
+			} );
+			const menuMesh = new THREE.Mesh( menuGeometry, menuMaterial );
+			menuMesh.position.set( 0.4, 1, - 1 );
+			menuMesh.rotation.y = - Math.PI / 12;
+			scene.add( menuMesh );
+
+			const resetButton = makeButtonMesh( 0.2, 0.1, 0.01, 0x355c7d );
+			const resetButtonText = createText( 'reset', 0.06 );
+			resetButton.add( resetButtonText );
+			resetButtonText.position.set( 0, 0, 0.0051 );
+			resetButton.position.set( 0, - 0.06, 0 );
+			menuMesh.add( resetButton );
+
+			const exitButton = makeButtonMesh( 0.2, 0.1, 0.01, 0xff0000 );
+			const exitButtonText = createText( 'exit', 0.06 );
+			exitButton.add( exitButtonText );
+			exitButtonText.position.set( 0, 0, 0.0051 );
+			exitButton.position.set( 0, - 0.18, 0 );
+			menuMesh.add( exitButton );
+
+			const instructionText = createText( 'This is a WebXR Hands demo, please explore with hands.', 0.04 );
+			instructionText.position.set( 0, 1.6, - 0.6 );
+			scene.add( instructionText );
+
+			const exitText = createText( 'Exiting session...', 0.04 );
+			exitText.position.set( 0, 1.5, - 0.6 );
+			exitText.visible = false;
+			scene.add( exitText );
+
+			world
+				.registerComponent( Object3D )
+				.registerComponent( Button )
+				.registerComponent( Intersectable )
+				.registerComponent( HandsInstructionText )
+				.registerComponent( OffsetFromCamera )
+				.registerComponent( NeedCalibration )
+				.registerComponent( Randomizable )
+				.registerComponent( Draggable );
+
+			world
+				.registerSystem( RandomizerSystem )
+				.registerSystem( InstructionSystem, { controllers: [ controllerGrip1, controllerGrip2 ] } )
+				.registerSystem( CalibrationSystem, { renderer: renderer, camera: camera } )
+				.registerSystem( ButtonSystem )
+				.registerSystem( DraggableSystem )
+				.registerSystem( HandRaySystem, { handPointers: [ handPointer1, handPointer2 ] } );
+
+			for ( let i = 0; i < 20; i ++ ) {
+
+				const object = new THREE.Mesh( new THREE.BoxGeometry( 0.15, 0.15, 0.15 ), new THREE.MeshLambertMaterial( { color: 0xffffff } ) );
+				scene.add( object );
+
+				const entity = world.createEntity();
+				entity.addComponent( Intersectable );
+				entity.addComponent( Randomizable );
+				entity.addComponent( Object3D, { object: object } );
+				entity.addComponent( Draggable );
+
+			}
+
+			const menuEntity = world.createEntity();
+			menuEntity.addComponent( Intersectable );
+			menuEntity.addComponent( OffsetFromCamera, { x: 0.4, y: 0, z: - 1 } );
+			menuEntity.addComponent( NeedCalibration );
+			menuEntity.addComponent( Object3D, { object: menuMesh } );
+
+			const rbEntity = world.createEntity();
+			rbEntity.addComponent( Intersectable );
+			rbEntity.addComponent( Object3D, { object: resetButton } );
+			const rbAction = function () {
+
+				world.getSystem( RandomizerSystem ).needRandomizing = true;
+
+			};
+
+			rbEntity.addComponent( Button, { action: rbAction } );
+
+			const ebEntity = world.createEntity();
+			ebEntity.addComponent( Intersectable );
+			ebEntity.addComponent( Object3D, { object: exitButton } );
+			const ebAction = function () {
+
+				exitText.visible = true;
+				setTimeout( function () {
+
+					exitText.visible = false; renderer.xr.getSession().end();
+
+				}, 2000 );
+
+			};
+
+			ebEntity.addComponent( Button, { action: ebAction } );
+
+			const itEntity = world.createEntity();
+			itEntity.addComponent( HandsInstructionText );
+			itEntity.addComponent( Object3D, { object: instructionText } );
+
+			window.addEventListener( 'resize', onWindowResize );
+
+		}
+
+		function onWindowResize() {
+
+			camera.aspect = window.innerWidth / window.innerHeight;
+			camera.updateProjectionMatrix();
+
+			renderer.setSize( window.innerWidth, window.innerHeight );
+
+		}
+
+		function animate() {
+
+			renderer.setAnimationLoop( render );
+
+		}
+
+		function render() {
+
+			const delta = clock.getDelta();
+			const elapsedTime = clock.elapsedTime;
+			world.execute( delta, elapsedTime );
+			renderer.render( scene, camera );
+
+		}
+
+	</script>
 </body>
 </body>
 
 
 </html>
 </html>

+ 555 - 449
examples/webxr_vr_handinput_pressbutton.html

@@ -2,459 +2,565 @@
 <html lang="en">
 <html lang="en">
 
 
 <head>
 <head>
-  <title>three.js webxr hands - press button</title>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-  <link type="text/css" rel="stylesheet" href="main.css">
+	<title>three.js vr - handinput - press button</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+	<link type="text/css" rel="stylesheet" href="main.css">
 </head>
 </head>
 
 
 <body>
 <body>
 
 
-  <div id="info">
-    <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>  vr - handinput - press button<br />
-    (Oculus Browser 15.1+)
-  </div>
-
-  <script type="module">
-
-    import * as THREE from '../build/three.module.js';
-    import { VRButton } from './jsm/webxr/VRButton.js';
-    import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
-    import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
-    import { createText } from './jsm/webxr/Text2D.js';
-
-    import { World, System, Component, TagComponent, Types } from './jsm/libs/ecsy.module.js';
-
-    class Object3D extends Component { }
-
-    Object3D.schema = {
-      object: { type: Types.Ref }
-    };
-
-    class Button extends Component { }
-
-    Button.schema = {
-      // button states: [resting, pressed, fully_pressed, recovering]
-      currState: { type: Types.String, default: 'resting' },
-      prevState: { type: Types.String, default: 'resting' },
-      pressSound: { type: Types.Ref, default: null },
-      releaseSound: { type: Types.Ref, default: null },
-      restingY: { type: Types.Number, default: null },
-      surfaceY: { type: Types.Number, default: null },
-      recoverySpeed: { type: Types.Number, default: 0.4 },
-      fullPressDistance: { type: Types.Number, default: null },
-      action: { type: Types.Ref, default: () => { } }
-    }
-
-    class ButtonSystem extends System {
-      init(attributes) {
-        this.renderer = attributes.renderer;
-        this.soundAdded = false;
-      }
-
-      execute(delta, time) {
-        let buttonPressSound, buttonReleaseSound;
-        if (this.renderer.xr.getSession() && !this.soundAdded) {
-          let xrCamera = this.renderer.xr.getCamera(camera);
-
-          const listener = new THREE.AudioListener();
-          xrCamera.add(listener);
-
-          // create a global audio source
-          buttonPressSound = new THREE.Audio(listener);
-          buttonReleaseSound = new THREE.Audio(listener);
-
-          // load a sound and set it as the Audio object's buffer
-          const audioLoader = new THREE.AudioLoader();
-          audioLoader.load('sounds/button-press.ogg', function (buffer) {
-            buttonPressSound.setBuffer(buffer);
-          });
-          audioLoader.load('sounds/button-release.ogg', function (buffer) {
-            buttonReleaseSound.setBuffer(buffer);
-          });
-          this.soundAdded = true;
-        }
-
-        this.queries.buttons.results.forEach(entity => {
-          var button = entity.getMutableComponent(Button);
-          var buttonMesh = entity.getComponent(Object3D).object;
-          // populate restingY
-          if (button.restingY == null) {
-            button.restingY = buttonMesh.position.y;
-          }
-          if (buttonPressSound) {
-            button.pressSound = buttonPressSound;
-          }
-          if (buttonReleaseSound) {
-            button.releaseSound = buttonReleaseSound;
-          }
-
-          if (button.currState == 'fully_pressed' && button.prevState != 'fully_pressed') {
-            button.pressSound?.play();
-            button.action();
-          }
-          if (button.currState == 'recovering' && button.prevState != 'recovering') {
-            button.releaseSound?.play();
-          }
-
-          // preserve prevState, clear currState
-          // FingerInputSystem will update currState
-          button.prevState = button.currState;
-          button.currState = 'resting';
-        });
-      }
-    }
-
-    ButtonSystem.queries = {
-      buttons: {
-        components: [Button]
-      }
-    }
-
-    class Pressable extends TagComponent { }
-
-    class FingerInputSystem extends System {
-      init(attributes) {
-        this.hands = attributes.hands;
-      }
-
-      execute(delta, time) {
-        this.queries.pressable.results.forEach(entity => {
-          var button = entity.getMutableComponent(Button);
-          let object = entity.getComponent(Object3D).object;
-          let pressingDistances = [];
-          this.hands.forEach(hand => {
-            if (hand && hand.intersectBoxObject(object)) {
-              let pressingPosition = hand.getPointerPosition();
-              pressingDistances.push(button.surfaceY - object.worldToLocal(pressingPosition).y);
-            }
-          });
-          if (pressingDistances.length == 0) { // not pressed this frame
-            if (object.position.y < button.restingY) {
-              object.position.y += button.recoverySpeed * delta;
-              button.currState = "recovering";
-            } else {
-              object.position.y = button.restingY;
-              button.currState = "resting";
-            }
-          } else {
-            button.currState = "pressed";
-            let pressingDistance = Math.max(pressingDistances);
-            if (pressingDistance > 0) {
-              object.position.y -= pressingDistance;
-            }
-            if (object.position.y <= button.restingY - button.fullPressDistance) {
-              button.currState = "fully_pressed";
-              object.position.y = button.restingY - button.fullPressDistance;
-            }
-          }
-        });
-      }
-    }
-
-    FingerInputSystem.queries = {
-      pressable: {
-        components: [Pressable]
-      }
-    };
-
-    class Rotating extends TagComponent { }
-
-    class RotatingSystem extends System {
-      execute(delta, time) {
-        this.queries.rotatingObjects.results.forEach(entity => {
-          var object = entity.getComponent(Object3D).object;
-          object.rotation.x += 0.4 * delta;
-          object.rotation.y += 0.4 * delta;
-        });
-      }
-    }
-
-    RotatingSystem.queries = {
-      rotatingObjects: {
-        components: [Rotating]
-      }
-    }
-
-    class HandsInstructionText extends TagComponent { }
-
-    class InstructionSystem extends System {
-      init(attributes) {
-        this.controllers = attributes.controllers;
-      }
-
-      execute(delta, time) {
-        let visible = false;
-        this.controllers.forEach(controller => {
-          if (controller.visible) {
-            visible = true;
-          }
-        })
-        this.queries.instructionTexts.results.forEach(entity => {
-          var object = entity.getComponent(Object3D).object;
-          object.visible = visible;
-        });
-      }
-    }
-
-    InstructionSystem.queries = {
-      instructionTexts: {
-        components: [HandsInstructionText]
-      }
-    }
-
-    class OffsetFromCamera extends Component { }
-
-    OffsetFromCamera.schema = {
-      x: { type: Types.Number, default: 0 },
-      y: { type: Types.Number, default: 0 },
-      z: { type: Types.Number, default: 0 },
-    }
-
-    class NeedCalibration extends TagComponent { }
-
-    class CalibrationSystem extends System {
-      init(attributes) {
-        this.camera = attributes.camera;
-        this.renderer = attributes.renderer;
-      }
-
-      execute(delta, time) {
-        this.queries.needCalibration.results.forEach(entity => {
-          if (this.renderer.xr.getSession()) {
-            let offset = entity.getComponent(OffsetFromCamera);
-            let object = entity.getComponent(Object3D).object;
-            let xrCamera = renderer.xr.getCamera(this.camera);
-            object.position.x = xrCamera.position.x + offset.x;
-            object.position.y = xrCamera.position.y + offset.y;
-            object.position.z = xrCamera.position.z + offset.z;
-            entity.removeComponent(NeedCalibration);
-          }
-        });
-      }
-    }
-
-    CalibrationSystem.queries = {
-      needCalibration: {
-        components: [NeedCalibration]
-      }
-    }
-
-    let world = new World();
-    var clock = new THREE.Clock();
-    let camera, scene, renderer;
-
-    init();
-    animate();
-
-    function makeButtonMesh(x, y, z, color) {
-      const geometry = new THREE.BoxGeometry(x, y, z);
-      const material = new THREE.MeshPhongMaterial({ color: color });
-      const buttonMesh = new THREE.Mesh(geometry, material);
-      return buttonMesh;
-    }
-
-
-    function init() {
-
-      let container = document.createElement('div');
-      document.body.appendChild(container);
-
-      scene = new THREE.Scene();
-      scene.background = new THREE.Color(0x444444);
-
-      camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
-      camera.position.set(0, 1.2, 0.3);
-
-      scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
-
-      const light = new THREE.DirectionalLight(0xffffff);
-      light.position.set(0, 6, 0);
-      light.castShadow = true;
-      light.shadow.camera.top = 2;
-      light.shadow.camera.bottom = - 2;
-      light.shadow.camera.right = 2;
-      light.shadow.camera.left = - 2;
-      light.shadow.mapSize.set(4096, 4096);
-      scene.add(light);
-
-      renderer = new THREE.WebGLRenderer({ antialias: true });
-      renderer.setPixelRatio(window.devicePixelRatio);
-      renderer.setSize(window.innerWidth, window.innerHeight);
-      renderer.outputEncoding = THREE.sRGBEncoding;
-      renderer.shadowMap.enabled = true;
-      renderer.xr.enabled = true;
-
-      container.appendChild(renderer.domElement);
-
-      document.body.appendChild(VRButton.createButton(renderer));
-
-      // controllers
-      let controller1 = renderer.xr.getController(0);
-      scene.add(controller1);
-
-      let controller2 = renderer.xr.getController(1);
-      scene.add(controller2);
-
-      const controllerModelFactory = new XRControllerModelFactory();
-
-      // Hand 1
-      let controllerGrip1 = renderer.xr.getControllerGrip(0);
-      controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
-      scene.add(controllerGrip1);
-
-      let hand1 = renderer.xr.getHand(0);
-      let handModel1 = new OculusHandModel(hand1)
-      hand1.add(handModel1);
-      scene.add(hand1);
-
-      // Hand 2
-      let controllerGrip2 = renderer.xr.getControllerGrip(1);
-      controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
-      scene.add(controllerGrip2);
-
-      let hand2 = renderer.xr.getHand(1);
-      let handModel2 = new OculusHandModel(hand2);
-      hand2.add(handModel2);
-      scene.add(hand2);
-
-
-      // buttons
-      const floorGeometry = new THREE.PlaneGeometry(4, 4);
-      const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 });
-      const floor = new THREE.Mesh(floorGeometry, floorMaterial);
-      floor.rotation.x = - Math.PI / 2;
-      scene.add(floor);
-
-      const consoleGeometry = new THREE.BoxGeometry(0.5, 0.12, 0.15);
-      const consoleMaterial = new THREE.MeshPhongMaterial({ color: 0x595959 });
-      let consoleMesh = new THREE.Mesh(consoleGeometry, consoleMaterial);
-      consoleMesh.position.set(0, 1, -0.3);
-      scene.add(consoleMesh);
-
-      let orangeButton = makeButtonMesh(0.08, 0.1, 0.08, 0xffd3b5);
-      orangeButton.position.set(-0.15, 0.04, 0);
-      consoleMesh.add(orangeButton);
-
-      let pinkButton = makeButtonMesh(0.08, 0.1, 0.08, 0xe84a5f);
-      pinkButton.position.set(-0.05, 0.04, 0);
-      consoleMesh.add(pinkButton);
-
-      let resetButton = makeButtonMesh(0.08, 0.1, 0.08, 0x355c7d);
-      let resetButtonText = createText("reset", 0.03);
-      resetButton.add(resetButtonText);
-      resetButtonText.rotation.x = - Math.PI / 2;
-      resetButtonText.position.set(0, 0.051, 0);
-      resetButton.position.set(0.05, 0.04, 0);
-      consoleMesh.add(resetButton);
-
-      let exitButton = makeButtonMesh(0.08, 0.1, 0.08, 0xff0000);
-      let exitButtonText = createText("exit", 0.03);
-      exitButton.add(exitButtonText);
-      exitButtonText.rotation.x = - Math.PI / 2;
-      exitButtonText.position.set(0, 0.051, 0);
-      exitButton.position.set(0.15, 0.04, 0);
-      consoleMesh.add(exitButton);
-
-      let tkGeometry = new THREE.TorusKnotGeometry(0.5, 0.2, 200, 32);
-      let tkMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
-      tkMaterial.metalness = 0.8;
-      let torusKnot = new THREE.Mesh(tkGeometry, tkMaterial);
-      torusKnot.position.set(0, 1, -5);
-      scene.add(torusKnot);
-
-      let instructionText = createText("This is a WebXR Hands demo, please explore with hands.", 0.04);
-      instructionText.position.set(0, 1.6, -0.6);
-      scene.add(instructionText);
-
-      let exitText = createText("Exiting session...", 0.04);
-      exitText.position.set(0, 1.5, -0.6);
-      exitText.visible = false;
-      scene.add(exitText);
-
-      world
-        .registerComponent(Object3D)
-        .registerComponent(Button)
-        .registerComponent(Pressable)
-        .registerComponent(Rotating)
-        .registerComponent(HandsInstructionText)
-        .registerComponent(OffsetFromCamera)
-        .registerComponent(NeedCalibration);
-
-      world
-        .registerSystem(RotatingSystem)
-        .registerSystem(InstructionSystem, { controllers: [controllerGrip1, controllerGrip2] })
-        .registerSystem(CalibrationSystem, { renderer: renderer, camera: camera })
-        .registerSystem(ButtonSystem, { renderer: renderer, camera: camera })
-        .registerSystem(FingerInputSystem, { hands: [handModel1, handModel2] });
-
-      var csEntity = world.createEntity();
-      csEntity.addComponent(OffsetFromCamera, { x: 0, y: -0.4, z: -0.3 });
-      csEntity.addComponent(NeedCalibration);
-      csEntity.addComponent(Object3D, { object: consoleMesh });
-
-      var obEntity = world.createEntity();
-      obEntity.addComponent(Pressable);
-      obEntity.addComponent(Object3D, { object: orangeButton });
-      let obAction = function () { torusKnot.material.color.setHex(0xffd3b5); };
-      obEntity.addComponent(Button, { action: obAction, surfaceY: 0.05, fullPressDistance: 0.02 });
-
-      var pbEntity = world.createEntity();
-      pbEntity.addComponent(Pressable);
-      pbEntity.addComponent(Object3D, { object: pinkButton });
-      let pbAction = function () { torusKnot.material.color.setHex(0xe84a5f); };
-      pbEntity.addComponent(Button, { action: pbAction, surfaceY: 0.05, fullPressDistance: 0.02 });
-
-      var rbEntity = world.createEntity();
-      rbEntity.addComponent(Pressable);
-      rbEntity.addComponent(Object3D, { object: resetButton });
-      let rbAction = function () { torusKnot.material.color.setHex(0xffffff); };
-      rbEntity.addComponent(Button, { action: rbAction, surfaceY: 0.05, fullPressDistance: 0.02 });
-
-      var ebEntity = world.createEntity();
-      ebEntity.addComponent(Pressable);
-      ebEntity.addComponent(Object3D, { object: exitButton });
-      let ebAction = function () {
-        exitText.visible = true;
-        setTimeout(function () { exitText.visible = false; renderer.xr.getSession().end(); }, 2000);
-      };
-      ebEntity.addComponent(Button, { action: ebAction, surfaceY: 0.05, recoverySpeed: 0.2, fullPressDistance: 0.03 });
-
-      var tkEntity = world.createEntity();
-      tkEntity.addComponent(Rotating);
-      tkEntity.addComponent(Object3D, { object: torusKnot });
-
-      var itEntity = world.createEntity();
-      itEntity.addComponent(HandsInstructionText);
-      itEntity.addComponent(Object3D, { object: instructionText });
-
-      window.addEventListener('resize', onWindowResize);
-
-    }
-
-    function onWindowResize() {
-
-      camera.aspect = window.innerWidth / window.innerHeight;
-      camera.updateProjectionMatrix();
-
-      renderer.setSize(window.innerWidth, window.innerHeight);
-
-    }
-
-    function animate() {
-
-      renderer.setAnimationLoop(render);
-
-    }
-
-    function render() {
-      var delta = clock.getDelta();
-      var elapsedTime = clock.elapsedTime;
-      world.execute(delta, elapsedTime);
-      renderer.render(scene, camera);
-    }
-
-  </script>
+	<div id="info">
+		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>	vr - handinput - press button<br />
+		(Oculus Browser 15.1+)
+	</div>
+
+	<script type="module">
+
+		import * as THREE from '../build/three.module.js';
+		import { VRButton } from './jsm/webxr/VRButton.js';
+		import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
+		import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
+		import { createText } from './jsm/webxr/Text2D.js';
+
+		import { World, System, Component, TagComponent, Types } from './jsm/libs/ecsy.module.js';
+
+		class Object3D extends Component { }
+
+		Object3D.schema = {
+			object: { type: Types.Ref }
+		};
+
+		class Button extends Component { }
+
+		Button.schema = {
+			// button states: [resting, pressed, fully_pressed, recovering]
+			currState: { type: Types.String, default: 'resting' },
+			prevState: { type: Types.String, default: 'resting' },
+			pressSound: { type: Types.Ref, default: null },
+			releaseSound: { type: Types.Ref, default: null },
+			restingY: { type: Types.Number, default: null },
+			surfaceY: { type: Types.Number, default: null },
+			recoverySpeed: { type: Types.Number, default: 0.4 },
+			fullPressDistance: { type: Types.Number, default: null },
+			action: { type: Types.Ref, default: () => { } }
+		};
+
+		class ButtonSystem extends System {
+
+			init( attributes ) {
+
+				this.renderer = attributes.renderer;
+				this.soundAdded = false;
+
+			}
+
+			execute( /*delta, time*/ ) {
+
+				let buttonPressSound, buttonReleaseSound;
+				if ( this.renderer.xr.getSession() && ! this.soundAdded ) {
+
+					const xrCamera = this.renderer.xr.getCamera( camera );
+
+					const listener = new THREE.AudioListener();
+					xrCamera.add( listener );
+
+					// create a global audio source
+					buttonPressSound = new THREE.Audio( listener );
+					buttonReleaseSound = new THREE.Audio( listener );
+
+					// load a sound and set it as the Audio object's buffer
+					const audioLoader = new THREE.AudioLoader();
+					audioLoader.load( 'sounds/button-press.ogg', function ( buffer ) {
+
+						buttonPressSound.setBuffer( buffer );
+
+					} );
+					audioLoader.load( 'sounds/button-release.ogg', function ( buffer ) {
+
+						buttonReleaseSound.setBuffer( buffer );
+
+					} );
+					this.soundAdded = true;
+
+				}
+
+				this.queries.buttons.results.forEach( entity => {
+
+					const button = entity.getMutableComponent( Button );
+					const buttonMesh = entity.getComponent( Object3D ).object;
+					// populate restingY
+					if ( button.restingY == null ) {
+
+						button.restingY = buttonMesh.position.y;
+
+					}
+
+					if ( buttonPressSound ) {
+
+						button.pressSound = buttonPressSound;
+
+					}
+
+					if ( buttonReleaseSound ) {
+
+						button.releaseSound = buttonReleaseSound;
+
+					}
+
+					if ( button.currState == 'fully_pressed' && button.prevState != 'fully_pressed' ) {
+
+						button.pressSound?.play();
+						button.action();
+
+					}
+
+					if ( button.currState == 'recovering' && button.prevState != 'recovering' ) {
+
+						button.releaseSound?.play();
+
+					}
+
+					// preserve prevState, clear currState
+					// FingerInputSystem will update currState
+					button.prevState = button.currState;
+					button.currState = 'resting';
+
+				} );
+
+			}
+
+		}
+
+		ButtonSystem.queries = {
+			buttons: {
+				components: [ Button ]
+			}
+		};
+
+		class Pressable extends TagComponent { }
+
+		class FingerInputSystem extends System {
+
+			init( attributes ) {
+
+				this.hands = attributes.hands;
+
+			}
+
+			execute( delta/*, time*/ ) {
+
+				this.queries.pressable.results.forEach( entity => {
+
+					const button = entity.getMutableComponent( Button );
+					const object = entity.getComponent( Object3D ).object;
+					const pressingDistances = [];
+					this.hands.forEach( hand => {
+
+						if ( hand && hand.intersectBoxObject( object ) ) {
+
+							const pressingPosition = hand.getPointerPosition();
+							pressingDistances.push( button.surfaceY - object.worldToLocal( pressingPosition ).y );
+
+						}
+
+					} );
+					if ( pressingDistances.length == 0 ) { // not pressed this frame
+
+						if ( object.position.y < button.restingY ) {
+
+							object.position.y += button.recoverySpeed * delta;
+							button.currState = 'recovering';
+
+						} else {
+
+							object.position.y = button.restingY;
+							button.currState = 'resting';
+
+						}
+
+					} else {
+
+						button.currState = 'pressed';
+						const pressingDistance = Math.max( pressingDistances );
+						if ( pressingDistance > 0 ) {
+
+							object.position.y -= pressingDistance;
+
+						}
+
+						if ( object.position.y <= button.restingY - button.fullPressDistance ) {
+
+							button.currState = 'fully_pressed';
+							object.position.y = button.restingY - button.fullPressDistance;
+
+						}
+
+					}
+
+				} );
+
+			}
+
+		}
+
+		FingerInputSystem.queries = {
+			pressable: {
+				components: [ Pressable ]
+			}
+		};
+
+		class Rotating extends TagComponent { }
+
+		class RotatingSystem extends System {
+
+			execute( delta/*, time*/ ) {
+
+				this.queries.rotatingObjects.results.forEach( entity => {
+
+					const object = entity.getComponent( Object3D ).object;
+					object.rotation.x += 0.4 * delta;
+					object.rotation.y += 0.4 * delta;
+
+				} );
+
+			}
+
+		}
+
+		RotatingSystem.queries = {
+			rotatingObjects: {
+				components: [ Rotating ]
+			}
+		};
+
+		class HandsInstructionText extends TagComponent { }
+
+		class InstructionSystem extends System {
+
+			init( attributes ) {
+
+				this.controllers = attributes.controllers;
+
+			}
+
+			execute( /*delta, time*/ ) {
+
+				let visible = false;
+				this.controllers.forEach( controller => {
+
+					if ( controller.visible ) {
+
+						visible = true;
+
+					}
+
+				} );
+
+				this.queries.instructionTexts.results.forEach( entity => {
+
+					const object = entity.getComponent( Object3D ).object;
+					object.visible = visible;
+
+				} );
+
+			}
+
+		}
+
+		InstructionSystem.queries = {
+			instructionTexts: {
+				components: [ HandsInstructionText ]
+			}
+		};
+
+		class OffsetFromCamera extends Component { }
+
+		OffsetFromCamera.schema = {
+			x: { type: Types.Number, default: 0 },
+			y: { type: Types.Number, default: 0 },
+			z: { type: Types.Number, default: 0 },
+		};
+
+		class NeedCalibration extends TagComponent { }
+
+		class CalibrationSystem extends System {
+
+			init( attributes ) {
+
+				this.camera = attributes.camera;
+				this.renderer = attributes.renderer;
+
+			}
+
+			execute( /*delta, time*/ ) {
+
+				this.queries.needCalibration.results.forEach( entity => {
+
+					if ( this.renderer.xr.getSession() ) {
+
+						const offset = entity.getComponent( OffsetFromCamera );
+						const object = entity.getComponent( Object3D ).object;
+						const xrCamera = renderer.xr.getCamera( this.camera );
+						object.position.x = xrCamera.position.x + offset.x;
+						object.position.y = xrCamera.position.y + offset.y;
+						object.position.z = xrCamera.position.z + offset.z;
+						entity.removeComponent( NeedCalibration );
+
+					}
+
+				} );
+
+			}
+
+		}
+
+		CalibrationSystem.queries = {
+			needCalibration: {
+				components: [ NeedCalibration ]
+			}
+		};
+
+		const world = new World();
+		const clock = new THREE.Clock();
+		let camera, scene, renderer;
+
+		init();
+		animate();
+
+		function makeButtonMesh( x, y, z, color ) {
+
+			const geometry = new THREE.BoxGeometry( x, y, z );
+			const material = new THREE.MeshPhongMaterial( { color: color } );
+			const buttonMesh = new THREE.Mesh( geometry, material );
+			return buttonMesh;
+
+		}
+
+		function init() {
+
+			const container = document.createElement( 'div' );
+			document.body.appendChild( container );
+
+			scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0x444444 );
+
+			camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 10 );
+			camera.position.set( 0, 1.2, 0.3 );
+
+			scene.add( new THREE.HemisphereLight( 0x808080, 0x606060 ) );
+
+			const light = new THREE.DirectionalLight( 0xffffff );
+			light.position.set( 0, 6, 0 );
+			light.castShadow = true;
+			light.shadow.camera.top = 2;
+			light.shadow.camera.bottom = - 2;
+			light.shadow.camera.right = 2;
+			light.shadow.camera.left = - 2;
+			light.shadow.mapSize.set( 4096, 4096 );
+			scene.add( light );
+
+			renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			renderer.outputEncoding = THREE.sRGBEncoding;
+			renderer.shadowMap.enabled = true;
+			renderer.xr.enabled = true;
+
+			container.appendChild( renderer.domElement );
+
+			document.body.appendChild( VRButton.createButton( renderer ) );
+
+			// controllers
+			const controller1 = renderer.xr.getController( 0 );
+			scene.add( controller1 );
+
+			const controller2 = renderer.xr.getController( 1 );
+			scene.add( controller2 );
+
+			const controllerModelFactory = new XRControllerModelFactory();
+
+			// Hand 1
+			const controllerGrip1 = renderer.xr.getControllerGrip( 0 );
+			controllerGrip1.add( controllerModelFactory.createControllerModel( controllerGrip1 ) );
+			scene.add( controllerGrip1 );
+
+			const hand1 = renderer.xr.getHand( 0 );
+			const handModel1 = new OculusHandModel( hand1 );
+			hand1.add( handModel1 );
+			scene.add( hand1 );
+
+			// Hand 2
+			const controllerGrip2 = renderer.xr.getControllerGrip( 1 );
+			controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
+			scene.add( controllerGrip2 );
+
+			const hand2 = renderer.xr.getHand( 1 );
+			const handModel2 = new OculusHandModel( hand2 );
+			hand2.add( handModel2 );
+			scene.add( hand2 );
+
+
+			// buttons
+			const floorGeometry = new THREE.PlaneGeometry( 4, 4 );
+			const floorMaterial = new THREE.MeshPhongMaterial( { color: 0x222222 } );
+			const floor = new THREE.Mesh( floorGeometry, floorMaterial );
+			floor.rotation.x = - Math.PI / 2;
+			scene.add( floor );
+
+			const consoleGeometry = new THREE.BoxGeometry( 0.5, 0.12, 0.15 );
+			const consoleMaterial = new THREE.MeshPhongMaterial( { color: 0x595959 } );
+			const consoleMesh = new THREE.Mesh( consoleGeometry, consoleMaterial );
+			consoleMesh.position.set( 0, 1, - 0.3 );
+			scene.add( consoleMesh );
+
+			const orangeButton = makeButtonMesh( 0.08, 0.1, 0.08, 0xffd3b5 );
+			orangeButton.position.set( - 0.15, 0.04, 0 );
+			consoleMesh.add( orangeButton );
+
+			const pinkButton = makeButtonMesh( 0.08, 0.1, 0.08, 0xe84a5f );
+			pinkButton.position.set( - 0.05, 0.04, 0 );
+			consoleMesh.add( pinkButton );
+
+			const resetButton = makeButtonMesh( 0.08, 0.1, 0.08, 0x355c7d );
+			const resetButtonText = createText( 'reset', 0.03 );
+			resetButton.add( resetButtonText );
+			resetButtonText.rotation.x = - Math.PI / 2;
+			resetButtonText.position.set( 0, 0.051, 0 );
+			resetButton.position.set( 0.05, 0.04, 0 );
+			consoleMesh.add( resetButton );
+
+			const exitButton = makeButtonMesh( 0.08, 0.1, 0.08, 0xff0000 );
+			const exitButtonText = createText( 'exit', 0.03 );
+			exitButton.add( exitButtonText );
+			exitButtonText.rotation.x = - Math.PI / 2;
+			exitButtonText.position.set( 0, 0.051, 0 );
+			exitButton.position.set( 0.15, 0.04, 0 );
+			consoleMesh.add( exitButton );
+
+			const tkGeometry = new THREE.TorusKnotGeometry( 0.5, 0.2, 200, 32 );
+			const tkMaterial = new THREE.MeshPhongMaterial( { color: 0xffffff } );
+			tkMaterial.metalness = 0.8;
+			const torusKnot = new THREE.Mesh( tkGeometry, tkMaterial );
+			torusKnot.position.set( 0, 1, - 5 );
+			scene.add( torusKnot );
+
+			const instructionText = createText( 'This is a WebXR Hands demo, please explore with hands.', 0.04 );
+			instructionText.position.set( 0, 1.6, - 0.6 );
+			scene.add( instructionText );
+
+			const exitText = createText( 'Exiting session...', 0.04 );
+			exitText.position.set( 0, 1.5, - 0.6 );
+			exitText.visible = false;
+			scene.add( exitText );
+
+			world
+				.registerComponent( Object3D )
+				.registerComponent( Button )
+				.registerComponent( Pressable )
+				.registerComponent( Rotating )
+				.registerComponent( HandsInstructionText )
+				.registerComponent( OffsetFromCamera )
+				.registerComponent( NeedCalibration );
+
+			world
+				.registerSystem( RotatingSystem )
+				.registerSystem( InstructionSystem, { controllers: [ controllerGrip1, controllerGrip2 ] } )
+				.registerSystem( CalibrationSystem, { renderer: renderer, camera: camera } )
+				.registerSystem( ButtonSystem, { renderer: renderer, camera: camera } )
+				.registerSystem( FingerInputSystem, { hands: [ handModel1, handModel2 ] } );
+
+			const csEntity = world.createEntity();
+			csEntity.addComponent( OffsetFromCamera, { x: 0, y: - 0.4, z: - 0.3 } );
+			csEntity.addComponent( NeedCalibration );
+			csEntity.addComponent( Object3D, { object: consoleMesh } );
+
+			const obEntity = world.createEntity();
+			obEntity.addComponent( Pressable );
+			obEntity.addComponent( Object3D, { object: orangeButton } );
+			const obAction = function () {
+
+				torusKnot.material.color.setHex( 0xffd3b5 );
+
+			};
+
+			obEntity.addComponent( Button, { action: obAction, surfaceY: 0.05, fullPressDistance: 0.02 } );
+
+			const pbEntity = world.createEntity();
+			pbEntity.addComponent( Pressable );
+			pbEntity.addComponent( Object3D, { object: pinkButton } );
+			const pbAction = function () {
+
+				torusKnot.material.color.setHex( 0xe84a5f );
+
+			};
+
+			pbEntity.addComponent( Button, { action: pbAction, surfaceY: 0.05, fullPressDistance: 0.02 } );
+
+			const rbEntity = world.createEntity();
+			rbEntity.addComponent( Pressable );
+			rbEntity.addComponent( Object3D, { object: resetButton } );
+			const rbAction = function () {
+
+				torusKnot.material.color.setHex( 0xffffff );
+
+			};
+
+			rbEntity.addComponent( Button, { action: rbAction, surfaceY: 0.05, fullPressDistance: 0.02 } );
+
+			const ebEntity = world.createEntity();
+			ebEntity.addComponent( Pressable );
+			ebEntity.addComponent( Object3D, { object: exitButton } );
+			const ebAction = function () {
+
+				exitText.visible = true;
+				setTimeout( function () {
+
+					exitText.visible = false; renderer.xr.getSession().end();
+
+				}, 2000 );
+
+			};
+
+			ebEntity.addComponent( Button, { action: ebAction, surfaceY: 0.05, recoverySpeed: 0.2, fullPressDistance: 0.03 } );
+
+			const tkEntity = world.createEntity();
+			tkEntity.addComponent( Rotating );
+			tkEntity.addComponent( Object3D, { object: torusKnot } );
+
+			const itEntity = world.createEntity();
+			itEntity.addComponent( HandsInstructionText );
+			itEntity.addComponent( Object3D, { object: instructionText } );
+
+			window.addEventListener( 'resize', onWindowResize );
+
+		}
+
+		function onWindowResize() {
+
+			camera.aspect = window.innerWidth / window.innerHeight;
+			camera.updateProjectionMatrix();
+
+			renderer.setSize( window.innerWidth, window.innerHeight );
+
+		}
+
+		function animate() {
+
+			renderer.setAnimationLoop( render );
+
+		}
+
+		function render() {
+
+			const delta = clock.getDelta();
+			const elapsedTime = clock.elapsedTime;
+			world.execute( delta, elapsedTime );
+			renderer.render( scene, camera );
+
+		}
+
+	</script>
 </body>
 </body>
 
 
 </html>
 </html>