From f296e66d142a14d4e52c1e0b6aa5910106a3595b Mon Sep 17 00:00:00 2001
From: johnshaughnessy <johnfshaughnessy@gmail.com>
Date: Mon, 5 Nov 2018 15:52:39 -0800
Subject: [PATCH] Sort bindings by dependency. Fix keyboard+mouse bindings.

---
 .../userinput/bindings/keyboard-mouse-user.js | 119 +++++----
 .../userinput/devices/app-aware-mouse.js      |   9 +-
 src/systems/userinput/userinput-debug.js      | 117 +++------
 src/systems/userinput/userinput.js            | 246 +++++++++++-------
 4 files changed, 252 insertions(+), 239 deletions(-)

diff --git a/src/systems/userinput/bindings/keyboard-mouse-user.js b/src/systems/userinput/bindings/keyboard-mouse-user.js
index 2ff3297c9..351b9e3e0 100644
--- a/src/systems/userinput/bindings/keyboard-mouse-user.js
+++ b/src/systems/userinput/bindings/keyboard-mouse-user.js
@@ -9,25 +9,11 @@ const arrows_vec2 = "/var/mouse-and-keyboard/arrows_vec2";
 const dropWithRMB = "/vars/mouse-and-keyboard/drop_with_RMB";
 const dropWithEsc = "/vars/mouse-and-keyboard/drop_with_esc";
 
