Extending THREE objects in r121 Module version

So I have had code for some time now that extends the THREE.Mesh object.

Something like:

THREE.Mesh.prototype.something = function(){};

Since SkinnedMesh is constructed from Mesh, this use to extend to SkinnedMesh as well.

It seems the module version of Three121 has broke this ability. It works fine in the Three121 regular version and the Three120 module build as well. Something in Three121 module version changed this behavior.

I’ve already re-written my game once to use modules since three has decided to no longer build or maintain examples for the non-module version. Now on just the next release I am again needing to re-write a bunch of code. This is becoming hard to keep up with.

Can anyone speak to this? Is this a bug or was it intentional?

Edit* Sorry I have been using prototype. I left it out of this by mistake.

  1. The fact that JS allows you to do something and doesn’t throw errors, doesn’t mean you should do it. You are kinda hacking the library and then blaming updates for breaking your hacks :man_shrugging:? If you need additional functionality, create wrappers around Mesh class and add them there - modifying library on the go is virtually asking for it to break at some point (and require partial re-write.)

  2. You may try changing:

THREE.Mesh.something = fn () {}

to

THREE.Mesh.prototype.something = fn () {}

Though I can’t promise it will help. The best solution would be to create a separate object that contains the Mesh and has the additional functionality you need.

1 Like

I tried with the prototype as well, no luck.

I suppose I’ll just have to do some re-writes and also figure out why some other 3rd party functionality stopped working as well.

This all worked fine for like 7 years lol. Oh well.

I consider modifying the prototype more of a feature of Javascript though it can get you in trouble. I agree that derived classes are better practice but there are some cases where it’s not possible or just far too cumbersome to do. Specifically the cases I’ve modified the prototype are to add raycast functionality using BVHs for all Mesh and adding an extra event when adding children to performantly track all the objects in a subhierarchy. I usually try to limit the modification to overriding or extending existing functions, though, and use it sparingly. Completely custom functions are riskier because you may wind up overwriting a new function added to THREE with the same name.

@titansoftime

This is the pattern I’ve always used:

import { Mesh } from 'three';

// extending an existing function
const originalRaycast = Mesh.prototype.raycast;
Mesh.prototype.raycast = function(...args) {

    const result = originalRaycast.apply( this, args );

    // custom logic...

    return result;

};

// Or you can add an entirely new function like this
Mesh.prototype.customFunction = function() {};

Can you make a fiddle showing the change in behavior from before and after r121? This should still work.

1 Like

Can fiddles even be made with the module based version? I kept getting error when I tried before with three.js. Granted I probably just was doing something wrong.

So to be clear (I don’t think I was), the extended function on THREE.Mesh still works but it is broken now on THREE.SkinnedMesh.

This also only applies to the module version of r121. The module version r120 worked fine. The non-module r121 also works fine.

Example:

import * as THREE from '../webgl/modules/Three121.module.js';

THREE.Mesh.prototype.something = fn () { do_something(); };

var m = new THREE.Mesh();

var sm = new THREE.SkinnedMesh();

m.something(); // works

sm.something(); // says "something" is not a property of object now

Yup – the three.js starter fiddle uses modules, as well:

2 Likes

Ah ok great thank you. I can’t load the file from my server like normal, but that unpkg.com link works fine.

Hmm, I’m not sure what is goin on now. I made an example and it seems to be working. I’m not sure why my live app is not liking r121.

I’ll have to do some more research.

Thanks for helping me with the module based fiddle.

You can use the JavaScript Class extends Keyword.
Example below extends the OrbitContols.

class ExtendedOrbitControls extends OrbitControls {
    constructor(camera, domElement) {
        super(camera, domElement);
        this.propertyA = "seanwasere";
    }
}

and then use it,

const controls = new ExtendedOrbitControls(camera, renderer.domElement);
console.log(controls.propertyA);

Running example

JavaScript version
See Lines 5-10, 21-23 : https://codesandbox.io/s/extended-orbitcontrols-j0tkz?file=/dist/client/client.js

TypeScript Version
See Lines 6-12, 27-29: https://codesandbox.io/s/extended-orbitcontrols-j0tkz?file=/src/client/client.ts

2 Likes

@gkjohnson hm, that’s true, mutability of everything is great, same with clojures, dynamic typing etc., but only as long as (1) it’s predictable (2) you know what you are doing. If our man @titansoftime goes once again mutating (or extending) Three’s classes (which may or may not change over time, you can’t predict that) he is kinda asking for trouble in the future once again. And I can’t really seem to understand why in the first place.

Imo the smart (and easy) way would be to just build stuff around it:

class MySomethingThatAlsoHasAMesh {
  mesh = new Three.Mesh(); // or a SkinnedMesh

  something() {
    // Do stuff that may or may not use this.mesh
  }
}

@seanwasere inheritance (except maybe for extending React.Component :upside_down_face: ) does seem to create quite a few more problems than solutions - it’s coupling and chaining classes, which can make any modifications a bit hard later on.

Monkey patching is a valid approach and a reason i highly dislike ES6 classes for locking this feature.

Some features that could be patched without core modifications can’t be done anymore with that, transpilation is a requirement then.

This is a perfect example why “Composition over Inheritance” is a good practice.

As stated before, you should write wrappers around the THREE.js objects. Let THREE.js become “an internal” in your engine, not something that ‘blends in’, otherwise you’ll always have issues like these when updating to a newer version.

I’ve always went with the approach of not touching or modifying the original code, but rather write wrappers around the objects. Yes, you’ll create some boilerplate code (basically quite a few proxy methods), but at least it won’t break the internal API’s of your own engine. When updating THREE, you’ll only have to fix the points in your own code which directly use THREE’s own objects.

For example:

# Your Mesh class which extends THREE.Mesh
export class MyMesh extends Mesh
{
    // Your own code.
}

# Some random part in your own code:
const m = new MyMesh();
m.position.set(x, y, z); // <- This is actually a call to THREE.Mesh.position.set

If for whatever reason, THREE decides to change the method signature of set, you’ll have to update every part of your own code where set is being called.

Now, taking the “Composition over Inheritance” approach:

# MyMesh wraps THREE.Mesh
export class MyMesh
{
    private _mesh: Mesh = new Mesh();

    public setPosition(x: number, y: number, z: number): MyMesh
    {
        this._mesh.position.set(x, y, z);

        return this;
    }
}

Now, if THREE decides to change the method signature of the set method, you’ll only need to update the setPosition method in your own code.

This is just a hypothetical example of course, but hopefully it shows how easily maintainable your own code becomes when a new update of THREE is released which you want to use.

1 Like