Three.js r125 generates a warning in Observable

three.js r125 got a small change: a warning is added to the console when various instances of THREE are present on the same page.

I understand the idea because using three.js objects from different versions might be dangerous, but it’s even triggered when the versions are the same (which wastes resources but is not dangerous, afaik)

I wonder how it relates to Observable. Obviously, it’s common to import cells from various notebooks (that’s the idea), so if at least both of them make use of THREE, we get the warning. We might avoid this by creating an instance of THREE in the main notebook, then passing the instance to all the imported notebooks using with {THREE}, but it’s a mess!

I would be interested in your thoughts about this new feature and if you think that we should just accept to see this warning.

Note that tried to write a bit about a best practice here: Import three.js / Sylvain Lesage / Observable but I’m not even sure it is a good idea :confused:. Any comment is welcome

Edit: I wrote 4 possible ways to manage this. The first one is the normal way, which triggers the warning, but has most pros, I think.

Apologies if I missed it, but it looks like none of your examples list the case where you simply import THREE from the respective notebook itself:

import {THREE} from '@severo/three-js-perspective-camera-utils'

Whenever I work with Zdog, I use helpers from Zdog Helpers, so I just go ahead and import Zdog as well.

Sure, but the problem is when “you import three.js objects from two notebooks” (or more). If there is only one dependency, it’s OK.
Another potential small problem is that you don’t have control on the version you will import.

If it’s safe to ignore the warnings, then you could probably just mess with window.__THREE__ after you imported the library.

Otherwise my first choice would probably be 3), passing in THREE. I’m not sure why you’d need to get the version number in this scenario:

  • If you pass in THREE, you’re already in control.
  • If you want to see the required version, you can inspect the notebook and optionally pin its version.

As for modules from examples, you’ll certainly have to restore any interface additions to the THREE object which are expected by the notebook (i.e., make sure all object types are available).

I’ve used the following format to deal with example modules:

function importExample(path, module = path.split('/').pop(), version = VERSION) {
  return import(`${version}/examples/jsm/${path}.js`)
    .then(m => module ? m[module] : m);
OrbitControls = importExample('controls/OrbitControls')
Line2 = importExample('lines/Line2')
LineMaterial = importExample('lines/LineMaterial')
LineGeometry = importExample('lines/LineGeometry')

I’m not sure why you’d need to get the version number in this scenario

Because I was thinking to pass only THREE, not the modules from examples. Therefore I had to pass threeVersion, or get the version from the THREE object. I understand we don’t have the version as such, but the REVISION. I could get the version with “0.”+REVISION, but this schema might break in future versions of Three.

But you mention a good idea which is to attach the modules like OrbitControls to the THREE object. I was doing this, but changed recently, maybe for a bad reason. As these modules are imported dynamically as ESM, I thought it would be coherent to do the same for THREE. But:

  const THREE = await import(`${threeVersion}/build/three.module.js`);
  const OrbitControls = (await import(`${threeVersion}/examples/jsm/controls/OrbitControls.js`)).OrbitControls;
  THREE.OrbitControls = OrbitControls;
  return THREE;

generates an error: “THREE = TypeError: “OrbitControls” is read-only”. That’s why I switched to manage them as different objects in different cells, instead of adding them as properties of the THREE object.

If I rollback to importing THREE with require, it’s OK:

  const THREE = await require(`three@${threeVersion}`);
  THREE.OrbitControls = (await import(`${threeVersion}/examples/jsm/controls/OrbitControls.js`)).OrbitControls;
  return THREE;

Thanks for your function importExample, it’s very nice, I will add it to the notebook.

So, finally, I think you’re right:

  • in the application notebook, create the THREE instance with a chosen version and attaching it all the modules from examples that will be needed in the dependencies.
  • pass THREE to all the dependencies (and so on transitively)

Module objects are not extensible, which means that you cannot modify their shape. However, you might be able to “cheat” by destructuring into a new object:

return {...THREE, OrbitControls};

Note that I haven’t tested wether this method has any downsides.

1 Like

OK, thanks for all @mootari. I reformulated the notebook: Import three.js / Sylvain Lesage / Observable