Search code examples
typescriptevent-handlingaframe

Add event listener on dynamically created in A-Frame


I created a class that helps me to show a 3D model using A-Frame. In this class, there are some spheres created at runtime ad inserted into the scene. I'm trying to add an event listener (I have to show a message when those spheres are clicked)

Here is the code:

export class AFrameObjViewNavMarkProvider implements Provider {
  // Class Variables...

  // Constructor...

  // ----- Method ----- \\
  public setPointerService(pointersService: PointersService) {
    // method code...
  }

  public setPointerTrigger(value: boolean) {
    // method code...
  }

  // ----- Handlers ----- \\

  // click Handler
  clickHandler(event, model: Model){
    // code for saving into backend...

    // save pointer into the back-end
    this.pointersService.loadPointer(newPointer).subscribe((pointer) => {
      this.showPointer(pointer);
    });
  }

  showPointer(pointer: Pointer){
    // create a string containing the position
    let pointString = pointer.position[0].toFixed(3) + " "
      + pointer.position[1].toFixed(3) + " "
      + pointer.position[2].toFixed(3);

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

    // 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");
    let marker = document.createElement("a-sphere");
    scene.appendChild(marker);

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

  // ----- Visual Methods ----- \\
  renderModel(model: Model) {
    // position-setter is used to set the model position according to its size
    AFrameUtils.registerPositionSetter();

    // reference to the provider itself
    let caller: any = this;

    function clickHandler(event) {
      caller.clickHandler(event, model);
    }

    // sets the behaviour in response to a click event
    AFRAME.registerComponent('click-handler', {
      // init also calls update
      init: function () {
        let mouseDownTime: number = null;
        let mouseDownPoint: any = null;

        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 within 185 ms
          //  we consider the event as a click
          if (timeDiff <= 185 && JSON.stringify(mouseDownPoint) === JSON.stringify(mouseUpPoint)) {
            clickHandler(event);
          }
        });
      }
    });

    let renderingArea = document.getElementById('rendering-area');
    renderingArea.innerHTML = `
      <a-scene embedded id="scene" cursor="rayOrigin: mouse" raycaster="objects: .clickable">
        <!-- Assets definition -->
        <a-assets>
            <a-asset-item id="object-ref" src="${model.sources[0]}"></a-asset-item>
            <a-asset-item id="material-ref" src="${model.sources[1]}"></a-asset-item>
        </a-assets>

        <!-- Using the asset management system. -->
        <a-obj-model id="model" class="clickable" src="#object-ref" mtl="#material-ref" position-setter click-handler>
        </a-obj-model>

        <!-- Camera -->
        <a-camera id="camera" wasd-controls="fly:true"></a-camera>

        <!-- Environment elements-->
        <a-sky id="sky" color="#000000"></a-sky>
      </a-scene>
    `;

    setTimeout(() => {
      this.pointersService.getPointersByModelId(model.id).subscribe(pointers => {
        for(let pointer of pointers){
          this.showPointer(pointer);
        }

        let markers = Array.from(document.getElementsByClassName('pointer'));
        for(let marker of markers){
          marker.addEventListener('click', () => {
            console.log('click on pointer');
          })
        }
      });
    }, 125);
  }
}

and a screenshot:

enter image description here

First Try: I tried to register another component, like this:

AFRAME.registerComponent('pointer-handler', {
      init: function(){
        this.el.addEventListener('click', () => {
          console.log('click on pointer');
        });
      }
    });

and use setAttribute("pointer-handler", "") into the showPointer method

Second Try: I tried to directly add the event listener using marker.addEventListener into the showPointer method.

Third Try: the third attempt can be found on the code, into the callback of setTimeout.

Every attempt doesn't work; if I try to click on one sphere then nothing happens. However, if I open the A-Frame inspector, navigate to one of the spheres and click on it, the message gets logged. I suspect that the handler is added, but something causes the click to be undetected.

Every suggestion will be appreciated.

Thanks!

EDIT: You can find the repo here


Solution

  • Using the setAttribute("pointer-handler", "") approach is absolutely valid and a correct way of doing what you want to achieve.

    I think it may be the click event that's causing problems. I suggest you replace it with mouseup and mousedown events.

    Also make sure you can indeed fire the events - you need a cursor attached to your camera

    Working example

    run in full screen (top right corner after running snippet to see the whole scene - close is also in the top right).

    You can add new elements by pressing the button in the top left. See how click sometimes is fired and sometimes not at all.

    let boxCounter = 0;
    
    function createNewElement() {
      let box = document.createElement("a-box");
      boxCounter++;
      box.setAttribute("position", "" + boxCounter + " 0 -2");
      box.setAttribute("scale", "0.5");
      box.setAttribute("color", "red");
      box.setAttribute("click-handler", "");
      document.getElementById("scene").appendChild(box);
    }
    <!DOCTYPE html>
    <html>
        <head>
            <script src="https://aframe.io/releases/1.0.0/aframe.min.js"></script>
        </head>
        <body>
          <script>
            AFRAME.registerComponent('click-handler', {
              init: function() {
                this.el.addEventListener('mousedown', (event) => {
                  console.log("I was mousedowned!");
                  this.el.setAttribute('material', 'color', 'green');
                });
    
                this.el.addEventListener('mouseup', (event) => {
                  console.log("I was mouseuped!");
                  this.el.setAttribute('material', 'color', 'red');
                });
                
                this.el.addEventListener('click', (event) => {
                  console.log("I was clicked!");
                  this.el.setAttribute('scale', '0.5 0.5 0.5');
                });
              }
            });
          </script>
            <a-scene id="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 color="red" position="0 0 -2" scale="0.5 0.5 0.5" click-handler></a-sphere>
            </a-scene>
            <button onclick="createNewElement()" style="width: 100px; height: 100px; position: absolute; top: 0; left: 0;" value="toggle"></button>
      </body>
        </body>
    </html>