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

FeatureExample.js
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.

MyFeature.js
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:

hierarchy.txt
[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 of CoreContext 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.

Source

src/core/Object3DFeature.ts

three-kvy-core ...