-const dropWithRMBorEscBindings = [
-  {
-    src: { value: paths.device.mouse.buttonRight },
-    dest: { value: dropWithRMB },
-    xform: xforms.falling,
-    root: "rmb",
-    priority: 200
-  },
-  {
-    src: { value: paths.device.keyboard.key("Escape") },
-    dest: { value: dropWithEsc },
-    xform: xforms.falling
-  },
-  {
-    src: [dropWithRMB, dropWithEsc],
-    dest: { value: paths.actions.cursor.drop },
-    xform: xforms.any
-  }
-];
+const k = name => {
+  return `/keyboard-mouse-user/keyboard-var/${name}`;
+};
+
+const dropWithRMBorEscBindings = [];
 
 export const keyboardMouseUserBindings = addSetsToBindings({
   [sets.global]: [
@@ -81,16 +67,12 @@ export const keyboardMouseUserBindings = addSetsToBindings({
     {
       src: { value: paths.device.keyboard.key("q") },
       dest: { value: paths.actions.snapRotateLeft },
-      xform: xforms.rising,
-      root: "q",
-      priority: 100
+      xform: xforms.rising
     },
     {
       src: { value: paths.device.keyboard.key("e") },
       dest: { value: paths.actions.snapRotateRight },
-      xform: xforms.rising,
-      root: "e",
-      priority: 100
+      xform: xforms.rising
     },
     {
       src: { value: paths.device.hud.penButton },
@@ -148,7 +130,6 @@ export const keyboardMouseUserBindings = addSetsToBindings({
         value: paths.actions.startGazeTeleport
       },
       xform: xforms.rising,
-      root: "rmb",
       priority: 100
     },
     {
@@ -202,7 +183,6 @@ export const keyboardMouseUserBindings = addSetsToBindings({
       src: { value: "/var/notshift+q" },
       dest: { value: paths.actions.snapRotateLeft },
       xform: xforms.rising,
-      root: "q",
       priority: 200
     },
     {
@@ -217,92 +197,116 @@ export const keyboardMouseUserBindings = addSetsToBindings({
       src: { value: "/var/notshift+e" },
       dest: { value: paths.actions.snapRotateRight },
       xform: xforms.rising,
-      root: "e",
       priority: 200
     },
     {
       src: { value: paths.device.mouse.buttonLeft },
       dest: { value: paths.actions.cursor.startDrawing },
       xform: xforms.rising,
-      priority: 200
+      priority: 3
     },
     {
       src: { value: paths.device.mouse.buttonLeft },
       dest: { value: paths.actions.cursor.stopDrawing },
       xform: xforms.falling,
-      priority: 200,
-      root: "lmb"
+      priority: 3
     },
     {
       src: {
-        bool: paths.device.keyboard.key("shift"),
-        value: paths.device.mouse.wheel
+        value: k("wheelWithShift")
       },
       dest: { value: "/var/cursorScalePenTipWheel" },
-      xform: xforms.copyIfTrue,
-      priority: 200,
-      root: "wheel"
+      xform: xforms.copy,
+      priority: 200
     },
     {
       src: { value: "/var/cursorScalePenTipWheel" },
       dest: { value: paths.actions.cursor.scalePenTip },
       xform: xforms.scale(0.12)
     },
-    ...dropWithRMBorEscBindings
+    {
+      src: { value: paths.device.mouse.buttonRight },
+      dest: { value: dropWithRMB },
+      xform: xforms.falling,
+      priority: 200
+    },
+    {
+      src: { value: paths.device.keyboard.key("Escape") },
+      dest: { value: dropWithEsc },
+      xform: xforms.falling
+    },
+    {
+      src: [dropWithRMB, dropWithEsc],
+      dest: { value: paths.actions.cursor.drop },
+      xform: xforms.any
+    }
   ],
 
   [sets.cursorHoldingCamera]: [
     {
       src: { value: paths.device.mouse.buttonLeft },
       dest: { value: paths.actions.cursor.takeSnapshot },
-      xform: xforms.rising
+      xform: xforms.rising,
+      priority: 3
     },
     {
-      src: { value: paths.device.mouse.buttonLeft },
-      xform: xforms.noop,
-      dest: { value: paths.noop },
-      priority: 200,
-      root: "lmb"
+      src: { value: paths.device.mouse.buttonRight },
+      dest: { value: dropWithRMB },
+      xform: xforms.falling,
+      priority: 200
+    },
+    {
+      src: { value: paths.device.keyboard.key("Escape") },
+      dest: { value: dropWithEsc },
+      xform: xforms.falling
     },
-    ...dropWithRMBorEscBindings
+    {
+      src: [dropWithRMB, dropWithEsc],
+      dest: { value: paths.actions.cursor.drop },
+      xform: xforms.any
+    }
   ],
 
   [sets.cursorHoldingInteractable]: [
     {
       src: {
+        bool: paths.device.keyboard.key("shift"),
         value: paths.device.mouse.wheel
       },
       dest: {
-        value: paths.actions.cursor.modDelta
+        value: k("wheelWithShift")
       },
-      xform: xforms.copy,
-      root: "wheel",
-      priority: 100
+      xform: xforms.copyIfTrue
     },
     {
       src: {
         bool: paths.device.keyboard.key("shift"),
         value: paths.device.mouse.wheel
       },
-      dest: { value: paths.actions.cursor.modDelta },
+      dest: {
+        value: k("wheelWithoutShift")
+      },
       xform: xforms.copyIfFalse
     },
     {
       src: {
-        bool: paths.device.keyboard.key("shift"),
-        value: paths.device.mouse.wheel
+        value: k("wheelWithoutShift")
+      },
+      dest: { value: paths.actions.cursor.modDelta },
+      xform: xforms.copy
+    },
+    {
+      src: {
+        value: k("wheelWithShift")
       },
       dest: { value: paths.actions.cursor.scaleGrabbedGrabbable },
-      xform: xforms.copyIfTrue,
-      priority: 150,
-      root: "wheel"
+      xform: xforms.copy
     },
     {
       src: { value: paths.device.mouse.buttonLeft },
       dest: { value: paths.actions.cursor.drop },
       xform: xforms.falling,
-      priority: 100,
-      root: "lmb"
+      priority: 2
     }
   ],
 
@@ -310,7 +314,8 @@ export const keyboardMouseUserBindings = addSetsToBindings({
     {
       src: { value: paths.device.mouse.buttonLeft },
       dest: { value: paths.actions.cursor.grab },
-      xform: xforms.rising
+      xform: xforms.rising,
+      priority: 1
     }
   ],
   [sets.inputFocused]: [
diff --git a/src/systems/userinput/devices/app-aware-mouse.js b/src/systems/userinput/devices/app-aware-mouse.js
index 66e006d44..de5f4acec 100644
--- a/src/systems/userinput/devices/app-aware-mouse.js
+++ b/src/systems/userinput/devices/app-aware-mouse.js
@@ -1,3 +1,4 @@
+import { sets } from "../sets";
 import { paths } from "../paths";
 import { Pose } from "../pose";
 
@@ -35,9 +36,13 @@ export class AppAwareMouseDevice {
       const rawIntersections = [];
       this.cursorController.raycaster.intersectObjects(this.cursorController.targets, true, rawIntersections);
       const intersection = rawIntersections.find(x => x.object.el);
+      const userinput = AFRAME.scenes[0].systems.userinput;
       this.clickedOnAnything =
-        intersection &&
-        intersection.object.el.matches(".pen, .pen *, .video, .video *, .interactable, .interactable *");
+        (intersection &&
+          intersection.object.el.matches(".pen, .pen *, .video, .video *, .interactable, .interactable *")) ||
+        userinput.activeSets.has(sets.cursorHoldingPen) ||
+        userinput.activeSets.has(sets.cursorHoldingInteractable) ||
+        userinput.activeSets.has(sets.cursorHoldingCamera);
     }
     this.prevButtonLeft = buttonLeft;
 
diff --git a/src/systems/userinput/userinput-debug.js b/src/systems/userinput/userinput-debug.js
index fe0362c2f..453bcdaf8 100644
--- a/src/systems/userinput/userinput-debug.js
+++ b/src/systems/userinput/userinput-debug.js
@@ -1,96 +1,45 @@
 import { paths } from "./paths";
-const line = "__________________________________________________________________";
-const bindingToString = b => {
-  const sb = [];
-  sb.push("{\n");
-  sb.push("  ");
-  sb.push("src: ");
-  sb.push("\n");
-  for (const s of Object.keys(b.src)) {
-    sb.push("  ");
-    sb.push("  ");
-    sb.push(s);
-    sb.push(" : ");
-    sb.push(b.src[s]);
-    sb.push("\n");
-  }
-  sb.push("  ");
-  sb.push("dest: ");
-  sb.push("\n");
-  for (const s of Object.keys(b.dest)) {
-    sb.push("  ");
-    sb.push("  ");
-    sb.push(s);
-    sb.push(" : ");
-    sb.push(b.dest[s]);
-    sb.push("\n");
-  }
-  sb.push("  ");
-  sb.push("priority");
-  sb.push(" : ");
-  sb.push(b.priority || 0);
-  for (const s of b.sets) {
-    sb.push("\n");
-    sb.push("  ");
-    sb.push("in set");
-    sb.push(" : ");
-    sb.push(s);
-    sb.push("\n");
-  }
-  sb.push("\n");
-  sb.push("}\n");
-  return sb.join("");
-};
 AFRAME.registerSystem("userinput-debug", {
   tick() {
     const userinput = AFRAME.scenes[0].systems.userinput;
     if (userinput.get(paths.actions.logDebugFrame) || userinput.get(paths.actions.log)) {
-      const sb = [];
-      sb.push("\n");
-      sb.push(line);
-      sb.push("\n");
-      sb.push("actives:");
-      sb.push("\n");
-      sb.push(line);
-      sb.push("\n");
-      for (let i = 0; i < userinput.runners.length; i++) {
-        if (userinput.actives[i]) {
-          sb.push(bindingToString(userinput.runners[i]));
-        }
-      }
-      sb.push("\n");
-      sb.push(line);
-      sb.push("\n");
-      sb.push("inactives:");
-      sb.push("\n");
-      sb.push(line);
-      sb.push("\n");
-      for (let i = 0; i < userinput.runners.length; i++) {
-        if (!userinput.actives[i]) {
-          sb.push("\n");
-          sb.push("The inactive binding:\n");
-          sb.push(bindingToString(userinput.runners[i]));
-          sb.push("\n");
-          sb.push("is overridden by the following ");
-          sb.push(userinput.overrides[i].length);
-          sb.push(" bindings.\n");
-          for (const o of userinput.overrides[i]) {
-            sb.push("\n");
-            sb.push("Override:\n");
-            sb.push(bindingToString(o));
-            sb.push("\n");
-          }
-        }
-      }
-      console.log("active and inactive bindings");
-      console.log(sb.join(""));
-      console.log("runners", userinput.runners);
+      console.log(userinput);
+      console.log("sorted", userinput.sortedBindings);
       console.log("actives", userinput.actives);
-      console.log("xformStates", userinput.xformStates);
+      console.log("masked", userinput.masked);
       console.log("devices", userinput.activeDevices);
-      console.log("map", userinput.map);
       console.log("activeSets", userinput.activeSets);
       console.log("frame", userinput.frame);
+      console.log("xformStates", userinput.xformStates);
+      const { sortedBindings, actives, masked, xformStates } = userinput;
+      for (const i in sortedBindings) {
+        const sb = [];
+        if (masked[i].length > 0) {
+          const xform = xformStates[sortedBindings[i]];
+          for (const j of masked[i]) {
+            sb.push(JSON.stringify(sortedBindings[j]));
+          }
+        }
+
+        console.log(
+          "binding: ",
+          i,
+          "\n",
+          sortedBindings[i],
+          "\n",
+          "dest: ",
+          Object.values(sortedBindings[i].dest),
+          "\n",
+          "active: ",
+          actives[i],
+          "\n",
+          "maskedBy: ",
+          masked[i],
+          "\n",
+          sb.join("\n"),
+          "\n"
+        );
+      }
     }
   }
 });
diff --git a/src/systems/userinput/userinput.js b/src/systems/userinput/userinput.js
index a00a277b1..45a255197 100644
--- a/src/systems/userinput/userinput.js
+++ b/src/systems/userinput/userinput.js
@@ -25,32 +25,125 @@ import { resolveActionSets } from "./resolve-action-sets";
 import { GamepadDevice } from "./devices/gamepad";
 import { gamepadBindings } from "./bindings/generic-gamepad";
 
-function buildBindingsForSrcs(registeredMappings) {
-  const map = new Map();
-  const add = (path, binding) => {
-    if (!map.has(path)) {
-      map.set(path, [binding]);
-    } else {
-      map.get(path).push(binding);
+const satisfiesPath = (binding, path) => {
+  return Object.values(binding.dest).indexOf(path) !== -1;
+};
+
+const satisfyPath = (bindings, path) => {
+  for (const binding of bindings) {
+    if (satisfiesPath(binding, path)) {
+      return true;
     }
-  };
-  for (const mapping of registeredMappings) {
+  }
+  return false;
+};
+
+const satisfiedBy = (binding, bindings) => {
+  for (const path of Object.values(binding.src)) {
+    if (path.startsWith("/device/")) continue;
+    if (!satisfyPath(bindings, path)) return false;
+  }
+  return true;
+};
+
+function dependencySort(mappings) {
+  const unsorted = [];
+  for (const mapping of mappings) {
     for (const setName in mapping) {
       for (const binding of mapping[setName]) {
-        if (Array.isArray(binding.src)) {
-          for (const path of binding.src) {
-            add(path, binding);
-          }
-        } else {
-          for (const srcKey in binding.src) {
-            const path = binding.src[srcKey];
-            add(path, binding);
-          }
+        unsorted.push(binding);
+      }
+    }
+  }
+
+  const sorted = [];
+  while (unsorted.length > 0) {
+    const binding = unsorted.shift();
+    if (satisfiedBy(binding, sorted)) {
+      sorted.push(binding);
+    } else {
+      unsorted.push(binding);
+    }
+  }
+
+  return sorted;
+}
+
+function computeDepsDAG(bindings) {
+  const dag = [];
+  for (const row in bindings) {
+    for (const col in bindings) {
+      for (const path of bindings[row].src) {
+        dag[Number(row) * bindings.length + Number(col)] = satisfiesPath(bindings[col], path) ? 1 : 0;
+      }
+    }
+  }
+  return dag;
+}
+
+function canMask(masker, masked) {
+  if (masker.priority === undefined) {
+    console.warn("priority undefined", masker);
+    masker.priority = 0;
+  }
+  if (masked.priority === undefined) {
+    console.warn("priority undefined", masked);
+    masked.priority = 0;
+  }
+  if (masked.priority >= masker.priority) return false;
+  for (const maskerPath of Object.values(masker.src)) {
+    for (const maskedPath of Object.values(masked.src)) {
+      if (maskedPath.indexOf(maskerPath) !== -1) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+function computeMasks(bindings) {
+  const masks = [];
+  for (const row in bindings) {
+    for (const col in bindings) {
+      let ColCanMaskRow = false;
+      for (const path of Object.values(bindings[row].src)) {
+        if (canMask(bindings[col], bindings[row])) {
+          ColCanMaskRow = true;
         }
       }
+      masks[Number(row) * bindings.length + Number(col)] = ColCanMaskRow;
     }
   }
-  return map;
+  return masks;
+}
+
+function isActive(binding, sets) {
+  for (const s of binding.sets) {
+    if (sets.has(s)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+function computeExecutionStrategy(sortedBindings, masks, activeSets) {
+  const actives = [];
+  for (const row in sortedBindings) {
+    actives[row] = isActive(sortedBindings[row], activeSets);
+  }
+
+  const masked = [];
+  for (const row in sortedBindings) {
+    for (const col in sortedBindings) {
+      let rowMask = masked[row] || [];
+      if (masks[Number(row) * sortedBindings.length + Number(col)] && isActive(sortedBindings[col], activeSets)) {
+        rowMask.push(col);
+      }
+      masked[row] = rowMask;
+    }
+  }
+
+  return { actives, masked };
 }
 
 AFRAME.registerSystem("userinput", {
@@ -64,14 +157,12 @@ AFRAME.registerSystem("userinput", {
 
   init() {
     this.frame = {};
-
     this.activeSets = new Set([sets.global]);
     this.pendingSetChanges = [];
+    this.xformStates = new Map();
     this.activeDevices = new Set([new MouseDevice(), new AppAwareMouseDevice(), new KeyboardDevice(), new HudDevice()]);
-
     this.registeredMappings = new Set([keyboardDebuggingBindings]);
-    this.bindingsForSrc = buildBindingsForSrcs(this.registeredMappings);
-    this.xformStates = new Map();
+    this.registeredMappingsChanged = true;
 
     const appAwareTouchscreenDevice = new AppAwareTouchscreenDevice();
     const updateBindingsForVRMode = () => {
@@ -91,7 +182,7 @@ AFRAME.registerSystem("userinput", {
           this.registeredMappings.add(keyboardMouseUserBindings);
         }
       }
-      this.bindingsForSrc = buildBindingsForSrcs(this.registeredMappings);
+      this.registeredMappingsChanged = true;
     };
     this.el.sceneEl.addEventListener("enter-vr", updateBindingsForVRMode);
     this.el.sceneEl.addEventListener("exit-vr", updateBindingsForVRMode);
@@ -128,7 +219,7 @@ AFRAME.registerSystem("userinput", {
           this.registeredMappings.add(gamepadBindings);
         }
         this.activeDevices.add(gamepadDevice);
-        this.bindingsForSrc = buildBindingsForSrcs(this.registeredMappings);
+        this.registeredMappingsChanged = true;
       },
       false
     );
@@ -137,8 +228,9 @@ AFRAME.registerSystem("userinput", {
       e => {
         for (const device of this.activeDevices) {
           if (device.gamepad === e.gamepad) {
+            console.warn("NEED TO UPDATE REGISTERED MAPPINGS WHEN GAMEPAD DISCONNECTED!");
             this.activeDevices.delete(device);
-            this.bindingsForSrc = buildBindingsForSrcs(this.registeredMappings);
+            this.registeredMappingsChanged = true;
             return;
           }
         }
@@ -148,75 +240,29 @@ AFRAME.registerSystem("userinput", {
   },
 
   tick() {
-    resolveActionSets();
+    const registeredMappingsChanged = this.registeredMappingsChanged;
+    if (registeredMappingsChanged) {
+      this.registeredMappingsChanged = false;
+      this.prevSortedBindings = this.sortedBindings;
+      this.sortedBindings = dependencySort(this.registeredMappings);
+      if (!this.prevSortedBindings) {
+        this.prevSortedBindings = this.sortedBindings;
+      }
+      this.masks = computeMasks(this.sortedBindings);
+    }
 
+    resolveActionSets();
     for (const { set, value } of this.pendingSetChanges) {
       this.activeSets[value ? "add" : "delete"](set);
     }
-    const runners = this.pendingSetChanges.length ? [] : this.runners;
-    if (this.pendingSetChanges.length) {
-      this.pendingSetChanges.length = 0;
-      this.actives = [];
-      this.overrides = [];
-      for (const mapping of this.registeredMappings) {
-        for (const setName in mapping) {
-          if (!this.activeSets.has(setName) || !mapping[setName]) continue;
-          for (const binding of mapping[setName]) {
-            let active = false;
-            for (const set of binding.sets) {
-              if (this.activeSets.has(set)) {
-                active = true;
-              }
-            }
-            this.actives.push(active);
-            runners.push(binding);
-            this.overrides.push([]);
-          }
-        }
-      }
-
-      const maxAmongActive = (path, map) => {
-        let max = { priority: -1 };
-        const bindings = map.get(path);
-        if (!bindings) {
-          return -1;
-        }
-        for (const binding of bindings) {
-          let active = false;
-          for (const set of binding.sets) {
-            if (this.activeSets.has(set)) {
-              active = true;
-            }
-          }
-          if (active && binding.priority && binding.priority > max.priority) {
-            max = binding;
-          }
-        }
-        return max;
-      };
-
-      for (const i in runners) {
-        if (!this.actives[i]) continue;
-        const binding = runners[i];
-        let active = true;
-        for (const p in binding.src) {
-          const path = binding.src[p];
-          const subpaths = String.split(path, "/");
-          while (subpaths.length > 1) {
-            const highestPriorityBindingForSubpath = maxAmongActive(
-              Array.join(subpaths, "/"),
-              this.bindingsForSrc,
-              this.activeSets
-            );
-            if ((binding.priority || 0) < highestPriorityBindingForSubpath.priority) {
-              this.overrides[i].push(highestPriorityBindingForSubpath);
-              active = false;
-            }
-            subpaths.pop();
-          }
-        }
-        this.actives[i] = active;
-      }
+    const activeSetsChanged = this.pendingSetChanges.length; // TODO: correct this
+    this.pendingSetChanges.length = 0;
+    if (registeredMappingsChanged || activeSetsChanged || (!this.actives && !this.masked)) {
+      this.prevActives = this.actives;
+      this.prevMasked = this.masked;
+      const { actives, masked } = computeExecutionStrategy(this.sortedBindings, this.masks, this.activeSets);
+      this.actives = actives;
+      this.masked = masked;
     }
 
     this.frame = {};
@@ -224,11 +270,18 @@ AFRAME.registerSystem("userinput", {
       device.write(this.frame);
     }
 
-    for (const i in runners) {
-      const binding = runners[i];
-      if (!this.actives[i]) continue;
-      const bindingExistedLastFrame = this.runners && this.runners.includes(binding);
+    for (const i in this.sortedBindings) {
+      if (!this.actives[i] || this.masked[i].length > 0) continue;
+
+      const binding = this.sortedBindings[i];
+
+      let bindingExistedLastFrame = true;
+      if (!registeredMappingsChanged && activeSetsChanged && this.prevSortedBindings) {
+        const j = this.prevSortedBindings.indexOf(binding);
+        bindingExistedLastFrame = j > -1 && this.prevActives[j] && this.prevMasked[j].length === 0;
+      }
       if (!bindingExistedLastFrame) {
+        console.log("deleting xform state for ", binding);
         this.xformStates.delete(binding);
       }
 
@@ -239,6 +292,7 @@ AFRAME.registerSystem("userinput", {
       }
     }
 
-    this.runners = runners;
+    this.prevSortedBindings = this.sortedBindings;
+    this.prevFrame = this.frame;
   }
 });
-- 
GitLab