From fd8606f2b09ce0e5d4271fec3c3dda8579071709 Mon Sep 17 00:00:00 2001
From: Robert Long <robert@robertlong.me>
Date: Wed, 7 Feb 2018 16:27:35 -0800
Subject: [PATCH] Update a-gltf-entity and a-proxy-entity

---
 package.json                      |   2 +-
 src/components/bone-visibility.js |  15 +++
 src/elements/a-gltf-entity.js     | 204 ++++++++++++++++++++++++++++
 src/elements/a-proxy-entity.js    |  83 ++++++------
 src/network-schemas.js            |  46 +++++--
 src/room.js                       |  16 ++-
 templates/room.hbs                | 217 ++++++++++++++----------------
 yarn.lock                         |   6 +-
 8 files changed, 416 insertions(+), 173 deletions(-)
 create mode 100644 src/components/bone-visibility.js
 create mode 100644 src/elements/a-gltf-entity.js

diff --git a/package.json b/package.json
index 391833c24..90e41cfe8 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "material-design-lite": "^1.3.0",
     "minijanus": "^0.1.6",
     "naf-janus-adapter": "^0.1.10",
-    "networked-aframe": "https://github.com/netpro2k/networked-aframe#bugfix/chrome/audio",
+    "networked-aframe": "https://github.com/netpro2k/networked-aframe#feature/networked-templates-refactor",
     "nipplejs": "^0.6.7",
     "query-string": "^5.0.1",
     "raven-js": "^3.20.1",
