From c989c1cf1c66171b0ac6beb1c6c132f99a56243a Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Sun, 18 Dec 2022 11:28:48 +0000 Subject: [PATCH 1/9] New attachToScene / detachFromScene methods on entity --- src/core/a-entity.js | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/core/a-entity.js b/src/core/a-entity.js index 6b39e4e0562..5d0217a2d91 100644 --- a/src/core/a-entity.js +++ b/src/core/a-entity.js @@ -37,6 +37,11 @@ class AEntity extends ANode { this.parentEl = null; this.rotationObj = {}; this.states = []; + this.attachedToParent = false; + // tracks attach to / detach from THREE.js Scene graph (independent of DOM attachment) + this.attachedToScene = true; + // Used to preserve object3D's parent when detached from THREE.js scene graph + this.object3DParent = null; } /** @@ -203,7 +208,13 @@ class AEntity extends ANode { if (!el.object3D) { throw new Error("Trying to add an element that doesn't have an `object3D`"); } - this.object3D.add(el.object3D); + if (el.attachedToScene) { + this.object3D.add(el.object3D); + } else { + // store off parent, for a later call to 'attachToScene()' + el.object3DParent = this.object3D; + } + this.emit('child-attached', {el: el}); } @@ -253,11 +264,37 @@ class AEntity extends ANode { remove (el) { if (el) { this.object3D.remove(el.object3D); + el.object3DParent = null; } else { this.parentNode.removeChild(this); } } + /** + * Attach el to THREE.js scene graph + */ + attachToScene () { + this.attachedToScene = true; + + if (this.attachedToParent) { + if (!this.object3DParent) { + this.object3DParent = this.parentNode.object3D; + } + this.object3DParent.add(this.object3D); + } + } + + /** + * Detach el from THREE.js scene graph + */ + detachFromScene () { + this.attachedToScene = false; + this.object3DParent = this.object3D.parent; + if (this.object3DParent) { + this.object3DParent.remove(this.object3D); + } + } + /** * @returns {array} Direct children that are entities. */ From ab7c136f7cbdff93a8216922bec4a46137ce7902 Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Sun, 18 Dec 2022 11:29:45 +0000 Subject: [PATCH 2/9] Detach unused pool entities from THREE.js scene This improves performance of updateMatrixWorld() in render cycle. --- src/components/scene/pool.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/scene/pool.js b/src/components/scene/pool.js index 618b2c76b1a..f9584e43e74 100644 --- a/src/components/scene/pool.js +++ b/src/components/scene/pool.js @@ -61,6 +61,7 @@ module.exports.Component = registerComponent('pool', { el.setAttribute('mixin', this.data.mixin); el.object3D.visible = false; el.pause(); + el.detachFromScene(); this.container.appendChild(el); this.availableEls.push(el); }, @@ -93,6 +94,7 @@ module.exports.Component = registerComponent('pool', { this.createEntity(); } el = this.availableEls.shift(); + el.attachToScene(); this.usedEls.push(el); el.object3D.visible = true; return el; @@ -110,6 +112,7 @@ module.exports.Component = registerComponent('pool', { this.usedEls.splice(index, 1); this.availableEls.push(el); el.object3D.visible = false; + el.detachFromScene(); el.pause(); return el; } From a23686b4dcd730d41031be7618ca10e9f42cd11d Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Sun, 18 Dec 2022 11:30:23 +0000 Subject: [PATCH 3/9] Unit tests for detaching unused pool entities from THREE.js scene --- tests/components/scene/pool.test.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/components/scene/pool.test.js b/tests/components/scene/pool.test.js index d24d64a7b70..db97e856991 100644 --- a/tests/components/scene/pool.test.js +++ b/tests/components/scene/pool.test.js @@ -99,6 +99,23 @@ suite('pool', function () { }); }); + suite('attachmentToThreeScene', function () { + test('Pool entity is not initially attached to scene', function () { + var sceneEl = this.sceneEl; + var poolComponent = sceneEl.components.pool; + assert.equal(poolComponent.availableEls[0].object3D.parent, null); + }); + + test('Pool entity is attached to scene when requested, and detached when released', function () { + var sceneEl = this.sceneEl; + var poolComponent = sceneEl.components.pool; + var el = poolComponent.requestEntity(); + assert.equal(el.object3D.parent, sceneEl.object3D); + poolComponent.returnEntity(el); + assert.equal(el.object3D.parent, null); + }); + }); + suite('wrapPlay', function () { test('cannot play an entity that is not in use', function () { var sceneEl = this.sceneEl; From e16c8a2a453911975615adc18e29adf2f81ea9df Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Sun, 18 Dec 2022 11:31:00 +0000 Subject: [PATCH 4/9] New "attached" component + UTs for it --- src/components/attached.js | 16 +++++++++++ src/components/index.js | 1 + tests/components/attached.test.js | 44 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 src/components/attached.js create mode 100644 tests/components/attached.test.js diff --git a/src/components/attached.js b/src/components/attached.js new file mode 100644 index 00000000000..7974a2a4e51 --- /dev/null +++ b/src/components/attached.js @@ -0,0 +1,16 @@ +var registerComponent = require('../core/component').registerComponent; + +/** + * Attached component. + */ +module.exports.Component = registerComponent('attached', { + schema: {default: true}, + + update: function () { + if (this.data) { + this.el.attachToScene(); + } else { + this.el.detachFromScene(); + } + } +}); diff --git a/src/components/index.js b/src/components/index.js index 4266f71dd14..24edcbe0dc5 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,4 +1,5 @@ require('./animation'); +require('./attached'); require('./camera'); require('./cursor'); require('./daydream-controls'); diff --git a/tests/components/attached.test.js b/tests/components/attached.test.js new file mode 100644 index 00000000000..31653ca6c86 --- /dev/null +++ b/tests/components/attached.test.js @@ -0,0 +1,44 @@ +/* global assert, process, setup, suite, test, THREE */ +var elFactory = require('../helpers').elFactory; + +suite('attached', function () { + var el; + + setup(function (done) { + elFactory().then(_el => { + el = _el; + done(); + }); + }); + + suite('update', function () { + test('treats empty as true', function () { + el.setAttribute('attached', ''); + assert.equal(el.object3D.parent, el.parentNode.object3D); + assert.ok(el.attachedToScene); + }); + + test('can set to attached', function () { + el.setAttribute('attached', true); + assert.equal(el.object3D.parent, el.parentNode.object3D); + assert.ok(el.attachedToScene); + }); + + test('can set to not attached', function () { + el.setAttribute('attached', false); + assert.equal(el.object3D.parent, null); + assert.notOk(el.attachedToScene); + }); + + test('Non-default object3D parent maintained when detached & re-attached', function () { + const alternateParent = new THREE.Group(); + alternateParent.add(el.object3D); + el.setAttribute('attached', false); + assert.equal(el.object3D.parent, null); + assert.notOk(el.attachedToScene); + el.setAttribute('attached', true); + assert.equal(el.object3D.parent, alternateParent); + assert.ok(el.attachedToScene); + }); + }); +}); From 7234e15de051753c2b1a9ba94b1c9b8bd0ab0dec Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Sun, 18 Dec 2022 12:16:32 +0000 Subject: [PATCH 5/9] Handle getAttribute similarly to 'visible' --- src/core/a-entity.js | 1 + tests/components/attached.test.js | 9 +++++++++ tests/components/visible.test.js | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/src/core/a-entity.js b/src/core/a-entity.js index 5d0217a2d91..915d55d00f8 100644 --- a/src/core/a-entity.js +++ b/src/core/a-entity.js @@ -769,6 +769,7 @@ class AEntity extends ANode { if (attr === 'rotation') { return getRotation(this); } if (attr === 'scale') { return this.object3D.scale; } if (attr === 'visible') { return this.object3D.visible; } + if (attr === 'attached') { return this.attachedToScene; } component = this.components[attr]; if (component) { return component.data; } return window.HTMLElement.prototype.getAttribute.call(this, attr); diff --git a/tests/components/attached.test.js b/tests/components/attached.test.js index 31653ca6c86..ac2ba1a1078 100644 --- a/tests/components/attached.test.js +++ b/tests/components/attached.test.js @@ -40,5 +40,14 @@ suite('attached', function () { assert.equal(el.object3D.parent, alternateParent); assert.ok(el.attachedToScene); }); + + test('getAttribute is affected by changes made direct to entity', function () { + el.setAttribute('attached', true); + assert.ok(el.getAttribute('attached')); + el.detachFromScene(); + assert.notOk(el.getAttribute('attached')); + el.attachToScene(); + assert.ok(el.getAttribute('attached')); + }); }); }); diff --git a/tests/components/visible.test.js b/tests/components/visible.test.js index 96d356824bb..b2c78dc4fec 100644 --- a/tests/components/visible.test.js +++ b/tests/components/visible.test.js @@ -27,5 +27,12 @@ suite('visible', function () { el.setAttribute('visible', false); assert.notOk(el.object3D.visible); }); + + test('getAttribute is affected by changes to Object3D.visible', function () { + el.setAttribute('visible', true); + assert.ok(el.getAttribute('visible')); + el.object3D.visible = false; + assert.notOk(el.getAttribute('visible')); + }); }); }); From 4fc237fe5cc0e275513a6068fcb3e1afa13216fd Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Sun, 18 Dec 2022 12:29:45 +0000 Subject: [PATCH 6/9] Documentation updates for attached component & entity interface changes. --- docs/components/attached.md | 51 +++++++++++++++++++++++++++++++++++++ docs/components/pool.md | 2 ++ docs/components/visible.md | 9 +++++-- docs/core/entity.md | 22 ++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 docs/components/attached.md diff --git a/docs/components/attached.md b/docs/components/attached.md new file mode 100644 index 00000000000..16771319cf9 --- /dev/null +++ b/docs/components/attached.md @@ -0,0 +1,51 @@ +--- +title: attached +type: components +layout: docs +parent_section: components +source_code: src/components/attached.js +examples: [] +--- + +The attached component determines whether an entity is attached to the THREE.js scene graph at all. + +All entities are attached by default. Detaching an entity from the scene means that the entity and its descendants will have no interactions in the 3D scene at all: it is not rendered, it will not interact with raycasters. + +This is similar to the [visible](visible) component, but places even more limitations on the entity. + +Invisible entities are not rendered, but their position in space is still updated every frame, and they can interact with raycasters, be checked for collisions etc. + +In contrast, when an entity is detached from the scene, by setting `attached="false"`, even these interactions do not occur, which will improve performance even further vs. just making an entity invisible. For example, entity pools implemented by the [pool](pool) component detach entities in the pool when they are not in use. + +It's a common pattern to create container entities that contain an entire group of entities that you can flip on an off with `attached`. + + +## Example + +```html +<a-entity attached="false"></a-entity> +``` + +## Value + +| Value | Description | +|-------|----------------------------------------------------------------------------------------| +| true | The entity will be rendered and visible; the default value. | +| false | The entity will be detached from the THREE.js scene. It will not be rendered nor + visible, and will not interact with anything else in the scene. | + +## Updating Attachment + +It is slightly faster to control attachment to the THREE.js scene using direct calls to [`attachToScene()`](../core/entity#attachtoscene) and [`detachFromScene()`](../core/entity#detachfromscene): + +```js +// direct use of entity interface +el.detachFromScene() + +// with setAttribute. +e.setAttribute('attached', false) + +``` + +Updates at the three.js level will still be reflected when doing +`entityEl.getAttribute('attached');`. diff --git a/docs/components/pool.md b/docs/components/pool.md index f9405fb16a3..0c7d5fd4289 100644 --- a/docs/components/pool.md +++ b/docs/components/pool.md @@ -15,6 +15,8 @@ entities in dynamic scenes. Object pooling helps reduce garbage collection pause Note that entities requested from the pool are paused by default and you need to call `.play()` in order to activate their components' tick functions. +For performance reasons, unused entities in the pool are [detached from the THREE.js scene](attached). + ## Example For example, we may have a game with enemy entities that we want to reuse. diff --git a/docs/components/visible.md b/docs/components/visible.md index ffd7ae61eb8..6f52a3a94fe 100644 --- a/docs/components/visible.md +++ b/docs/components/visible.md @@ -12,8 +12,13 @@ The visible component determines whether to render an entity. If set to Visibility effectively applies to all children. If an entity's parent or ancestor entity has visibility set to false, then the entity will also not be -visible nor draw. It's a common pattern to create container entities that -contain an entire group of entities that you can flip on an off with `visible`. +visible nor draw. However, the entity's position in space is still maintained, +so it can still iuinteract with raycasters, be checked for collisions etc. + +When you want an entity or group of entities to have no interactions at all, it +is usually preferable (for performance reasons) to detach them from the THREE.js scene +entirely by toggling the [`attached`](attached) component. + ## Example diff --git a/docs/core/entity.md b/docs/core/entity.md index 5f7d439e7ba..bc4bd7ad776 100644 --- a/docs/core/entity.md +++ b/docs/core/entity.md @@ -168,10 +168,32 @@ entity.addState('selected'); entity.is('selected'); // >> true ``` +### `attachToScene ()` + +`attachToScene` can be used to re-attach an entity to the THREE.js scene that has been detached +using `detachFromScene`. + +See [`detachFromScene`](#detachfromscene) for more explanation. + ### `destroy ()` Clean up memory related to the entity such as clearing all components and their data. +### `detachFromScene ()` + +`detachFromScene` will detach the element from the THREE.js scene, typically for performance reasons. +Detaching an entity from the scene means that the entity and its descendants will have no interactions +in the 3D scene at all: it is not rendered, it will not interact with raycasters. + +The main reason for detaching an entity from the scene, rather than destroying it are: +- to maintain state on the entity +- for performance reasons, as it is typically faster to re-attach an entity to the scene, than to recreate it and all its descendants. + +For performance reasons, entity pools implemented by the [pool](pool) component detach entities in the pool when they are not in use. + +Attachment to the THREE.js scene can also be controlled through the [attached](attached) component. + + ### `emit (name, detail, bubbles)` [animation]: ../components/animation.md From 697fb5713879f5f00a34b3432281c08f1de1fe9c Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Sun, 18 Dec 2022 12:44:23 +0000 Subject: [PATCH 7/9] Fix links in docs --- docs/components/attached.md | 11 ++++++++--- docs/components/pool.md | 4 +++- docs/components/visible.md | 5 +++-- docs/core/entity.md | 11 ++++++++--- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/components/attached.md b/docs/components/attached.md index 16771319cf9..991ef560173 100644 --- a/docs/components/attached.md +++ b/docs/components/attached.md @@ -7,15 +7,20 @@ source_code: src/components/attached.js examples: [] --- +[visible]: ./visible.md +[pool]: ./pool.md +[attach]: ../core/entity.md#attachtoscene- +[detach]: ../core/entity.md#detachfromscene- + The attached component determines whether an entity is attached to the THREE.js scene graph at all. All entities are attached by default. Detaching an entity from the scene means that the entity and its descendants will have no interactions in the 3D scene at all: it is not rendered, it will not interact with raycasters. -This is similar to the [visible](visible) component, but places even more limitations on the entity. +This is similar to the [visible][visible] component, but places even more limitations on the entity. Invisible entities are not rendered, but their position in space is still updated every frame, and they can interact with raycasters, be checked for collisions etc. -In contrast, when an entity is detached from the scene, by setting `attached="false"`, even these interactions do not occur, which will improve performance even further vs. just making an entity invisible. For example, entity pools implemented by the [pool](pool) component detach entities in the pool when they are not in use. +In contrast, when an entity is detached from the scene, by setting `attached="false"`, even these interactions do not occur, which will improve performance even further vs. just making an entity invisible. For example, entity pools implemented by the [pool][pool] component detach entities in the pool when they are not in use. It's a common pattern to create container entities that contain an entire group of entities that you can flip on an off with `attached`. @@ -36,7 +41,7 @@ It's a common pattern to create container entities that contain an entire group ## Updating Attachment -It is slightly faster to control attachment to the THREE.js scene using direct calls to [`attachToScene()`](../core/entity#attachtoscene) and [`detachFromScene()`](../core/entity#detachfromscene): +It is slightly faster to control attachment to the THREE.js scene using direct calls to [`attachToScene()`][attach] and [`detachFromScene()`][detach]: ```js // direct use of entity interface diff --git a/docs/components/pool.md b/docs/components/pool.md index 0c7d5fd4289..e19cddaffb9 100644 --- a/docs/components/pool.md +++ b/docs/components/pool.md @@ -7,6 +7,8 @@ source_code: src/components/scene/pool.js examples: [] --- +[attached]: ./attached.md + The pool component allows for [object pooling](https://en.wikipedia.org/wiki/Object_pool_pattern). This gives us a reusable pool of entities to avoid creating and destroying the same kind of @@ -15,7 +17,7 @@ entities in dynamic scenes. Object pooling helps reduce garbage collection pause Note that entities requested from the pool are paused by default and you need to call `.play()` in order to activate their components' tick functions. -For performance reasons, unused entities in the pool are [detached from the THREE.js scene](attached). +For performance reasons, unused entities in the pool are [detached from the THREE.js scene][attached]. ## Example diff --git a/docs/components/visible.md b/docs/components/visible.md index 6f52a3a94fe..3830b64a9f3 100644 --- a/docs/components/visible.md +++ b/docs/components/visible.md @@ -6,6 +6,7 @@ parent_section: components source_code: src/components/visible.js examples: [] --- +[attached]: ./attached.md The visible component determines whether to render an entity. If set to `false`, then the entity will not be visible nor drawn. @@ -17,7 +18,7 @@ so it can still iuinteract with raycasters, be checked for collisions etc. When you want an entity or group of entities to have no interactions at all, it is usually preferable (for performance reasons) to detach them from the THREE.js scene -entirely by toggling the [`attached`](attached) component. +entirely by toggling the [`attached`][attached] component. ## Example @@ -35,7 +36,7 @@ entirely by toggling the [`attached`](attached) component. ## Updating Visibility -[update]: ../introduction/javascript-events-and-dom-apis.md#updating-a-component-with-setattribute +[update]: ../introduction/javascript-events-dom-apis.md#updating-a-component-with-setattribute- It is slightly faster to update visibility at the three.js level versus [via `.setAttribute`][update]. diff --git a/docs/core/entity.md b/docs/core/entity.md index bc4bd7ad776..7c00b2058cc 100644 --- a/docs/core/entity.md +++ b/docs/core/entity.md @@ -170,10 +170,12 @@ entity.is('selected'); // >> true ### `attachToScene ()` +[detach]: #detachfromscene- + `attachToScene` can be used to re-attach an entity to the THREE.js scene that has been detached using `detachFromScene`. -See [`detachFromScene`](#detachfromscene) for more explanation. +See [`detachFromScene`][detach] for more explanation. ### `destroy ()` @@ -181,6 +183,9 @@ Clean up memory related to the entity such as clearing all components and their ### `detachFromScene ()` +[pool]: ../components/pool.md +[attached]: ../components/attached.md + `detachFromScene` will detach the element from the THREE.js scene, typically for performance reasons. Detaching an entity from the scene means that the entity and its descendants will have no interactions in the 3D scene at all: it is not rendered, it will not interact with raycasters. @@ -189,9 +194,9 @@ The main reason for detaching an entity from the scene, rather than destroying i - to maintain state on the entity - for performance reasons, as it is typically faster to re-attach an entity to the scene, than to recreate it and all its descendants. -For performance reasons, entity pools implemented by the [pool](pool) component detach entities in the pool when they are not in use. +For performance reasons, entity pools implemented by the [pool][pool] component detach entities in the pool when they are not in use. -Attachment to the THREE.js scene can also be controlled through the [attached](attached) component. +Attachment to the THREE.js scene can also be controlled through the [attached][attached] component. ### `emit (name, detail, bubbles)` From 9c031299cd61f5eb78dd98cc934f521a73c1000a Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Mon, 19 Dec 2022 09:45:30 +0000 Subject: [PATCH 8/9] Add events to auto-update raycast targets --- docs/core/entity.md | 7 +++- src/components/raycaster.js | 20 +++++++-- src/core/a-entity.js | 3 ++ tests/components/attached.test.js | 2 +- tests/components/raycaster.test.js | 50 ++++++++++++++++++++++ tests/core/a-entity.test.js | 67 ++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 6 deletions(-) diff --git a/docs/core/entity.md b/docs/core/entity.md index 7c00b2058cc..1d2aaf56121 100644 --- a/docs/core/entity.md +++ b/docs/core/entity.md @@ -168,10 +168,11 @@ entity.addState('selected'); entity.is('selected'); // >> true ``` -### `attachToScene ()` - +[attach]: #attachtoscene- [detach]: #detachfromscene- +### `attachToScene ()` + `attachToScene` can be used to re-attach an entity to the THREE.js scene that has been detached using `detachFromScene`. @@ -479,6 +480,8 @@ entity.is('selected'); // >> false |----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | child-attached | A child entity was attached to the entity. | | child-detached | A child entity was detached from the entity. | +| attached-to-scene | The entity was attached to the THREE.js scene. This occurs on initial entity creation, and following subsequent calls to [`attachToScene()`][attach] | +| detached-from-scene | The entity was detached from the THREE.js scene. This occurs a call to to [`detachFromScene()`][detach] (unless the call is made durign entity creation, prior to the entity being attached to the THREE.js scene). | | componentchanged | One of the entity's components was modified. This event is throttled. Do not use this for reading position and rotation changes, rather [use a tick handler](../camera.md#reading-position-or-rotation-of-the-camera). | | componentinitialized | One of the entity's components was initialized. | | componentremoved | One of the entity's components was removed. | diff --git a/src/components/raycaster.js b/src/components/raycaster.js index b23652f0b3a..c1bdf2400e6 100644 --- a/src/components/raycaster.js +++ b/src/components/raycaster.js @@ -154,12 +154,16 @@ module.exports.Component = registerComponent('raycaster', { this.observer.observe(this.el.sceneEl, OBSERVER_CONFIG); this.el.sceneEl.addEventListener('object3dset', this.setDirty); this.el.sceneEl.addEventListener('object3dremove', this.setDirty); + this.el.sceneEl.addEventListener('attached-to-scene', this.setDirty); + this.el.sceneEl.addEventListener('detached-from-scene', this.setDirty); }, removeEventListeners: function () { this.observer.disconnect(); this.el.sceneEl.removeEventListener('object3dset', this.setDirty); this.el.sceneEl.removeEventListener('object3dremove', this.setDirty); + this.el.sceneEl.removeEventListener('attached-to-scene', this.setDirty); + this.el.sceneEl.removeEventListener('detached-from-scene', this.setDirty); }, /** @@ -409,13 +413,23 @@ module.exports.Component = registerComponent('raycaster', { var key; var i; var objects = this.objects; + var scene = this.el.sceneEl.object3D; + + function isAttachedToScene (object) { + if (object.parent) { + return isAttachedToScene(object.parent); + } else { + return (object === scene); + } + } // Push meshes and other attachments onto list of objects to intersect. objects.length = 0; for (i = 0; i < els.length; i++) { - if (els[i].isEntity && els[i].object3D) { - for (key in els[i].object3DMap) { - objects.push(els[i].getObject3D(key)); + var el = els[i]; + if (el.isEntity && el.object3D && isAttachedToScene(el.object3D)) { + for (key in el.object3DMap) { + objects.push(el.getObject3D(key)); } } } diff --git a/src/core/a-entity.js b/src/core/a-entity.js index 915d55d00f8..8330befdbde 100644 --- a/src/core/a-entity.js +++ b/src/core/a-entity.js @@ -210,6 +210,7 @@ class AEntity extends ANode { } if (el.attachedToScene) { this.object3D.add(el.object3D); + el.emit('attached-to-scene'); } else { // store off parent, for a later call to 'attachToScene()' el.object3DParent = this.object3D; @@ -281,6 +282,7 @@ class AEntity extends ANode { this.object3DParent = this.parentNode.object3D; } this.object3DParent.add(this.object3D); + this.emit('attached-to-scene'); } } @@ -292,6 +294,7 @@ class AEntity extends ANode { this.object3DParent = this.object3D.parent; if (this.object3DParent) { this.object3DParent.remove(this.object3D); + this.emit('detached-from-scene'); } } diff --git a/tests/components/attached.test.js b/tests/components/attached.test.js index ac2ba1a1078..1dcfa6de118 100644 --- a/tests/components/attached.test.js +++ b/tests/components/attached.test.js @@ -31,7 +31,7 @@ suite('attached', function () { }); test('Non-default object3D parent maintained when detached & re-attached', function () { - const alternateParent = new THREE.Group(); + var alternateParent = new THREE.Group(); alternateParent.add(el.object3D); el.setAttribute('attached', false); assert.equal(el.object3D.parent, null); diff --git a/tests/components/raycaster.test.js b/tests/components/raycaster.test.js index f2a1324c8a3..89425d57956 100644 --- a/tests/components/raycaster.test.js +++ b/tests/components/raycaster.test.js @@ -118,6 +118,44 @@ suite('raycaster', function () { sceneEl.appendChild(el2); sceneEl.appendChild(el3); }); + + test('Objects not attached to scene are not whitelisted', function (done) { + var el2 = document.createElement('a-entity'); + var el3 = document.createElement('a-entity'); + el2.setAttribute('class', 'clickable'); + el2.setAttribute('geometry', 'primitive: box'); + el3.setAttribute('class', 'clickable'); + el3.setAttribute('geometry', 'primitive: box'); + el3.detachFromScene(); + el3.addEventListener('loaded', function () { + el.setAttribute('raycaster', 'objects', '.clickable'); + component.tock(); + assert.equal(component.objects.length, 1); + assert.equal(component.objects[0], el2.object3D.children[0]); + assert.equal(el2, el2.object3D.children[0].el); + done(); + }); + sceneEl.appendChild(el2); + sceneEl.appendChild(el3); + }); + + test('Objects with parent not attached to scene are not whitelisted', function (done) { + var el2 = document.createElement('a-entity'); + var el3 = document.createElement('a-entity'); + el2.setAttribute('class', 'clickable'); + el2.setAttribute('geometry', 'primitive: box'); + el3.setAttribute('class', 'clickable'); + el3.setAttribute('geometry', 'primitive: box'); + el2.detachFromScene(); + el3.addEventListener('loaded', function () { + el.setAttribute('raycaster', 'objects', '.clickable'); + component.tock(); + assert.equal(component.objects.length, 0); + done(); + }); + sceneEl.appendChild(el2); + el2.appendChild(el3); + }); }); suite('tock', function () { @@ -215,6 +253,18 @@ suite('raycaster', function () { sceneEl.emit('object3dremove'); assert.equal(component.dirty, true); }); + + test('refresh objects when attachToScene() or detachFromScene() is called', function () { + el.setAttribute('raycaster', {objects: '[ray-target]'}); + component.tock(); + assert.equal(component.dirty, false); + sceneEl.emit('attached-to-scene'); + assert.equal(component.dirty, true); + component.tock(); + assert.equal(component.dirty, false); + sceneEl.emit('detached-from-scene'); + assert.equal(component.dirty, true); + }); }); suite('raycaster', function () { diff --git a/tests/core/a-entity.test.js b/tests/core/a-entity.test.js index deaa5585c63..807cf944f93 100644 --- a/tests/core/a-entity.test.js +++ b/tests/core/a-entity.test.js @@ -1585,6 +1585,73 @@ suite('a-entity component lifecycle management', function () { assert.ok(destroySpy.callCount); }); }); + + suite('attach / detach to / from scene', function () { + test('emits attached-to-scene event when attached to THREE.js scene', function (done) { + const el2 = document.createElement('a-entity'); + el2.object3D = new THREE.Mesh(); + el2.addEventListener('attached-to-scene', function (event) { + assert.equal(event.target, el2); + el2.addEventListener('loaded', function () { + done(); + }); + }); + el.appendChild(el2); + }); + + test('attached-to-scene event is delayed when entity detached from scene during creation', function (done) { + const el2 = document.createElement('a-entity'); + var detachEventReceived = false; + el2.object3D = new THREE.Mesh(); + + const detachListener = function (event) { + detachEventReceived = true; + }; + el2.addEventListener('detached-from-scene', detachListener); + el2.addEventListener('loaded', function () { + el2.addEventListener('attached-to-scene', function (event) { + // no detached-from-scene event received, as entity was never attached. + assert.notOk(detachEventReceived); + assert.equal(event.target, el2); + el2.removeEventListener('detached-from-scene', detachListener); + done(); + }); + el2.attachToScene(); + }); + el2.detachFromScene(); + el.appendChild(el2); + }); + + test('emits detached-from-scene event when detached from THREE.js scene', function (done) { + const el2 = document.createElement('a-entity'); + el2.object3D = new THREE.Mesh(); + el2.addEventListener('attached-to-scene', function (event) { + assert.equal(event.target, el2); + el2.addEventListener('detached-from-scene', function (event) { + assert.equal(event.target, el2); + done(); + }); + el2.detachFromScene(); + }); + el.appendChild(el2); + }); + + test('detachment from scene after entity loaded', function (done) { + const el2 = document.createElement('a-entity'); + el2.object3D = new THREE.Mesh(); + el2.addEventListener('attached-to-scene', function (event) { + assert.equal(event.target, el2); + el2.addEventListener('loaded', function (event) { + el2.addEventListener('detached-from-scene', function (event) { + assert.equal(event.target, el2); + done(); + }); + el2.detachFromScene(); + }); + }); + el.appendChild(el2); + }); + }); }); suite('a-entity component dependency management', function () { From 1e5762c614a2a0ae8702c1f1b05f0f361ea2b8c9 Mon Sep 17 00:00:00 2001 From: diarmidmackenzie <diarmid.mackenzie@gmail.com> Date: Mon, 19 Dec 2022 10:17:54 +0000 Subject: [PATCH 9/9] Work around docsLint.js error docsLint.js appears to not like URLs that end with a "-", though this is necessary to link to the anchors for these functions... [error]: docs/components/attached.md Page does not exist: [attach]: ../core/entity.md#attachtoscene- Page does not exist: [detach]: ../core/entity.md#detachfromscene- --- docs/components/attached.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/components/attached.md b/docs/components/attached.md index 991ef560173..342df30228e 100644 --- a/docs/components/attached.md +++ b/docs/components/attached.md @@ -9,8 +9,6 @@ examples: [] [visible]: ./visible.md [pool]: ./pool.md -[attach]: ../core/entity.md#attachtoscene- -[detach]: ../core/entity.md#detachfromscene- The attached component determines whether an entity is attached to the THREE.js scene graph at all. @@ -41,7 +39,7 @@ It's a common pattern to create container entities that contain an entire group ## Updating Attachment -It is slightly faster to control attachment to the THREE.js scene using direct calls to [`attachToScene()`][attach] and [`detachFromScene()`][detach]: +It is slightly faster to control attachment to the THREE.js scene using direct calls to [`attachToScene()`](../core/entity.md#attachtoscene-) and [`detachFromScene()`](../core/entity.md#detachfromscene-): ```js // direct use of entity interface