Object3DFeature
Base class for implementing reusable components (features) that can be added to any THREE.Object3D
. Features attach to the context when their object is added to ctx.root
hierarchy, and lose it when removed, or forcibly on call.
- Context lifecycle and its modules access through overridable
useCtx
. - Built-in overridable lifecycle event methods like
onBeforeRender
etc. - Direct access to object
this.object
the feature is attached to.
Examples
//TODO short examples
Usage
export class FeatureExample extends KVY.Object3DFeature {
constructor(object, props) {
super(object);
this.someValue = props.someValue;
this.otherProp = props.otherProp;
}
// override built-in lifecycle event methods
useCtx(ctx) {
console.log("FeatureExample: ctx was attached 🌐");
// use attached ctx on its attach;
ctx.deltaTime;
ctx.three.scene;
ctx.three.camera;
// use ctx modules you defined;
ctx.modules.moduleExample;
return () => {
console.log("FeatureExample: ctx was detached 🚫");
// do cleanup on ctx detach
}
}
onBeforeRender(ctx) {
console.log('before render');
// use attached ctx;
const dt = ctx.deltaTime;
// use object this feature is attached to
const obj = this.object;
obj.rotateY(dt * this.speed);
}
onDestroy() {
console.log("FeatureExample: destroyed 💥")
}
onLoopStop(ctx) {
console.log('ctx loop stopped');
}
onResize(ctx) {
console.log('renderer canvas resized');
}
onLoopRun(ctx) { ... }
onAfterRender(ctx) { ... }
}
class AnotherFeature extends KVY.Object3DFeature {
useCtx(ctx) {
console.log("AnotherFeature: ctx was attached 🌐");
return () => console.log("AnotherFeature: ctx was detached 🚫");
}
onDestroy() {
console.log("AnotherFeature: destroyed 💥")
}
}
How to work with features?
Read the section "Add, get, clear features" to learn all the static methods for managing features addFeature
, getFeature
, getFeatureBy
, and clear
to work with them.
Add features
We add features to the object. It is important that we provide the class itself as an argument in addFeature
, not an instance of the feature. The third and last argument is optional if the feature class constructor has its own props.
const obj = new THREE.Object3D();
const example = KVY.addFeature(obj, FeatureExample, { speed: 7 });
const feature = KVY.addFeature(obj, AnotherFeature);
But it should be remembered that for the features to "work", the object with features must be attached to the hierarchy that is in ctx.root
.
So we add obj
to ctx.root
.
const ctx = KVY.CoreContext.create(THREE, { moduleExample: new ModuleExample() });
ctx.root.add(obj)
// >>> "FeatureExample: ctx was attached 🌐"
// >>> "AnotherFeature: ctx was attached 🌐"
And so, the context was attached, and useCtx
was called!
Similarly, if object is removed from the hierarchy ctx.root
, the context is detached. The cleanup function defined in useCtx
is called.
ctx.root.remove(obj);
// >>> "FeatureExample: ctx was detached 🚫"
// >>> "AnotherFeature: ctx was detached 🚫"
Get features, destroy them
We can also retrieve the features of an object or destroy them
const exampleFeature = KVY.getFeature(obj, FeatureExample); // -> FeatureExample | null
const someFeature = KVY.getFeatureBy(obj, (f) => f.isSmth); // -> Object3DFeature | null
const featureList = KVY.getFeatures(obj) // -> Object3DFeature[] | null
featureList?.forEach((feature) => {
console.log(feature); // Object3DFeature
})
exampleFeature?.destroy();
// >>> "FeatureExample: destroyed 💥"
KVY.getFeature(obj, AnotherFeature)?.destroy();
Context Propagation. Attach and Detach.
Attention! This is a very important section. Be very attentive please.
How does it attach to features?
As soon as an object with added features (Object3DFeature
) or its hierarchy is added to ctx.root
or to a hierarchy that has at any top-level ancestor ctx.root
, the context will attach to all features of this object.
Let's illustrate how this happens.
We will implement a test feature MyFeature
with console logs for subsequent explanations of when things happen.
class MyFeature extends KVY.Object3DFeature {
constructor(object) {
super(object);
console.log(`${this.object.name} - MyFeature: inited`);
}
useCtx(ctx) {
console.log(`${this.object.name} - MyFeature: ctx was attached!`);
return () => console.log('MyFeature: ctx was detached!');
}
onBeforeRender(ctx) {
console.log(`${this.object.name} - MyFeature: render`)
}
onDestroy() {
console.log(`${this.object.name} - MyFeature: destroyed`);
}
}
Now an example that shows when what will be called.
Let's assume that the context ctx
is already initialized, mounted, and the loop is running.
const obj = new THREE.Object3D();
obj.name = "obj";
KVY.addFeature(obj, MyFeature);
// >>> "obj - MyFeature: inited"
ctx.root.add(obj);
// >>> "obj - MyFeature: ctx was attached"
// >>> "obj - MyFeature: render"
// >>> "obj - MyFeature: render"
// >>> "MyFeature: render"
// >>> ...
ctx.root.remove(obj);
// >>> "obj - MyFeature: ctx was detached"
This can also be done in a different order. You can also get a reference to the created instance of the feature featureInstance
.
const obj = new THREE.Object3D();
ctx.root.add(obj);
const featureInstance = KVY.addFeature(obj, MyFeature);
// >>> "obj - MyFeature: inited"
// >>> "obj - MyFeature: ctx was attached"
// >>> "obj - MyFeature: render"
// >>> "obj - MyFeature: render"
// >>> "obj - MyFeature: render"
// >>> ...
And when the feature is removed, it will be like this.
Thus, if the object obj
is added back to ctx.root
, we see that nothing happens since the feature has been removed.
featureInstance.destroy()
// >>> "obj - MyFeature: ctx was detached"
// >>> "obj - MyFeature: destroyed"
ctx.root.add(obj);
// nothing happens
KVY.getFeature(obj, MyFeature) // -> null
KVY.getFeatures(obj) // -> []
If anything, you can add features of the same type to one object in any quantity.
What if I don't want to add objects directly to root?
No problem. But the main thing is that at some upper level there should be an ancestor of ctx.root
.
Let's imagine that we have some hierarchy of 3D objects.
For example, like this:
[Scene] ctx.root
├── [Group] group_a
├── [Group] group_b
│ ├── [Object3D] obj_b1
│ │ ├── [Mesh] mesh_b1a
│ │ ├── [Mesh] mesh_b1b
│ │ └── [Sprite] sprite_b1c
│ ├── [Light] light_b3
│ └── [Object3D] obj_b4
Let's take a few objects for example and attach the feature MyFeature
with logs that we wrote earlier.
light_b3.addFeature(MyFeature)
// >>> 'light_b3 - MyFeature: inited'
// >>> 'light_b3 - MyFeature: ctx was attached'
light_b3.addFeature(MyFeature)
// >>> 'light_b3 - MyFeature: inited'
// >>> 'light_b3 - MyFeature: ctx was attached'
mesh_b1b.addFeature(MyFeature)
// >>> 'mesh_b1b - MyFeature: inited'
// >>> 'mesh_b1b - MyFeature: ctx was attached'
group_a.addFeature(MyFeature)
// >>> 'group_a - MyFeature: inited'
// >>> 'group_a - MyFeature: ctx was attached'
// ...
Or we can add a new object with a feature to any place in this hierarchy
const newObj = new THREE.Object3D();
newObj.addFeature(MyFeature);
// >>> "newObj - MyFeature: inited"
group_b.add(newObj);
// >>> "newObj - MyFeature: ctx was attached"
// ...
The context attaches to features in such a hierarchy.
When a feature is added to an object, a search for the root and the context to which it belongs is performed upwards through the hierarchy of ancestors.
Similarly, if an object with features is added to some hierarchy.
The main thing
is that the hierarchy to which an object with features is added must already have ctx.root
-ancestor at any level above. Then the context will attach. Either, when adding a feature, the object must already be in such a "ctx.root
-hierarchy".
So, when does detach happen?
The context will be detached from features in the following cases:
1. Object with features removed from its direct parent.
This will also lead to the context being detached from all descendants.
[Scene] ctx.root
├── ...
│ ├── [Object3D] objParent
│ │ ├── [Object3D] obj
│ │ │ ├── ...
│ │ │ │ ├── [Object3D] objDescendant
obj.addFeature(MyFeature);
// >>> "obj - MyFeature - inited"
// >>> "obj - MyFeature: ctx was attached"
objDescendant.addFeature(MyFeature);
// >>> "objDescendant - MyFeature - inited"
// >>> "objDescendant - MyFeature: ctx was attached"
objParent.remove(obj);
// or
obj.removeFromParent();
// >>> "obj - MyFeature: ctx was detached"
// >>> "objDescendant - MyFeature: ctx was detached"
2. Feature destroyed
ctx.root.add(obj);
const feature = obj.addFeature(MyFeature);
// >>> "obj - MyFeature - inited"
// >>> "obj - MyFeature: ctx was attached"
feature.destroy();
// >>> "obj - MyFeature: ctx was detached"
3. Context destroyed
ctx.root.add(obj);
obj.addFeature(MyFeature);
// >>> "obj - MyFeature - inited"
// >>> "obj - MyFeature: ctx was attached"
ctx.destroy();
// ...
// >>> "obj - MyFeature: ctx was detached"
// >>> "obj - MyFeature: destroyed"
This will also lead to the features attached to the destroyed context ctx
being destroyed as well.
API
Base abstract class for implementing reusable components (features) that can be added to any THREE.Object3D
.
Extends EventEmitter
.
Static methods
log( target: Object3DFeature, msg: String ): undefined
Static method for overriding to handle logs.
Object3DFeature.log = (target, msg) => {
console.log(`Feature-${target.id}, Object-${target.object.id}:`, msg);
}
generateUUID(): String
Generates a unique identifier for Object3DFeature
instances.
By default, it uses crypto.randomUUID()
, but in case of fall back, it returns ${Math.random()}-${Date.now()}
. You can freely override this static method to any of your own generation:
CoreContext.generateUUID = () => nanoid(10)
Properties
isObject3DFeature: Boolean
(readonly) Flag to mark that it is an instance of Object3DFeature
.
id: Integer
(readonly) Unique increment number for this feature instance.
uuid: String
UUID of feature instance. This gets automatically assigned, so this shouldn't be edited. Its generation way can be changed via overriding Object3DFeature.generateUUID
static method.
object: Object3D
(readonly) An instance of Three.js Object3D
which this feature was added to.
ctx: CoreContext
(readonly) Getter for the current attached CoreContext
. Throws exception if try to access before it is attached.
hasCtx: Boolean
(readonly) Flag to check if this feature has attached CoreContext
.
Methods
destroy(): undefined
Destroys this feature instance.
Overridable Lifecycle Methods
useCtx( ctx: CoreContext ): Function
Overridable Lifecycle Method. Called when some CoreContext
is attached to this feature. The defined returned cleanup function (optional) is called when the context is detached from the feature. It is prohibitted to be called manually.
ctx
- An instance ofCoreContext
which was attached to this feature.
useCtx(ctx) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial());
this.object.add(mesh);
const listener = () => {
// ...
}
const customTicker = ctx.modules.customTicker;
customTicker.on("tick", listener);
return () => {
this.object.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
customTicker.off("tick", listener);
}
}
onDestroy()
When this feature destroy()
is called.
onBeforeRender( ctx: CoreContext )
Before render is called. On each frame after loop run ctx.run()
or ctx.three.render()
is called manually.
onAfterRender( ctx: CoreContext )
After render is called. On each frame after loop run ctx.run()
or ctx.three.render()
is called manually.
onLoopRun( ctx: CoreContext )
When ctx.run()
is called.
onLoopStop( ctx: CoreContext )
When ctx.stop()
is called.
onMount( ctx: CoreContext )
When ctx.three.mount(container)
is called.
onUnmount( ctx: CoreContext )
When ctx.three.unmount()
is called.
onResize( ctx: CoreContext )
When container (where mounted) is resized.