Three.js: Adding prefix to Object3D id

Created on 4 Jun 2018  Â·  26Comments  Â·  Source: mrdoob/three.js

Using 2 different libraries (A and B) based on threejs.

Each library generates valid threejs meshes independently. However, the id of the meshes from library A and B can conflict (i.e. be the same). Then when we add meshes from library A and B into the same scene it does not render properly.

Is there a mechanism we can use to generate meshes independently and ensure IDs do not conflict while rendering it in the same scene?

One idea is to add a prefix concept:

Object.defineProperty( this, 'id', { value: object3DId ++ } );
=>
Object.defineProperty( this, 'id', { value: `${prefix}-${object3DId ++}` } );

Not quite sure which would be the best way to pass the prefix though.

Any thoughts on that?

Thanks

Suggestion

Most helpful comment

Sure, lets say you have a global namespace THREE, now if your App A sets a property value THREE.generator to X and your app B sets a property value for the same property to Y - only one of those will actually stick.

Now I think about it, I think you have bigger problems if there are actually multiple instances of THREE loaded in your app. If you manage to let both libraries use the same instance, then your problems will also be gone. I would suggest trying this instead of "hacking" around the ID generation.

All 26 comments

I believe that updating the object ID after calling the constructor is not enough (or is it?):

class HelpersContour extends Object3D {
  constructor(stack, geometry, texture) {
    //
    super();
    this.id = 'mynew' + this.id;
    // should I loop though all children and update IDs in the same way?

We would need to make id reconfigurable in such a case.

Object.defineProperty( this, 'id', { value: object3DId ++, writable: true } );

Set writable to true seems the easiest way to solve this problem. You can manage a global object ID on app level and overwrite the .id property immediately after object creation or a super() call.

Yes, definitely - I'd be happy to make a PR if it is fine with you.

Would updating the ID of the object extending Object3D be enough for threejs to render it nicely of should we also update IDs of all children (if any)?

class SuperArrow extends ArrowHelper {
  constructor(stack, geometry, texture) {
    //
    super();
    this.id = 'mynew' + this.id;
    // should I loop though all children and update IDs in the same way?
    // or not needed?

Thanks!

You have to update all objects. I think it is also necessary to overwrite the ids of geometries, materials and textures if both of your libraries generate these types of objects.

I'd be happy to make a PR if it is fine with you.

Let's see what other developers think if the suggestion.

Yes - so the suggestion is to make id writable in Object3D:

Object.defineProperty( this, 'id', { value: object3DId ++, writable: true } );

To be consistent, I would also perform this change for Geometry, BufferGeometry, Material and Texture.

I think it would be more beneficial to have a form of a generator for this.

For example:

// Your app.
let id = 0;

function ThreeIdGenerator() {
    return id++;
}

THREE.idGenerator = ThreeIdGenerator;

And have something similar in threejs itself to use by default. Since objects aren't (under normal circumstances) created in real-time, there should be very little to no overhead, while letting developers using threejs determine their own ID generation-style.

This way, developers don't have to "hack around" objects generated by THREE and having a mechanism like this also ensures consistency between the instantiation of different types of objects. Also, if there comes a time where THREE keeps track of object ID's it has created, functionality _will_ break if your app decides to change the numbers _after_ the object was created.

I see what you suggested but unfortunately, it relies on global variables and I'm not sure it would work all the time: for instance, how would you go to update the "generator" function without using the global variable window.THREE?

import {Mesh} from 'three';

// how do you update the ThreeIdGenerator ?
const myMesh = new Mesh();

I'm still in favor of having ids writable :)

import {Mesh} from 'three';

const myMesh = new Mesh();
myMesh.id = `customId-${myMesh.id}`

I like the idea of providing custom ID generators but I'm not sure that is doable

The way module resolving works in javascript is that every time you import a module, you effectively get back the same "instance" of a module.

So if THREE has a module called IdGenerator, it can use it internally, and you can overwrite it.

For example:

import {IdGenerator} from 'three';

let id = 0;
IdGenerator.generate = function(id) {
    return id++;
}

While inside THREE itself, it looks like this:

function Mesh ( ) {
    this.id = THREE.IdGenerator.generate();
}

a few things.

  1. Having library-wide generator be global, and editable is not a good idea. If you have 2 libraries: A and B that rely on THREE.js and both rewrite that generator - you're not in a good situation.
  2. suffixes are a bad idea. Currently, ID follows a standard - UUID spec of IETF, adding anything to that string will basically make it garbage instead of a standardized ID.

I think your pursuit is misguided in the first place. If 2 libraries make use of three.js - they should make use of the same instance of three.js, this avoids version conflicts, separate caches, gl contexts etc. I also don't consider it to be a good design to include the same library twice, especially if you plan to mix the outputs of those two copies.

@haroldiedema I like your suggestion if we can make it work (it seems we need different id generators for meshes, materials, etc.)

@Usnul

Having library-wide generator be global, and editable is not a good idea. If you have 2 libraries: A and B that rely on THREE.js and both rewrite that generator - you're not in a good situation.

Can you detail? What if you build extensions for threeJS, say a set of new material for "cinema" and a set of helpers for "video games". That would be great if people can just import it into their existing app and just use it without worrying about conflict of ids.

suffixes are a bad idea. Currently, ID follows a standard - UUID spec of IETF, adding anything to that string will basically make it garbage instead of a standardized ID.

There are 2 things there, uuid and id. I am proposing to extend the id.

I understand what you say about caching, gl context, etc.. I agree, we should only use 1 version of threeJS for all the rendering part but would be nice to be able to pick meshes/materials/geometries from different places seamlessly.

Developers have to make sure versions are compatible of course.

Can you detail? What if you build extensions for threeJS, say a set of new material for "cinema" and a set of helpers for "video games". That would be great if people can just import it into their existing app and just use it without worrying about conflict of ids.

Sure, lets say you have a global namespace THREE, now if your App A sets a property value THREE.generator to X and your app B sets a property value for the same property to Y - only one of those will actually stick. This is more about the case where you do, in fact, have only 1 instance of the library.

There are 2 things there, uuid and id. I am proposing to extend the id.

Okay, sorry, I made a pre-emptive conclusion. There are some other considerations though, if ID is an integer - it maps very well to an array index, if it's a string, however, your arrays becomes a glorified map with a lot of properties which are not actually indices - this leads to degraded performance for one, and makes any attempts at index-based iteration over that array fruitless, including built-in methods such as .forEach, .map, .filter etc.

Sure, lets say you have a global namespace THREE, now if your App A sets a property value THREE.generator to X and your app B sets a property value for the same property to Y - only one of those will actually stick.

Now I think about it, I think you have bigger problems if there are actually multiple instances of THREE loaded in your app. If you manage to let both libraries use the same instance, then your problems will also be gone. I would suggest trying this instead of "hacking" around the ID generation.

@haroldiedema
Yeah, that would be my suggestion too.

Make those libraries accept three.js instance as an input in the initialization code (e.g. constructor) or just the relevant parts.

@Usnul @haroldiedema yes your suggestion is good, however how would you implement it? I've been toying around but can not find the right way.

import {Mesh} from 'three'

// do not extend Mesh
export class MyMesh {
  constructor (color, mesh=Mesh) {
     // use "mesh" to initiate the class
     // can not use super because my class doesn't extend anything
     // must use es5 syntax to mimic the "extend / super" behavior

     // option 1
     this = new mesh(…);.

    // option 2
    // use call/create on "mesh" to initialize set prototype?
   }
}

I'm writing this with protest, since - as I said before - you'll have to fix the libraries so they all accept the same instance of THREE.

But if you _really_ want to do this, here's how I'd go about it (untested, but you'll get the idea) in just 4 easy steps:

Step 1: Write a generator inside THREE itself that it can use by default. This could look something like this:

class ObjectIdGenerator
{
    constructor()
    {
        this.index = 0;
    }

    /**
     * @returns {number}
     */
    generateId()
    {
        return (++this.index);
    }
}

THREE.objectIdGenerator = new ObjectIdGenerator();

Step 2: Inside THREE itself you find the part that generates the ID of an Object3D instance.

function Object3D() {
    Object.defineProperty( this, 'id', { value: object3DId ++ } );
    // ...

And change it to:

function Object3D() {
    Object.defineProperty( this, 'id', { value: THREE.objectIdGenerator.generateId() } );
    // ...

Step 3: Write your own generator that has a generateId() method.

class MyCustomIdGenerator()
{
    constructor(prefix)
    {
        // Assuming prefix is a number, but could be anything.
        this.id = prefix;
    }

    generateId()
    {
        return (++this.id);
    }
}

Step 4: Apply your custom generator to all THREE instances.

const generator = new MyCustomIdGenerator(42);
THREE.objectIdGenerator = generator;
AnotherTHREE.objectIdGenerator = generator;

Now all created Object3D instances will get their ID's from your custom generator.
For example:

let obj1 = new THREE.Object3D();
let obj2 = new AnotherTHREE.Object3D();

console.log(obj1.id) // 43;
console.log(obj2.id) // 44;

@haroldiedema sorry maybe I was not clear, I do want to fix the external libraries to accept the same instance of THREEJS as you suggested. However that is a bit challenging:

That is how I envision it, in the JS application:

import {* as three} from 'three';
import {customMesh} from 'anotherLibrary';

const validThreeJSMesh = new customMesh(three.Mesh);
scene.add(validThreeJSMesh);

Main issue is what is customMesh gonna look like:

class CustomMesh() {
  constructor(threeRef) {
    // how do I use threeRef
    // to make this class become a valid THREEJS Mesh?
  }
}

@haroldiedema sorry maybe I was not clear, I do want to fix the external libraries to accept the same instance of THREEJS as you suggested.

Which are these libraries?

@mrdoob 'ami' is my current use case. It is a JS library for medical imaging. It can be seen as an extension for three.

For instance, it provides 'meshes' or 'materials' than people can use in their own applications.

It used to rely on the global namespace to create meshes and materials but I want to get rid of that .

Currently you can use it as:

import {SpecialMesh} from 'ami';

const mesh = new SpecialMesh();
scene.add(mesh);

const regularMesh = new THREE.Mesh();
scene.add(regularMesh);
...

It works well because under the hood, SpecialMesh is defined as:

class SpecialMesh extends THREE.Mesh {
}

I want to get rid of global namespace, and the problem I have is that ids of meshes from 'ami' and 'three' can conflict.

I think the following API makes sense but I do not know which would be the best implementation details or if there is any recommendation for such use cases.

// ideally in the app
import {SpecialMesh} from 'ami';

const mesh = new SpecialMesh(THREE);
scene.add(mesh);

const regularMesh = new THREE.Mesh();
scene.add(regularMesh);

// ideally in ami
export SpecialMesh {
  constructor(three) {
    // do some magics
  }
}

My first idea was to generate custom or random ids somehow so it would never conflict regardless we use or not global namespace.

Concrete current implementation example:
https://github.com/FNNDSC/ami/blob/dev/src/geometries/geometries.voxel.js#L8-L20

this makes no sense...

I want to get rid of global namespace

why can't ami author add three to package.json and then

import * as THREE from 'three';

in SpecialMesh.js or whatever?

in your example, geometries.voxel.js imports Matrix4, but uses BoxGeometry via global THREE. this is an issue with ami, not three.js

in your example, geometries.voxel.js imports Matrix4, but uses BoxGeometry via global THREE. this is an issue with ami, not three.js

@makc Yes, that is the whole problem I want to address. It can not use the localthree fromnpm, because after compilation, ami has a reference to its "own" three. Therefore geometries created by ami has their own "id counter" and geometries created outside of my library use a different 'id counter'.

Basically, I'm looking for a mechanism to extend threejs, custom ideas was one option, pass a reference to three in the object constructor is another (not sure how implement this one).

I can't believe I'm saying this, but I think the only thing you can do is:

import * as THREE from 'three';
window.THREE = THREE;

... before loading ami.

Or just file a bug report there.

FYI - I fixed ami to work nicely with different version of three using a factory pattern. We can close the issue or keep it open if you think such a feature (prefix id) may be useful at some point.

//
import * as THREE from 'three';
import {customHelperFactory} from 'ami';

const CustomHelper = customHelperFactory(THREE);
const helper = new CustomHelper();
scene.add(helper);

Closing for now.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

konijn picture konijn  Â·  3Comments

jlaquinte picture jlaquinte  Â·  3Comments

zsitro picture zsitro  Â·  3Comments

clawconduce picture clawconduce  Â·  3Comments

akshaysrin picture akshaysrin  Â·  3Comments