From 013942090da53b20387351877da73c3dcf9d4044 Mon Sep 17 00:00:00 2001
From: Kevin Lee <kevin@infinite-lee.com>
Date: Tue, 24 Apr 2018 16:05:35 -0700
Subject: [PATCH] a bunch of different tweaks and polishes for interactables
 and the duck

---
 src/components/controls-shape-offset.js       | 45 +++++++++++++++++++
 src/components/cursor-controller.js           | 30 ++++++++-----
 src/components/duck.js                        | 40 +++++++++++++++++
 src/components/hand-controls2.js              |  2 +-
 src/components/haptic-feedback.js             | 21 ++++++---
 .../super-networked-interactable.js           | 13 ++++++
 src/components/super-spawner.js               | 21 ++++++++-
 src/hub.html                                  | 32 +++++++++----
 src/hub.js                                    |  2 +
 9 files changed, 178 insertions(+), 28 deletions(-)
 create mode 100644 src/components/controls-shape-offset.js
 create mode 100644 src/components/duck.js

diff --git a/src/components/controls-shape-offset.js b/src/components/controls-shape-offset.js
new file mode 100644
index 000000000..7ce876433
--- /dev/null
+++ b/src/components/controls-shape-offset.js
@@ -0,0 +1,45 @@
+import { CONTROLLER_OFFSETS } from "./hand-controls2.js";
+
+AFRAME.registerComponent("controls-shape-offset", {
+  schema: {
+    additionalOffset: { default: { x: 0, y: -0.03, z: -0.04 } }
+  },
+  init: function() {
+    this.controller = null;
+    this.shapeAdded = false;
+
+    this._handleControllerConnected = this._handleControllerConnected.bind(this);
+    this.el.addEventListener("controllerconnected", this._handleControllerConnected);
+  },
+
+  remove: function() {
+    this.el.removeEventListener("controllerconnected", this._handleControllerConnected);
+  },
+
+  tick: function() {
+    if (!this.shapeAdded && this.controller) {
+      this.shapeAdded = true;
+      const hasOffset = CONTROLLER_OFFSETS.hasOwnProperty(this.controller);
+      const offset = hasOffset ? CONTROLLER_OFFSETS[this.controller] : CONTROLLER_OFFSETS.default;
+      const position = new THREE.Vector3();
+      const quaternion = new THREE.Quaternion();
+      const scale = new THREE.Vector3();
+      offset.decompose(position, quaternion, scale);
+      position.add(this.data.additionalOffset);
+      quaternion.conjugate();
+
+      const shape = {
+        shape: "sphere",
+        radius: "0.02",
+        orientation: quaternion,
+        offset: position
+      };
+
+      this.el.setAttribute("shape", shape);
+    }
+  },
+
+  _handleControllerConnected: function(e) {
+    this.controller = e.detail.name;
+  }
+});
diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
index 572b17d91..f17be4e82 100644
--- a/src/components/cursor-controller.js
+++ b/src/components/cursor-controller.js
@@ -10,6 +10,7 @@ AFRAME.registerComponent("cursor-controller", {
     camera: { type: "selector" },
     playerRig: { type: "selector" },
     gazeTeleportControls: { type: "selector" },
+    haptic: { type: "selector" },
     physicalHandSelector: { type: "string" },
     handedness: { default: "right", oneOf: ["right", "left"] },
     maxDistance: { default: 3 },
@@ -78,16 +79,13 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   play: function() {
-    if (!this.inVR && this.isMobile && !this.hasPointingDevice) {
-      document.addEventListener("touchstart", this._handleTouchStart);
-      document.addEventListener("touchmove", this._handleTouchMove);
-      document.addEventListener("touchend", this._handleTouchEnd);
-    } else {
-      document.addEventListener("mousedown", this._handleMouseDown);
-      document.addEventListener("mousemove", this._handleMouseMove);
-      document.addEventListener("mouseup", this._handleMouseUp);
-      document.addEventListener("wheel", this._handleWheel);
-    }
+    document.addEventListener("touchstart", this._handleTouchStart);
+    document.addEventListener("touchmove", this._handleTouchMove);
+    document.addEventListener("touchend", this._handleTouchEnd);
+    document.addEventListener("mousedown", this._handleMouseDown);
+    document.addEventListener("mousemove", this._handleMouseMove);
+    document.addEventListener("mouseup", this._handleMouseUp);
+    document.addEventListener("wheel", this._handleWheel);
 
     window.addEventListener("enter-vr", this._handleEnterVR);
     window.addEventListener("exit-vr", this._handleExitVR);
@@ -258,6 +256,8 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleTouchStart: function(e) {
+    if (!this.isMobile || this.hasPointingDevice) return;
+
     const touch = e.touches[0];
     if (touch.clientY / window.innerHeight >= 0.8) return true;
     this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
@@ -303,6 +303,8 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleTouchMove: function(e) {
+    if (!this.isMobile || this.hasPointingDevice) return;
+
     for (let i = 0; i < e.touches.length; i++) {
       const touch = e.touches[i];
       if (touch.clientY / window.innerHeight >= 0.8) return true;
@@ -312,6 +314,8 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleTouchEnd: function(e) {
+    if (!this.isMobile || this.hasPointingDevice) return;
+
     for (let i = 0; i < e.changedTouches.length; i++) {
       const touch = e.changedTouches[i];
       const thisTouchDidNotDriveMousePos =
@@ -326,6 +330,8 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleMouseDown: function() {
+    if (this.isMobile && !this.hasPointingDevice) return;
+
     if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) {
       this._setLookControlsEnabled(false);
       this.data.cursor.emit("cursor-grab", {});
@@ -335,10 +341,14 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleMouseMove: function(e) {
+    if (this.isMobile && !this.hasPointingDevice) return;
+
     this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
   },
 
   _handleMouseUp: function() {
+    if (this.isMobile && !this.hasPointingDevice) return;
+
     this._setLookControlsEnabled(true);
     this.data.cursor.emit("cursor-release", {});
     this._endTeleport();
diff --git a/src/components/duck.js b/src/components/duck.js
new file mode 100644
index 000000000..50f24de70
--- /dev/null
+++ b/src/components/duck.js
@@ -0,0 +1,40 @@
+AFRAME.registerComponent("duck", {
+  schema: {
+    initialForce: { default: 0 },
+    maxForce: { default: 6.5 },
+    maxScale: { default: 5 }
+  },
+
+  init: function() {
+    this.system = this.el.sceneEl.systems.physics;
+    this.hasBody = false;
+    this.position = new CANNON.Vec3();
+    this.force = new CANNON.Vec3(0, this.data.initialForce, 0);
+    this.initialScale = this.el.object3D.scale.x;
+    this.maxScale = this.data.maxScale * this.initialScale;
+  },
+
+  play: function() {
+    this.system.addComponent(this);
+  },
+
+  pause: function() {
+    this.system.removeComponent(this);
+  },
+
+  beforeStep: function() {
+    if (this.el.body) {
+      const currentScale = this.el.object3D.scale.x;
+      const ratio = Math.min(1, (currentScale - this.initialScale) / (this.maxScale - this.initialScale));
+      const force = ratio * this.data.maxForce;
+      if (force > 0) {
+        const angle = Math.random() * Math.PI * 2;
+        const x = Math.cos(angle);
+        const z = Math.sin(angle);
+        this.force.set(x, force, z);
+        this.position.set(x * 0.01, 0, z * 0.01);
+        this.el.body.applyForce(this.force, this.position);
+      }
+    }
+  }
+});
diff --git a/src/components/hand-controls2.js b/src/components/hand-controls2.js
index af86cd5da..4eef8a499 100644
--- a/src/components/hand-controls2.js
+++ b/src/components/hand-controls2.js
@@ -10,7 +10,7 @@ const POSES = {
   mrpDown: "mrpDown"
 };
 
-const CONTROLLER_OFFSETS = {
+export const CONTROLLER_OFFSETS = {
   default: new THREE.Matrix4(),
   "oculus-touch-controls": new THREE.Matrix4().makeTranslation(0, -0.015, 0.04),
   "vive-controls": new THREE.Matrix4().compose(
diff --git a/src/components/haptic-feedback.js b/src/components/haptic-feedback.js
index 35a79cd95..d7db51d5e 100644
--- a/src/components/haptic-feedback.js
+++ b/src/components/haptic-feedback.js
@@ -10,7 +10,7 @@ AFRAME.registerComponent("haptic-feedback", {
   },
 
   init: function() {
-    this.pulse = this.pulse.bind(this);
+    this.handlePulse = this.handlePulse.bind(this);
     this.getActuator = this.getActuator.bind(this);
     this.getActuator().then(actuator => {
       this.actuator = actuator;
@@ -37,21 +37,28 @@ AFRAME.registerComponent("haptic-feedback", {
   },
 
   play: function() {
-    this.el.addEventListener(this.data.hapticEventName, this.pulse);
+    this.el.addEventListener(this.data.hapticEventName, this.handlePulse);
   },
   pause: function() {
-    this.el.removeEventListener(this.data.hapticEventName, this.pulse);
+    this.el.removeEventListener(this.data.hapticEventName, this.handlePulse);
   },
 
-  pulse: function(event) {
+  handlePulse: function(event) {
     const { intensity } = event.detail;
-    if (!strengthForIntensity[intensity]) {
+    let strength;
+
+    if (strengthForIntensity[intensity]) {
+      this.pulse(strengthForIntensity[intensity]);
+    } else if (Number(intensity) === intensity) {
+      this.pulse(intensity);
+    } else {
       console.warn(`Invalid intensity : ${intensity}`);
-      return;
     }
+  },
 
+  pulse: function(intensity) {
     if (this.actuator) {
-      this.actuator.pulse(strengthForIntensity[intensity], 15);
+      this.actuator.pulse(intensity, 15);
     }
   }
 });
diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js
index a5baed53d..0d9fc26de 100644
--- a/src/components/super-networked-interactable.js
+++ b/src/components/super-networked-interactable.js
@@ -1,10 +1,12 @@
 AFRAME.registerComponent("super-networked-interactable", {
   schema: {
     mass: { default: 1 },
+    hapticsMassVelocityFactor: { default: 0.1 },
     counter: { type: "selector" }
   },
 
   init: function() {
+    this.system = this.el.sceneEl.systems.physics;
     this.counter = this.data.counter.components["networked-counter"];
     this.hand = null;
 
@@ -21,12 +23,23 @@ AFRAME.registerComponent("super-networked-interactable", {
     this.ownershipLostListener = this._onOwnershipLost.bind(this);
     this.el.addEventListener("grab-start", this.grabStartListener);
     this.el.addEventListener("ownership-lost", this.ownershipLostListener);
+    this.system.addComponent(this);
   },
 
   remove: function() {
     this.counter.deregister(this.el);
     this.el.removeEventListener("grab-start", this.grabStartListener);
     this.el.removeEventListener("ownership-lost", this.ownershipLostListener);
+    this.system.removeComponent(this);
+  },
+
+  afterStep: function() {
+    if (this.el.is("grabbed") && this.hand && this.hand.components.hasOwnProperty("haptic-feedback")) {
+      const hapticFeedback = this.hand.components["haptic-feedback"];
+      let velocity = this.el.body.velocity.lengthSquared() * this.el.body.mass * this.data.hapticsMassVelocityFactor;
+      velocity = Math.min(1, velocity);
+      hapticFeedback.pulse(velocity);
+    }
   },
 
   _onGrabStart: function(e) {
diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js
index 9d36c6583..27fee415c 100644
--- a/src/components/super-spawner.js
+++ b/src/components/super-spawner.js
@@ -3,11 +3,13 @@ AFRAME.registerComponent("super-spawner", {
     template: { default: "" },
     useCustomSpawnPosition: { default: false },
     spawnPosition: { type: "vec3" },
-    events: { default: ["cursor-grab", "action_grab"] }
+    events: { default: ["cursor-grab", "action_grab"] },
+    spawnCooldown: { default: 1 }
   },
 
   init: function() {
     this.entities = new Map();
+    this.timeout = null;
   },
 
   play: function() {
@@ -17,19 +19,26 @@ AFRAME.registerComponent("super-spawner", {
 
   pause: function() {
     this.el.removeEventListener("grab-start", this.handleGrabStart);
+
+    clearTimeout(this.timeout);
+    this.timeout = null;
+    this.el.setAttribute("visible", true);
   },
 
   remove: function() {
     for (const entity of this.entities.keys()) {
       const data = this.entities.get(entity);
       entity.removeEventListener("componentinitialized", data.componentinInitializedListener);
-      entity.removeEventListener("bodyloaded", data.bodyLoadedListener);
+      entity.removeEventListener("body-loaded", data.bodyLoadedListener);
     }
 
     this.entities.clear();
   },
 
   _handleGrabStart: function(e) {
+    if (this.timeout) {
+      return;
+    }
     const hand = e.detail.hand;
     const entity = document.createElement("a-entity");
 
@@ -51,6 +60,14 @@ AFRAME.registerComponent("super-spawner", {
     const pos = this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.getAttribute("position");
     entity.setAttribute("position", pos);
     this.el.sceneEl.appendChild(entity);
+
+    if (this.data.spawnCooldown > 0) {
+      this.el.setAttribute("visible", false);
+      this.timeout = setTimeout(() => {
+        this.el.setAttribute("visible", true);
+        this.timeout = null;
+      }, this.data.spawnCooldown * 1000);
+    }
   },
 
   _handleComponentInitialzed: function(entity, e) {
diff --git a/src/hub.html b/src/hub.html
index 6b759eb1a..9b47dcb37 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -21,7 +21,7 @@
 
     <a-scene
         networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;"
-        physics
+        physics="gravity: -6;"
         mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
         personal-space-bubble="debug: false;"
         >
@@ -89,6 +89,13 @@
                                 personal-space-invader="radius: 0.15; useMaterial: true;"
                                 bone-visibility
                             >
+                            <a-cylinder
+                                static-body
+                                radius="0.13"
+                                height="0.2"
+                                position="0 0.07 0.05"
+                                visible="false"
+                            ></a-cylinder>
                             </a-entity>
                         </template>
 
@@ -108,10 +115,11 @@
                     gltf-model-plus="src: #interactable-duck; inflate: true;"
                     scale="2 2 2"
                     class="interactable" 
-                    super-networked-interactable="counter: #counter; mass: 5;"
-                    body="type: dynamic; shape: none; mass: 5;"
+                    super-networked-interactable="counter: #counter; mass: 1;"
+                    body="type: dynamic; shape: none; mass: 1;"
                     grabbable
                     stretchable="useWorldPosition: true;"
+                    duck
                 ></a-entity>
             </template>
 
@@ -137,6 +145,7 @@
             super-spawner="template: #interactable-template;" 
             position="2.9 1.2 0" 
             body="mass: 0; type: static; shape: box;"
+            collision-filter="collisionForces: false;"
         ></a-entity>
 
         <a-entity
@@ -230,7 +239,11 @@
                     hitOpacity: 0.3;
                     missOpacity: 0.2;"
                 haptic-feedback
-            ></a-entity>
+                body="type: static; shape: none;"
+                mixin="super-hands"
+                controls-shape-offset
+            >
+            </a-entity>
 
             <a-entity
                 id="player-right-controller"
@@ -247,6 +260,9 @@
                     hitOpacity: 0.3;
                     missOpacity: 0.2;"
                 haptic-feedback
+                body="type: static; shape: none;"
+                mixin="super-hands"
+                controls-shape-offset
             ></a-entity>
 
             <a-entity gltf-model-plus="inflate: true;" class="model">
@@ -273,7 +289,7 @@
 
                 <template data-selector=".LeftHand">
                     <a-entity bone-visibility>
-                        <a-entity
+                        <!-- <a-entity
                             id="left-super-hand"
                             event-repeater="
                             events: action_grab, action_release, action_primary_down, action_primary_up; 
@@ -281,13 +297,13 @@
                             static-body="shape: sphere; sphereRadius: 0.02"
                             mixin="super-hands"
                             position="0 0.05 0"
-                        ></a-entity>
+                        ></a-entity> -->
                     </a-entity>
                 </template>
 
                 <template data-selector=".RightHand">
                     <a-entity bone-visibility>
-                        <a-entity
+<!--                         <a-entity
                             id="right-super-hand"
                             event-repeater="
                             events: action_grab, action_release, action_primary_down, action_primary_up; 
@@ -295,7 +311,7 @@
                             static-body="shape: sphere; sphereRadius: 0.02"
                             mixin="super-hands"
                             position="0 -0.05 0"
-                        ></a-entity>
+                        ></a-entity> -->
                     </a-entity>
                 </template>
 
diff --git a/src/hub.js b/src/hub.js
index a412bb4d9..4a3946382 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -83,6 +83,8 @@ import "./components/super-networked-interactable";
 import "./components/networked-counter";
 import "./components/super-spawner";
 import "./components/event-repeater";
+import "./components/controls-shape-offset";
+import "./components/duck";
 
 import "./components/cursor-controller";
 
-- 
GitLab