Skip to content

Commit d82d7b2

Browse files
committed
keep components attached to entity if whole entity is detached (fixes #4027) (#4121)
1 parent b5eb3f0 commit d82d7b2

File tree

5 files changed

+78
-54
lines changed

5 files changed

+78
-54
lines changed

docs/core/entity.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,10 @@ object:
320320
entity.setAttribute('position', { x: 1, y: 2, z: 3 });
321321
```
322322

323+
### `destroy ()`
324+
325+
Clean up memory related to the entity such as clearing all components and their data.
326+
323327
#### Updating Multi-Property Component Data
324328

325329
To update component data for a multi-property component, we can pass the name

src/core/a-entity.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ var proto = Object.create(ANode.prototype, {
100100
if (!this.parentEl) { return; }
101101

102102
// Remove components.
103-
for (componentName in this.components) { this.removeComponent(componentName); }
103+
for (componentName in this.components) {
104+
this.removeComponent(componentName, false);
105+
}
104106

105107
if (this.isScene) { return; }
106108

@@ -377,7 +379,7 @@ var proto = Object.create(ANode.prototype, {
377379
},
378380

379381
removeComponent: {
380-
value: function (name) {
382+
value: function (name, destroy) {
381383
var component;
382384

383385
component = this.components[name];
@@ -387,16 +389,22 @@ var proto = Object.create(ANode.prototype, {
387389
if (!component.initialized) {
388390
this.addEventListener('componentinitialized', function tryRemoveLater (evt) {
389391
if (evt.detail.name !== name) { return; }
390-
this.removeComponent(name);
392+
this.removeComponent(name, destroy);
391393
this.removeEventListener('componentinitialized', tryRemoveLater);
392394
});
393395
return;
394396
}
395397

396398
component.pause();
397399
component.remove();
398-
delete this.components[name];
399-
this.emit('componentremoved', component.evtDetail);
400+
401+
// Keep component attached to entity in case of just full entity detach.
402+
if (destroy) {
403+
component.destroy();
404+
delete this.components[name];
405+
}
406+
407+
this.emit('componentremoved', component.evtDetail, false);
400408
},
401409
writable: window.debug
402410
},
@@ -475,7 +483,7 @@ var proto = Object.create(ANode.prototype, {
475483
if (component) {
476484
// Remove component.
477485
if (attrValue === null && !checkComponentDefined(this, attr)) {
478-
this.removeComponent(attr);
486+
this.removeComponent(attr, true);
479487
return;
480488
}
481489
// Component already initialized. Update component.
@@ -502,7 +510,7 @@ var proto = Object.create(ANode.prototype, {
502510

503511
// Remove component.
504512
if (component && propertyName === undefined) {
505-
this.removeComponent(attr);
513+
this.removeComponent(attr, true);
506514
}
507515

508516
// Reset component property value.
@@ -645,7 +653,7 @@ var proto = Object.create(ANode.prototype, {
645653
this.components[component].handleMixinUpdate();
646654
} else {
647655
// Remove component if not explicitly defined.
648-
this.removeComponent(component);
656+
this.removeComponent(component, true);
649657
}
650658
}
651659
}
@@ -832,6 +840,22 @@ var proto = Object.create(ANode.prototype, {
832840
value: function () {
833841
this.sceneEl.components.inspector.openInspector(this);
834842
}
843+
},
844+
845+
/**
846+
* Clean up memory and return memory to object pools.
847+
*/
848+
destroy: {
849+
value: function () {
850+
var key;
851+
if (this.el.parentNode) {
852+
warn('Entity can only be destroyed if detached from scenegraph.');
853+
return;
854+
}
855+
for (key in this.components) {
856+
this.components[key].destroy();
857+
}
858+
}
835859
}
836860
});
837861

src/core/component.js

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,16 @@ Component.prototype = {
572572
for (eventName in this.events) {
573573
this.el.removeEventListener(eventName, this.events[eventName]);
574574
}
575+
},
576+
577+
/**
578+
* Release and free memory.
579+
*/
580+
destroy: function () {
581+
this.objectPool.recycle(this.attrValue);
582+
this.objectPool.recycle(this.oldData);
583+
this.objectPool.recycle(this.parsingAttrValue);
584+
this.attrValue = this.oldData = this.parsingAttrValue = undefined;
575585
}
576586
};
577587

@@ -658,7 +668,6 @@ module.exports.registerComponent = function (name, definition) {
658668
NewComponent.prototype.system = systems && systems.systems[name];
659669
NewComponent.prototype.play = wrapPlay(NewComponent.prototype.play);
660670
NewComponent.prototype.pause = wrapPause(NewComponent.prototype.pause);
661-
NewComponent.prototype.remove = wrapRemove(NewComponent.prototype.remove);
662671

663672
schema = utils.extend(processSchema(NewComponent.prototype.schema,
664673
NewComponent.prototype.name));
@@ -782,22 +791,6 @@ function wrapPlay (playMethod) {
782791
};
783792
}
784793

785-
/**
786-
* Wrapper for defined remove method.
787-
* Clean up memory.
788-
*
789-
* @param removeMethod {function} - Defined remove method.
790-
*/
791-
function wrapRemove (removeMethod) {
792-
return function remove () {
793-
removeMethod.call(this);
794-
this.objectPool.recycle(this.attrValue);
795-
this.objectPool.recycle(this.oldData);
796-
this.objectPool.recycle(this.parsingAttrValue);
797-
this.attrValue = this.oldData = this.parsingAttrValue = undefined;
798-
};
799-
}
800-
801794
function isObject (value) {
802795
return value && value.constructor === Object && !(value instanceof window.HTMLElement);
803796
}

tests/components/geometry.test.js

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ var degToRad = require('index').THREE.Math.degToRad;
77
* parameters. That info is mostly lost when converting a Geometry to a BufferGeometry.
88
*/
99
suite('geometry', function () {
10+
let el;
11+
1012
setup(function (done) {
11-
var el = this.el = helpers.entityFactory();
13+
el = helpers.entityFactory();
1214
el.setAttribute('geometry', 'buffer: false; primitive: box;');
1315
el.addEventListener('loaded', function () {
1416
done();
@@ -17,31 +19,29 @@ suite('geometry', function () {
1719

1820
suite('update', function () {
1921
test('allows empty geometry', function () {
20-
this.el.setAttribute('geometry', '');
22+
el.setAttribute('geometry', '');
2123
});
2224

2325
test('creates geometry', function () {
24-
var mesh = this.el.getObject3D('mesh');
26+
var mesh = el.getObject3D('mesh');
2527
assert.ok(mesh.geometry);
2628
assert.equal(mesh.geometry.type, 'BoxGeometry');
2729
});
2830

2931
test('updates geometry', function () {
30-
var mesh = this.el.getObject3D('mesh');
31-
this.el.setAttribute('geometry', 'buffer: false; primitive: box; width: 5');
32+
var mesh = el.getObject3D('mesh');
33+
el.setAttribute('geometry', 'buffer: false; primitive: box; width: 5');
3234
assert.equal(mesh.geometry.parameters.width, 5);
3335
});
3436

3537
test('updates geometry for segment-related attribute', function () {
36-
var el = this.el;
3738
var mesh = el.getObject3D('mesh');
3839
el.setAttribute('geometry', 'buffer: false; primitive: sphere');
3940
el.setAttribute('geometry', 'buffer: false; primitive: sphere; segmentsWidth: 8');
4041
assert.equal(mesh.geometry.parameters.widthSegments, 8);
4142
});
4243

4344
test('can change type of geometry', function () {
44-
var el = this.el;
4545
var mesh = el.getObject3D('mesh');
4646
el.setAttribute('geometry', 'buffer: false; primitive: sphere');
4747
assert.equal(mesh.geometry.type, 'SphereGeometry');
@@ -50,7 +50,6 @@ suite('geometry', function () {
5050
});
5151

5252
test('disposes geometry', function () {
53-
var el = this.el;
5453
var geometry = el.getObject3D('mesh').geometry;
5554
var disposeSpy = this.sinon.spy(geometry, 'dispose');
5655
assert.notOk(disposeSpy.called);
@@ -61,22 +60,21 @@ suite('geometry', function () {
6160

6261
suite('remove', function () {
6362
test('removes geometry', function () {
64-
var mesh = this.el.getObject3D('mesh');
65-
this.el.removeAttribute('geometry');
63+
var mesh = el.getObject3D('mesh');
64+
el.removeAttribute('geometry');
6665
assert.equal(mesh.geometry.type, 'Geometry');
6766
});
6867

6968
test('disposes geometry', function () {
70-
var geometry = this.el.getObject3D('mesh').geometry;
69+
var geometry = el.getObject3D('mesh').geometry;
7170
var disposeSpy = this.sinon.spy(geometry, 'dispose');
72-
this.el.removeAttribute('geometry');
71+
el.removeAttribute('geometry');
7372
assert.ok(disposeSpy.called);
7473
});
7574
});
7675

7776
suite('buffer', function () {
7877
test('uses BufferGeometry', function () {
79-
var el = this.el;
8078
assert.notEqual(el.getObject3D('mesh').geometry.type, 'BufferGeometry');
8179
el.setAttribute('geometry', 'buffer', true);
8280
assert.equal(el.getObject3D('mesh').geometry.type, 'BufferGeometry');
@@ -85,16 +83,16 @@ suite('geometry', function () {
8583
});
8684

8785
suite('standard geometries', function () {
86+
let el;
8887
setup(function (done) {
89-
var el = this.el = helpers.entityFactory();
88+
el = helpers.entityFactory();
9089
el.setAttribute('geometry', 'primitive: box');
9190
el.addEventListener('loaded', function () {
9291
done();
9392
});
9493
});
9594

9695
test('circle', function () {
97-
var el = this.el;
9896
var geometry;
9997
el.setAttribute('geometry', {
10098
buffer: false, primitive: 'circle', radius: 5, segments: 4, thetaStart: 0, thetaLength: 350
@@ -109,7 +107,6 @@ suite('standard geometries', function () {
109107
});
110108

111109
test('cylinder', function () {
112-
var el = this.el;
113110
var geometry;
114111
el.setAttribute('geometry', {
115112
buffer: false,
@@ -136,7 +133,6 @@ suite('standard geometries', function () {
136133
});
137134

138135
test('cone', function () {
139-
var el = this.el;
140136
var geometry;
141137
el.setAttribute('geometry', {
142138
buffer: false,
@@ -163,7 +159,6 @@ suite('standard geometries', function () {
163159
});
164160

165161
test('icosahedron', function () {
166-
var el = this.el;
167162
var geometry;
168163
el.setAttribute('geometry', {
169164
buffer: false, primitive: 'icosahedron', detail: 0, radius: 5});
@@ -175,7 +170,6 @@ suite('standard geometries', function () {
175170
});
176171

177172
test('plane', function () {
178-
var el = this.el;
179173
var geometry;
180174
el.setAttribute('geometry', {buffer: false, primitive: 'plane', width: 1, height: 2});
181175

@@ -186,7 +180,6 @@ suite('standard geometries', function () {
186180
});
187181

188182
test('ring', function () {
189-
var el = this.el;
190183
var geometry;
191184
el.setAttribute('geometry', {
192185
buffer: false, primitive: 'ring', radiusInner: 1, radiusOuter: 2, segmentsTheta: 3});
@@ -199,7 +192,6 @@ suite('standard geometries', function () {
199192
});
200193

201194
test('sphere', function () {
202-
var el = this.el;
203195
var geometry;
204196
el.setAttribute('geometry', {
205197
buffer: false,
@@ -224,7 +216,6 @@ suite('standard geometries', function () {
224216
});
225217

226218
test('torus', function () {
227-
var el = this.el;
228219
var geometry;
229220
el.setAttribute('geometry', {
230221
buffer: false,
@@ -246,7 +237,6 @@ suite('standard geometries', function () {
246237
});
247238

248239
test('torus knot', function () {
249-
var el = this.el;
250240
var geometry;
251241
el.setAttribute('geometry', {
252242
buffer: false,
@@ -270,7 +260,6 @@ suite('standard geometries', function () {
270260
});
271261

272262
test('triangle', function () {
273-
var el = this.el;
274263
var geometry;
275264
el.setAttribute('geometry', {
276265
buffer: false,
@@ -294,4 +283,18 @@ suite('standard geometries', function () {
294283
assert.equal(vertices[2].y, 8);
295284
assert.equal(vertices[2].z, 9);
296285
});
286+
287+
test('retains data on detach and reattach', function (done) {
288+
helpers.elFactory().then(el => {
289+
el.setAttribute('geometry', 'primitive', 'plane');
290+
el.sceneEl.removeChild(el);
291+
setTimeout(() => {
292+
el.sceneEl.appendChild(el);
293+
setTimeout(() => {
294+
assert.equal(el.components.geometry.data.primitive, 'plane');
295+
done();
296+
});
297+
});
298+
});
299+
});
297300
});

tests/core/a-entity.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ suite('a-entity', function () {
8080
const el2 = document.createElement('a-entity');
8181
el2.object3D = new THREE.Mesh();
8282
el2.setAttribute('geometry', 'primitive:plane');
83-
parentEl.appendChild(el2);
8483
el2.addEventListener('loaded', afterFirstAttachment);
84+
parentEl.appendChild(el2);
8585

8686
function afterFirstAttachment () {
8787
el2.removeEventListener('loaded', afterFirstAttachment);
@@ -93,18 +93,18 @@ suite('a-entity', function () {
9393
assert.isTrue(el2.hasLoaded);
9494

9595
parentEl.removeChild(el2);
96-
process.nextTick(afterDetachment);
96+
setTimeout(afterDetachment);
9797
}
9898

9999
function afterDetachment () {
100100
assert.equal(parentEl.object3D.children.length, 0);
101101
assert.notOk(el2.parentEl);
102102
assert.notOk(el2.parentNode);
103-
assert.notOk(el2.components.geometry);
103+
assert.ok(el2.components.geometry);
104104
assert.isFalse(el2.hasLoaded);
105105

106-
parentEl.appendChild(el2);
107106
el2.addEventListener('loaded', afterSecondAttachment);
107+
parentEl.appendChild(el2);
108108
}
109109

110110
function afterSecondAttachment () {
@@ -544,7 +544,7 @@ suite('a-entity', function () {
544544
assert.notEqual(el.sceneEl.behaviors.tock.indexOf(el.components.test), -1);
545545
parentEl.removeChild(el);
546546
process.nextTick(function () {
547-
assert.notOk('test' in el.components);
547+
assert.ok('test' in el.components);
548548
assert.equal(el.sceneEl.behaviors.tick.indexOf(el.components.test), -1);
549549
assert.equal(el.sceneEl.behaviors.tock.indexOf(el.components.test), -1);
550550
done();

0 commit comments

Comments
 (0)