1
votes

I'm trying to reparent an element (entity), keeping its position, rotation (and if possible, size, eg, scale) in the scene. Ideally, I would like to have a component (say "reparent") that when set on an entity, "moves" it to the specified parent, keeping the aspect of the entity in the scene. For example, for the next HTML code, the green entity would become a child of the red one, but would keep its position and rotation in the scene:

  <a-scene id="scene" background="color: grey">
      <a-entity id="red"
        position="-1 3.5 -4" rotation="30 0 0" material="color: red"
        geometry="primitive: cylinder"></a-entity>
        
      <a-entity id="green" reparent="parent: #red"
        position="-3 2 -4" rotation="0 45 0" material="color: green"
        geometry="primitive: box"></a-entity>
  </a-scene>

Apparently, this can be done at the three level using attach, but when I try to write a simple component using it, it doesn't work, apparently due to what the reparenting at the HTML does (I assume, because of AFrame).

I've written his test component to show the problem:

AFRAME.registerComponent('reparent', {
  schema: {
    parent: {type: 'selector', default: null},
  },
  init: function () {
    const el = this.el;
    const parent = this.data.parent;

    move = function(evt) {
      parent.object3D.attach(el.object3D);
      console.log(el.object3D.parent.el.id, el.parentElement.id);
//      parent.appendChild(el);
//      console.log(el.object3D.parent.el.id, el.parentElement.id);
    };
    el.sceneEl.addEventListener('loaded', move);
  }
});

When run, the result is red scene: the parent of object3D changed, but at the HTML level the parent of the element remains the scene. The green box appears as intended (in "-3 2 -4", world coordinates, and with the right rotation).

If I uncomment the two commented lines, I get a second line red red, but the green box disappears. In other words, now at the HTML the element got reparented, as intended, but somehow its object3D is not working anymore.

Any idea about why this fails, or some other idea for such a component?

All of this with AFrame 1.1.0.

1

1 Answers

1
votes

After getting the advice that reusing the same element (entity) for reparenting is not a good idea (see Change parent of component and keep position), I explored the idea of copying the entity to be reparented in a new element under the new parent, and then remove the "old" one (instead of directly reparenting the entity). Granted, it is not the most clean hack in the world, but I hope it helps in my use case. So, I wrote a component:

AFRAME.registerComponent('reparent', {
  schema: {
    parent: {type: 'selector', default: null},
  },
  update: function () {
    const el = this.el;
    const parent = this.data.parent;

    if (el.parentElement == parent) {
      // We're already a child of the intended parent, do nothing
      return;
    };
    // Reparent, once object3D is ready
    reparent = function() {
      // Attach the object3D to the new parent, to get position, rotation, scale
      parent.object3D.attach(el.object3D);
      let position = el.object3D.position;
      let rotation = el.object3D.rotation;
      let scale = el.object3D.scale;

      // Create new element, copy the current one on it
      let newEl = document.createElement(el.tagName);
      if (el.hasAttributes()) {
        let attrs = el.attributes;
        for(var i = attrs.length - 1; i >= 0; i--) {
          let attrName = attrs[i].name;
          let attrVal = el.getAttribute(attrName);
          newEl.setAttribute(attrName, attrVal);
        };
      };
      // Listener for location, rotation,... when the new el is laded
      relocate = function() {
        newEl.object3D.location = location;
        newEl.object3D.rotation = rotation;
        newEl.object3D.scale = scale;
      };
      newEl.addEventListener('loaded', relocate, {'once': true});
      // Attach the new element, and remove this one
      parent.appendChild(newEl);
      el.parentElement.removeChild(el);
    };
    if (el.getObject3D('mesh')) {
      reparent();
    } else {
      el.sceneEl.addEventListener('object3dset', reparent, {'once': true});
    };
  }
});

You can check it works with the following HTML file:

<!DOCTYPE html>
<html>
  <head>
    <script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script>
    <script src="reparent.js"></script>
  </head>
  <body>
    <a-scene id="scene" background="color: grey">
      <a-entity id="red"
        position="1 2 -4" rotation="30 0 0" material="color: red"
        geometry="primitive: cylinder"></a-entity>
        
      <a-entity id="green" reparent="parent: #red"
        position="-1 0 -4" rotation="0 45 0" material="color: green"
        geometry="primitive: box"></a-entity>

      <a-entity id="wf"
        position="-1 0 -4" rotation="0 45 0" material="wireframe: true"
        geometry="primitive: box"></a-entity>
    </a-scene>
  </body>
</html>

The wireframe entity is just to show how the green box stays exactly in the same position, although it was "reparented" under the red entity (explore the DOM to check that). As explained above, the green box is really a new one, cloned from the original one, which was then deleted by the component.

The component should work when its parent property is changed, allowing for dynamic reparenting when needed.