Aframe: Components used in primitives are initialized before mappings are applied

Created on 12 Sep 2016  Â·  7Comments  Â·  Source: aframevr/aframe

Description:

Code:

AFRAME.registerPrimitive('a-tube', {
  defaultComponents: {
    tube:           {},
  },
  mappings: {
    path:           'tube.path',
    segments:       'tube.segments',
    radius:         'tube.radius',
    radialSegments: 'tube.radialSegments',
    closed:         'tube.closed'
  }
});

AFRAME.registerComponent('tube', {
  schema: {
    path:           {default: []},
    segments:       {default: 64},
    radius:         {default: 1},
    radialSegments: {default: 8},
    closed:         {default: false}
  },

  init: function () {
    var el = this.el,
        data = this.data,
        material = el.components.material;

    var curve = new THREE.CatmullRomCurve3(data.path.map(function (point) {
      point = point.split(' ');
      return new THREE.Vector3(Number(point[0]), Number(point[1]), Number(point[2]));
    }));
    var geometry = new THREE.TubeGeometry(
      curve, data.segments, data.radius, data.radialSegments, data.closed
    );

    this.mesh = new THREE.Mesh(geometry, material.material);
    this.el.setObject3D('mesh', this.mesh);
  },

  remove: function () {
    if (this.mesh) this.el.removeObject3D('mesh');
  }
});

Markup:

<a-tube path="5 0 5, 5 0 -5, -5 0 -5" radius="0.1" segments="8" closed="true"></a-tube>

After some debugging, here's what's happening:

  1. Default properties are applied, and component is instantiated.
  2. The first mapping is applied, and tube.init() is called.
  3. For each subsequent mapping, tube.update() is called.

I could / should implement tube.update() of course, but this would result in re-creating the geometry up to six times during scene load, so it doesn't solve the issue.

Related: https://github.com/donmccurdy/aframe-extras/issues/80, https://github.com/donmccurdy/aframe-extras/issues/85 (and possibly #1609 ?).

  • A-Frame Version: v0.3.1
bug

Most helpful comment

Going to work on this. Maybe

c) Have primitives call initComponent or some other entity method rather than going through setAttribute

All 7 comments

I'm not sure what the best approach to fixing this would be, but some thoughts:

a) Batch up defaults and mappings onto an object and do the update in a single pass.

or

b) Remove calls to init() inside setAttribute().

Going to work on this. Maybe

c) Have primitives call initComponent or some other entity method rather than going through setAttribute

The bug was as Don described:

  1. Default components and initial mapping seem to get batched together and called on the init().
  2. Subsequent mappings trigger the update() however. This set of data should be part of the init() process.

This happens when we loop over the attributes and call setAttribute. The first setAttribute triggers the init(), the later ones trigger update().

To resolve this, we need to modify setAttribute to be able to do batched updates. I propose an update flag to specify that setAttribute does an "post" rather than a "put" on the data. Currently, if you pass an object, it will do a "put".

setAttribute('test', {
  foo: 'bar',
  baz: 'qux',
  quux: 'corge'
}, true);

I believe this has performance benefits as well. When we want to update a batch of data, this is more efficient than looping over the properties you want to update, and calling setAttribute individually.

What about an updateAttribute method?

Would setAttribute('foo', {}, true) or updateAttribute('foo', {}) be implemented so as to not invoke init() or update()? From the component's perspective, the problem is receiving this chain of calls:

<a-waffle foo="1" bar="2" baz="3" etc="4" />
  • init({foo: 1})
  • update({foo: 1, bar: 2})
  • update({foo: 1, bar: 2, baz: 3})
  • update({foo: 1, bar: 2, baz: 3, etc: 4})

... where the complete data isn't present until that final update(). It's not clear to me how a different setAttribute API would solve the problem, unless it invokes component lifecycle methods less often than the existing method.

My vote, if possible, would be to put the responsibility onto primitive-specific code — use the list of mappings to determine which attributes are aliases up front, and only call setAttribute() once.

The primitive would be able to call setAttribute('foo', {data}) once which all the data it has, and the component init() would be called with all the data such that update() won't be called in this case.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

huhsame picture huhsame  Â·  3Comments

FireDragonGameStudio picture FireDragonGameStudio  Â·  5Comments

RangerMauve picture RangerMauve  Â·  4Comments

rich311 picture rich311  Â·  3Comments

jgbarah picture jgbarah  Â·  4Comments