Search code examples
typescriptevent-handlingaframe

Update Event Listener in A-Frame according to a boolean variable


I'm trying to create, with A-Frame, a Typescript class that allows me to render models and insert elements inside the scene in response to clicks. The handler should be bound by the value of a boolean variable that acts as a trigger; specifically, the class should insert an element only if this trigger has the value set to true.

My approach is based on the registration of a component, inside which I insert the definition of the handler inside the update function. This is a draft of the code:

AFRAME.registerComponent('click-handler', {
  schema: {
    trigger: { type: 'boolean' },
  },

  // init function definition

  update: function () {
    let mouseDownTime: number = null;
    let mouseDownPoint: any = null;
    let trigger = this.data.trigger;

    function clickHandler(event) {
      let point = event.detail.intersection.point;
      let pointString = point.x.toFixed(3) + " " + point.y.toFixed(3) + " " + point.z.toFixed(3);

      // compute the box that contains the model
      let model = <any>document.getElementById("model")
      const box = new THREE.Box3().setFromObject(model.object3D);
      const boxSizes = box.getSize(new THREE.Vector3());

      if (trigger) {
        // compute the min size of the box (x, y, z)
        // it will be used to set pointer radius
        let minBoxSize = Math.min(boxSizes.x, boxSizes.y, boxSizes.z);
        let radius = minBoxSize / 30;

        let scene = document.getElementById("scene");
        var marker = document.createElement("a-sphere");
        scene.appendChild(marker);

        marker.setAttribute("class", "pointer");
        marker.setAttribute("radius", `${radius}`);
        marker.setAttribute("color", "#CC0000");
        marker.setAttribute("position", pointString);
      }
    }

    this.el.addEventListener('mousedown', event => {
      mouseDownTime = new Date().getTime();
      mouseDownPoint = event.detail.intersection.point;
    });

    this.el.addEventListener('mouseup', event => {
      if (!event.detail.intersection) return;

      let mouseUpTime = new Date().getTime();
      let mouseUpPoint = event.detail.intersection.point;

      // compute the differences (time and position) between press and release
      let timeDiff = mouseUpTime - mouseDownTime;

      // if press and release occur on the same point within 185 ms
      //  we consider the overall "event" as a click
      if (timeDiff <= 185 && JSON.stringify(mouseDownPoint) === JSON.stringify(mouseUpPoint)) {
        clickHandler(event);
      }
    });
  },

  // remove function definition
}

What I would like to understand is if it is possible to update the event listener so that it takes into account the new value of the trigger. I'm afraid that the current code is always inserting new event listeners, and this should be the cause of my problem.

EDIT: In the Angular component template I have a button; when it gets pressed it sets the "outer trigger" to true. What I'm trying to do is to find a way to make the handler aware of this change.

Thank you all.


Solution

  • I think you should put the addEventListener in the init.

    Right now it is in update which is

    Called both when the component is initialized and whenever any of the component’s properties is updated (e.g, via setAttribute). Used to modify the entity. source

    So you end up adding an event listener every time something changes.

    And then you probably should check inside the event listener whether your flag is set. Inside the event listener, you should be able to access the variable by:

    event.target.components['component-name'].data.trigger for the attribute itself.

    event.target.components['component-name'].data this will work if you don't specify the name for the schema property, and just use something called single property component

    event.target.components['component-name'].variable for a variable inside component or this.variable

    Also inside the component, you can freely change the trigger variable - but not the one related to the schema but the one you would create in init let trigger = this.data.trigger; for the schema one you would need to use el.setAttribute('click-handler', true) but I don't know if it would rerun the init part or not. edit: this would run the update - not init.

    Working example:

    there is a button in the upper left corner to toggle the clickability of the sphere.

    if you don't want to use a variable this is how you can access the component value inside the event listener. event.target.components['click-handler'].trigger

    function toggleClickHandler() {
      let el = document.getElementById("clickableElement");
      // here you can set the new value for the component
      // also look how you can access component variable from outside
      // changing the value will trigger the 'update' lifecycle function 
      el.setAttribute("click-handler", "trigger", !el.components['click-handler'].trigger);
      console.log(el.components['click-handler'].trigger);
    }
    <!DOCTYPE html>
    <html>
      <head>
        <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
      </head>
      <body>
        <!-- script here because on stack JS is after the HTML --> 
        <script>
          AFRAME.registerComponent('click-handler', {
            schema: {
              // if you specify name you have to use it when setting the attribute
              trigger: { type: 'boolean' },
            },
            // init function definition
            init: function() {
              this.el.addEventListener('mousedown', (event) => {
                if (!this.trigger) return;
                // ... your logic ...
                console.log("I was mousedowned!");
                this.el.setAttribute('material', 'color', 'green');
              });
    
              this.el.addEventListener('mouseup', (event) => {
                if (!this.trigger) return;
                // ... your logic ...
                console.log("I was mouseuped!");
                this.el.setAttribute('material', 'color', 'red');
              });
            },
            // update is called once on init and every time the elements attribute is changed.
            update: function() {
              this.trigger = this.data.trigger;
            }
          });
        </script>
        <a-scene>
          <a-entity camera look-controls>
            <a-entity cursor="fuse: true; fuseTimeout: 500;"
                      position="0 0 -1"
                      geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03"
                      material="color: black; shader: flat">
            </a-entity>
          </a-entity>
          <a-sphere id="clickableElement" scale="1 2 3" position="6 1 -5" click-handler="trigger: true">
          </a-sphere>
        </a-scene>
        <button onclick="toggleClickHandler()" style="width: 100px; height: 100px; position: absolute; top: 0; left: 0;" value="toggle"></button>
      </body>
    </html>

    Let me know if this helps.