diff --git a/src/components/bone-visibility.js b/src/components/bone-visibility.js
new file mode 100644
index 000000000..7e2dd7bf6
--- /dev/null
+++ b/src/components/bone-visibility.js
@@ -0,0 +1,15 @@
+AFRAME.registerComponent("bone-visibility", {
+  tick() {
+    const visible = this.el.getAttribute("visible");
+
+    if (this.lastVisible !== visible) {
+      if (visible) {
+        this.el.object3D.scale.set(1, 1, 1);
+      } else {
+        this.el.object3D.scale.set(0, 0, 0);
+      }
+
+      this.lastVisible = visible;
+    }
+  }
+});
diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js
new file mode 100644
index 000000000..cf0a2c632
--- /dev/null
+++ b/src/elements/a-gltf-entity.js
@@ -0,0 +1,204 @@
+const GLTFCache = {};
+
+// From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87
+// Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573
+function cloneGltf(gltf) {
+  const clone = {
+    animations: gltf.animations,
+    scene: gltf.scene.clone(true)
+  };
+
+  const skinnedMeshes = {};
+
+  gltf.scene.traverse(node => {
+    if (node.isSkinnedMesh) {
+      skinnedMeshes[node.name] = node;
+    }
+  });
+
+  const cloneBones = {};
+  const cloneSkinnedMeshes = {};
+
+  clone.scene.traverse(node => {
+    if (node.isBone) {
+      cloneBones[node.name] = node;
+    }
+
+    if (node.isSkinnedMesh) {
+      cloneSkinnedMeshes[node.name] = node;
+    }
+  });
+
+  for (const name in skinnedMeshes) {
+    const skinnedMesh = skinnedMeshes[name];
+    const skeleton = skinnedMesh.skeleton;
+    const cloneSkinnedMesh = cloneSkinnedMeshes[name];
+
+    const orderedCloneBones = [];
+
+    for (let i = 0; i < skeleton.bones.length; ++i) {
+      const cloneBone = cloneBones[skeleton.bones[i].name];
+      orderedCloneBones.push(cloneBone);
+    }
+
+    cloneSkinnedMesh.bind(
+      new THREE.Skeleton(orderedCloneBones, skeleton.boneInverses),
+      cloneSkinnedMesh.matrixWorld
+    );
+
+    cloneSkinnedMesh.material = skinnedMesh.material.clone();
+  }
+
+  return clone;
+}
+
+const inflateEntities = function(classPrefix, parentEl, node) {
+  // setObject3D mutates the node's parent, so we have to copy
+  const children = node.children.slice(0);
+
+  const el = document.createElement("a-entity");
+
+  // Remove invalid CSS class name characters.
+  const className = node.name.replace(/[^\w-]/g, "");
+  el.classList.add(classPrefix + className);
+  parentEl.appendChild(el);
+
+  // Copy over transform to the THREE.Group and reset the actual transform of the Object3D
+  el.setAttribute("position", {
+    x: node.position.x,
+    y: node.position.y,
+    z: node.position.z
+  });
+  el.setAttribute("rotation", {
+    x: node.rotation.x * THREE.Math.RAD2DEG,
+    y: node.rotation.y * THREE.Math.RAD2DEG,
+    z: node.rotation.z * THREE.Math.RAD2DEG
+  });
+  el.setAttribute("scale", {
+    x: node.scale.x,
+    y: node.scale.y,
+    z: node.scale.z
+  });
+
+  node.position.set(0, 0, 0);
+  node.rotation.set(0, 0, 0);
+  node.scale.set(1, 1, 1);
+
+  el.setObject3D(node.type.toLowerCase(), node);
+
+  children.forEach(childNode => {
+    inflateEntities(classPrefix, el, childNode);
+  });
+};
+
+function attachTemplate(templateEl) {
+  const selector = templateEl.getAttribute("data-selector");
+  const targetEls = document.querySelectorAll(selector);
+  const clone = document.importNode(templateEl.content, true);
+  const templateRoot = clone.firstElementChild;
+  const templateRootAttrs = templateRoot.attributes;
+
+  for (var i = 0; i < targetEls.length; i++) {
+    const targetEl = targetEls[i];
+
+    // Merge root element attributes with the target element
+    for (var i = 0; i < templateRootAttrs.length; i++) {
+      targetEl.setAttribute(
+        templateRootAttrs[i].name,
+        templateRootAttrs[i].value
+      );
+    }
+
+    // Append all child elements
+    for (var i = 0; i < templateRoot.children.length; i++) {
+      targetEl.appendChild(document.importNode(templateRoot.children[i], true));
+    }
+  }
+}
+
+AFRAME.registerElement("a-gltf-entity", {
+  prototype: Object.create(AFRAME.AEntity.prototype, {
+    load: {
+      value() {
+        if (this.hasLoaded || !this.parentEl) {
+          return;
+        }
+
+        // Get the src url.
+        let src = this.getAttribute("src");
+
+        // If the src attribute is a selector, get the url from the asset item.
+        if (src.charAt(0) === "#") {
+          const assetEl = document.getElementById(src.substring(1));
+          src = assetEl.getAttribute("src");
+        }
+
+        // Load the gltf model from the cache if it exists.
+        const gltf = GLTFCache[src];
+
+        if (gltf) {
+          // Use a cloned copy of the cached model.
+          const clonedGltf = cloneGltf(gltf);
+          this.onLoad(clonedGltf);
+          return;
+        }
+
+        const finalizeLoad = () => {
+          AFRAME.ANode.prototype.load.call(this, () => {
+            // Check if entity was detached while it was waiting to load.
+            if (!this.parentEl) {
+              return;
+            }
+
+            this.updateComponents();
+            if (this.isScene || this.parentEl.isPlaying) {
+              this.play();
+            }
+          });
+        };
+
+        const inflate = (model, callback) => {
+          inflateEntities("", this, model);
+          this.querySelectorAll(":scope > template").forEach(attachTemplate);
+          setTimeout(callback, 0);
+        };
+
+        const onLoad = gltfModel => {
+          if (!GLTFCache[this.data]) {
+            // Store a cloned copy of the gltf model.
+            GLTFCache[this.data] = cloneGltf(gltfModel);
+          }
+
+          this.model = gltfModel.scene || gltfModel.scenes[0];
+          this.model.animations = gltfModel.animations;
+
+          this.setObject3D("mesh", this.model);
+          this.emit("model-loaded", { format: "gltf", model: this.model });
+
+          if (this.getAttribute("inflate")) {
+            inflate(this.model, finalizeLoad);
+          } else {
+            finalizeLoad();
+          }
+        };
+
+        const onError = error => {
+          const message =
+            error && error.message
+              ? error.message
+              : "Failed to load glTF model";
+          console.warn(message);
+          this.emit("model-error", { format: "gltf", src: this.data });
+        };
+
+        // Otherwise load the new gltf model.
+        new THREE.GLTFLoader().load(
+          src,
+          onLoad,
+          undefined /* onProgress */,
+          onError
+        );
+      }
+    }
+  })
+});
diff --git a/src/elements/a-proxy-entity.js b/src/elements/a-proxy-entity.js
index c0e212df2..488c55fc6 100644
--- a/src/elements/a-proxy-entity.js
+++ b/src/elements/a-proxy-entity.js
@@ -1,56 +1,57 @@
-function getParentModelInflatorEl(el) {
-  let cur = el;
-
-  while (cur && !cur.hasAttribute("model-inflator")) {
-    cur = cur.parentNode;
-  }
-
-  if (cur.hasAttribute("model-inflator")) {
-    return cur;
-  }
-
-  return undefined;
-}
-
 AFRAME.registerElement("a-proxy-entity", {
   prototype: Object.create(HTMLElement.prototype, {
     attachedCallback: {
       value() {
-        this.inflatorEl = getParentModelInflatorEl(this);
-
-        if (!this.inflatorEl) {
-          throw new Error(
-            "a-proxy-entity could not find parent element with model-inflator component."
-          );
-        }
-
-        this.onModelLoad = () => {
-          setTimeout(() => {
-            const selector = this.getAttribute("selector");
-            const targetEls = this.inflatorEl.querySelectorAll(selector);
-            const attributeNames = this.getAttributeNames();
-
-            for (const attributeName of attributeNames) {
-              const attributeValue = this.getAttribute(attributeName);
-              if (AFRAME.components[attributeName] !== undefined) {
-                for (const el of targetEls) {
-                  el.setAttribute(attributeName, attributeValue);
-                }
-              }
+        const waitForEvent = this.getAttribute("wait-for-event");
+
+        const attachTemplate = () => {
+          const selector = this.getAttribute("selector");
+          const targetEls = this.parentNode.querySelectorAll(selector);
+
+          const template = this.firstElementChild;
+          const clone = document.importNode(template.content, true);
+          const templateRoot = clone.firstElementChild;
+          const templateRootAttrs = templateRoot.attributes;
+
+          for (var i = 0; i < targetEls.length; i++) {
+            const targetEl = targetEls[i];
+
+            // Merge root element attributes with the target element
+            for (var i = 0; i < elAttrs.length; i++) {
+              targetEl.setAttribute(
+                templateRootAttrs[i].name,
+                templateRootAttrs[i].value
+              );
             }
 
-            this.parentNode.removeChild(this);
-          }, 0);
+            // Append all child elements
+            for (var i = 0; i < templateRoot.children.length; i++) {
+              targetEl.appendChild(
+                document.importNode(templateRoot.children[i], true)
+              );
+            }
+          }
         };
 
-        this.inflatorEl.addEventListener("model-loaded", this.onModelLoad, {
-          once: true
-        });
+        if (waitForEvent != null) {
+          this.parentNode.addEventListener(waitForEvent, attachTemplate, {
+            once: true
+          });
+        } else {
+          attachTemplate();
+        }
       }
     },
     detachedCallback: {
       value() {
-        this.inflatorEl.removeEventListener("model-loaded", this.onModelLoad);
+        const waitForEvent = this.getAttribute("wait-for-event");
+
+        if (waitForEvent != null) {
+          this.parentNode.removeEventListener(
+            waitForEvent,
+            this.attachTemplate
+          );
+        }
       }
     }
   })
diff --git a/src/network-schemas.js b/src/network-schemas.js
index d93c552cb..e21109007 100644
--- a/src/network-schemas.js
+++ b/src/network-schemas.js
@@ -1,7 +1,41 @@
 function registerNetworkSchemas() {
   NAF.schemas.add({
-    template: "#nametag-template",
+    template: "#remote-avatar-template",
     components: [
+      "position",
+      "rotation",
+      {
+        selector: ".Head",
+        component: "position"
+      },
+      {
+        selector: ".Head",
+        component: "rotation"
+      },
+      {
+        selector: ".LeftHand",
+        component: "position"
+      },
+      {
+        selector: ".LeftHand",
+        component: "rotation"
+      },
+      {
+        selector: ".LeftHand",
+        component: "visible"
+      },
+      {
+        selector: ".RightHand",
+        component: "position"
+      },
+      {
+        selector: ".RightHand",
+        component: "rotation"
+      },
+      {
+        selector: ".RightHand",
+        component: "visible"
+      },
       {
         selector: ".nametag",
         component: "text",
@@ -10,16 +44,6 @@ function registerNetworkSchemas() {
     ]
   });
 
-  NAF.schemas.add({
-    template: "#right-hand-template",
-    components: ["position", "rotation", "visible"]
-  });
-
-  NAF.schemas.add({
-    template: "#left-hand-template",
-    components: ["position", "rotation", "visible"]
-  });
-
   NAF.schemas.add({
     template: "#video-template",
     components: ["position", "rotation", "visible"]
diff --git a/src/room.js b/src/room.js
index ae0406936..e4fddbbf3 100644
--- a/src/room.js
+++ b/src/room.js
@@ -36,9 +36,11 @@ import "./components/layers";
 import "./components/spawn-controller";
 import "./components/model-inflator";
 import "./components/spin";
+import "./components/bone-visibility";
 
 import "./systems/personal-space-bubble";
 
+import "./elements/a-gltf-entity";
 import "./elements/a-proxy-entity";
 
 import { promptForName, getCookie, parseJwt } from "./utils";
@@ -98,8 +100,9 @@ window.App = {
       scene.setAttribute("stats", true);
     }
 
+    const playerRig = document.querySelector("#player-rig");
+
     if (AFRAME.utils.device.isMobile() || qs.gamepad) {
-      const playerRig = document.querySelector("#player-rig");
       playerRig.setAttribute("virtual-gamepad-controls", {});
     }
 
@@ -117,8 +120,15 @@ window.App = {
       username = promptForName(username); // promptForName is blocking
     }
 
-    const myNametag = document.querySelector("#player-rig .nametag");
-    myNametag.setAttribute("text", "value", username);
+    playerRig.addEventListener(
+      "model-loaded",
+      () => {
+        console.log(playerRig);
+        const myNametag = playerRig.querySelector(".nametag");
+        myNametag.setAttribute("text", "value", username);
+      },
+      { once: true }
+    );
 
     scene.addEventListener("action_share_screen", shareScreen);
 
diff --git a/templates/room.hbs b/templates/room.hbs
index 12b7d2b41..41efd401b 100644
--- a/templates/room.hbs
+++ b/templates/room.hbs
@@ -45,11 +45,6 @@
 
         <a-assets>
             <a-asset-item id="bot-skinned-mesh" response-type="arraybuffer" src="{{asset "assets/avatars/Bot_Skinned.glb" }}"></a-asset-item>
-            <a-asset-item id="bot-head-mesh" response-type="arraybuffer" src="{{asset "assets/avatars/Bot_Head_Mesh.glb" }}"></a-asset-item>
-            <a-asset-item id="bot-body-mesh" response-type="arraybuffer" src="{{asset "assets/avatars/Bot_Body_Mesh.glb" }}"></a-asset-item>
-            <a-asset-item id="bot-left-hand-mesh" response-type="arraybuffer" src="{{asset "assets/avatars/Bot_LeftHand_Mesh.glb" }}"></a-asset-item>
-            <a-asset-item id="bot-right-hand-mesh" response-type="arraybuffer" src="{{asset "assets/avatars/Bot_RightHand_Mesh.glb"}}"></a-asset-item>
-
             <a-asset-item id="watch-model" response-type="arraybuffer" src="{{asset "assets/hud/watch.glb"}}"></a-asset-item>
 
             <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="{{asset "assets/environments/MeetingSpace1_mesh.glb"}}"></a-asset-item>
@@ -60,129 +55,123 @@
             <img id="water-normal-map" src="{{asset "assets/waternormals.jpg"}}"></a-asset-item>
 
             <!-- Templates -->
-            <script id="head-template" type="text/html">
-                <a-entity
-                    class="head"
-                    networked-audio-source
-                    networked-audio-analyser
-                    matcolor-audio-feedback="objectName: Head_Mesh"
-                    cached-gltf-model="#bot-head-mesh"
-                    scale-audio-feedback
-                    personal-space-invader
-                    rotation="0 180 0"
-                    animation-mixer
-                ></a-entity>
-            </script>
 
-            <script id="body-template" type="text/html">
-                <a-entity
-                    class="body"
-                    cached-gltf-model="#bot-body-mesh"
-                    personal-space-invader
-                    rotation="0 180 0"
-                    position="0 -0.05 0"
-                ></a-entity>
-            </script>
-
-            <script id="left-hand-template" type="text/html">
-                <a-entity
-                    class="hand"
-                    cached-gltf-model="#bot-left-hand-mesh"
-                    personal-space-invader
-                    rotation="-90 90 0"
-                    position="0 0 0.075"
-                ></a-entity>
-            </script>
-
-            <script id="right-hand-template" type="text/html">
-                <a-entity
-                    class="hand"
-                    cached-gltf-model="#bot-right-hand-mesh"
-                    personal-space-invader
-                    rotation="-90 -90 0"
-                    position="0 0 0.075"
-                ></a-entity>
-            </script>
-
-            <script id="video-template" type="text/html">
+            <template id="video-template">
                 <a-entity class="video" geometry="primitive: plane;" material="side: double" networked-video-player></a-entity>
-            </script>
+            </template>
 
-            <script id="nametag-template" type="text/html">
+            <template id="remote-avatar-template">
                 <a-entity
-                    class="nametag"
-                    nametag-transform="follow: .head"
-                    text="side: double; align: center; color: #ddd"
-                    position="0 2.5 0"
-                    scale="6 6 6"
-                ></a-entity>
-            </script>
+                    cached-gltf-model="#bot-skinned-mesh"
+                >
+                    <a-proxy-entity selector=".Head" wait-for-event="model-loaded">
+                        <template>
+                            <a-entity
+                                networked-audio-source
+                                networked-audio-analyser
+                                matcolor-audio-feedback="objectName: Head_Mesh"
+                                scale-audio-feedback
+                                personal-space-invader
+                                animation-mixer
+                            >
+                                <a-entity
+                                    class="nametag"
+                                    nametag-transform="follow: .Head"
+                                    text="side: double; align: center; color: #ddd"
+                                    position="0 2.5 0"
+                                    scale="6 6 6"
+                                ></a-entity>
+                            </a-entity>
+                        </template>
+                    </a-proxy-entity>
+
+                    <a-proxy-entity selector=".Body" wait-for-event="model-loaded">
+                        <template>
+                            <a-entity body-controller="eyeNeckOffset: 0 -0.11 0.09; neckHeight: 0.05" ></a-entity>
+                        </template>
+                    </a-proxy-entity>
+
+                    <a-proxy-entity selector=".Lefthand" wait-for-event="model-loaded">
+                        <template>
+                            <a-entity personal-space-invader bone-visibility ></a-entity>
+                        </template>
+                    </a-proxy-entity>
+
+                    <a-proxy-entity selector=".RightHand" wait-for-event="model-loaded">
+                        <template>
+                            <a-entity personal-space-invader bone-visibility ></a-entity>
+                        </template>
+                    </a-proxy-entity>
+                </a-entity>
+            </template>
         </a-assets>
 
         <!-- Player Rig -->
-        <a-entity
+        <a-gltf-entity
             id="player-rig"
-            networked
+            src="#bot-skinned-mesh"
+            inflate="true"
+            networked="template: #remote-avatar-template; attachLocalTemplate: false;"
             spawn-controller="radius: 4;"
             wasd-to-analog2d
             character-controller="pivot: #head"
         >
-            <a-entity
-                id="head"
-                camera="userHeight: 1.6"
-                personal-space-bubble
-                look-controls
-                networked="template: #head-template; showLocalTemplate: false;"
-            ></a-entity>
-
-            <a-entity
-                id="body"
-                body-controller="eyeNeckOffset: 0 -0.11 0.09; neckHeight: 0.05"
-                networked="template: #body-template;"
-            ></a-entity>
-            <a-entity
-                id="nametag"
-                networked="template: #nametag-template; showLocalTemplate: false;"
-            ></a-entity>
-
-            <a-entity
-                id="left-hand"
-                hand-controls2="left"
-                tracked-controls
-                haptic-feedback
-                teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; button: action_teleport_"
-                networked="template: #left-hand-template;"
-            >
+            <template data-selector=".Head">
                 <a-entity
-                    id="watch"
-                    cached-gltf-model="#watch-model"
-                    bone-mute-state-indicator
-                    position="-0.003 0.009 0.085"
-                    rotation="-79.12547150756669 -160.1417037390651 -100.1530225888679"
-                    scale="1.5 1.5 1.5">
+                    id="head"
+                    camera="userHeight: 1.6; active: true;"
+                    personal-space-bubble
+                    look-controls
+                    visible="false"
+                >
+                    <a-entity
+                        class="nametag"
+                        nametag-transform="follow: .Head"
+                        text="side: double; align: center; color: #ddd"
+                        position="0 2.5 0"
+                        scale="6 6 6"
+                    ></a-entity>
                 </a-entity>
-            </a-entity>
-
-            <a-entity
-                id="right-hand"
-                hand-controls2="right"
-                haptic-feedback
-                teleport-controls="cameraRig: #player-rig;
-                                    teleportOrigin: #head;
-                                    hitEntity: #telepor-indicator;
-                                    button: action_teleport_;"
-                networked="template: #right-hand-template;"
-            ></a-entity>
-        </a-entity>
-
-         <a-entity
-            id="bot-skinned"
-            cached-gltf-model="#bot-skinned-mesh"
-            position="0 0 0"
-            model-inflator
-        >
-            <a-proxy-entity selector=".RightHand" spin ></a-proxy-entity>
-        </a-entity>
+            </template>
+
+            <template data-selector=".Chest">
+                <a-entity
+                    id="body"
+                    body-controller="eyeNeckOffset: 0 -0.11 0.09; neckHeight: 0.05"
+                ></a-entity>
+            </template>
+
+            <template data-selector=".LeftHand">
+                <a-entity
+                    id="left-hand"
+                    hand-controls2="left"
+                    tracked-controls
+                    haptic-feedback
+                    teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; button: action_teleport_"
+                    bone-visibility
+                >
+                    <a-entity
+                        id="watch"
+                        cached-gltf-model="#watch-model"
+                        bone-mute-state-indicator
+                        position="-0.003 0.009 0.085"
+                        rotation="-79.12547150756669 -160.1417037390651 -100.1530225888679"
+                        scale="1.5 1.5 1.5"
+                    >
+                    </a-entity>
+                </a-entity>
+            </template>
+
+            <template data-selector=".RightHand">
+                <a-entity
+                    id="right-hand"
+                    hand-controls2="right"
+                    haptic-feedback
+                    teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; button: action_teleport_;"
+                    bone-visibility
+                ></a-entity>
+            </template>
+        </a-gltf-entity>
 
         <!-- Environment -->
         <a-entity
diff --git a/yarn.lock b/yarn.lock
index 8ad070c0c..ed2a8c159 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3813,9 +3813,9 @@ negotiator@0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
 
-"networked-aframe@https://github.com/netpro2k/networked-aframe#bugfix/chrome/audio":
-  version "0.3.2"
-  resolved "https://github.com/netpro2k/networked-aframe#29efff590bc2edded4424203831ecc4671b1be69"
+"networked-aframe@https://github.com/netpro2k/networked-aframe#feature/networked-templates-refactor":
+  version "0.5.1"
+  resolved "https://github.com/netpro2k/networked-aframe#32627650faa088593600045bdafb717ed5a07d51"
   dependencies:
     aframe-lerp-component "^1.1.0"
     aframe-template-component "3.2.1"
-- 
GitLab