Anvil.js.import_from - three.js - OrbitControls - FontLoader

I am working on a 3D viewer and I am able to struggle my way through a working app.

I am getting the working app to do what I want, so I’m fairly happy. But there are things that I don’t understand and I would be even happier if someone with a good understanding of the inner workings of Anvil or JavaScript or Threejs or whatever I don’t understand helped me understand what happens.

Here are a few questions about the snippet of code below:

  1. Why do I need that ['default'] when I get OrbitControls and FontLoader?
  2. Why the font_loaded and the font_load_error are not called?
    Since they are not called, I decided to just wait for the fonts to be loaded, and…
  3. Why removing the time.sleep(0.001) the font is not loaded (well, it’s loaded later)?
    I thought the print would do whatever the sleep does and some more, but I was surprised to see that a print doesn’t help, while a sleep does the job.
def canvas_3d_show(self, **event_args):
    def font_loaded(font):
        print(f'font_loaded {font.family}')
    def font_load_error(error):
        if error:
            print(f'error.message {error.message}')
            print(f'error.notLoadedFonts {error.notLoadedFonts}')
        else:
            print('all fonts loaded')
    THREE = anvil.js.import_from("https://cdn.skypack.dev/three@0.138.0")
    OrbitControls = anvil.js.import_from("https://cdn.skypack.dev/three-orbit-controls")['default'](THREE)
    FontLoader = anvil.js.import_from("https://cdn.skypack.dev/FontLoader")['default']
    font_loader = FontLoader(['Trebuchet MS:n4', 'Arial:n4', 'Serif:n4'], font_loaded, font_load_error)
    font_loader.loadFonts()
    t = time.time() + 5
    counter = 0
    while not font_loader._finished and time.time() < t:
        counter += 1
        time.sleep(0.001)  # removing this line it doesn't work
        if counter < 10:   # prevent it from printing thousands of times
            print('hi')
    print(font_loader._finished, counter)
  1. Threejs either uses the canvas provided as argument if one is provided, or adds one to the specified container element. Why providing a canvas doesn’t work?
    # this works
    canvas = anvil.js.get_dom_node(self.column_panel_1)
    renderer = THREE.WebGLRenderer()
    renderer.shadowMap.enabled = True
    renderer.shadowMap.type = THREE.PCFSoftShadowMap
    renderer.setSize(canvas.clientWidth, 300)
    canvas.appendChild(renderer.domElement)
    canvas.children[0].style = 'display: block;'  # see question # 5 below

    # this doesn't
    canvas = anvil.js.get_dom_node(self.canvas_1)
    renderer = THREE.WebGLRenderer({'canvas': canvas)
  1. Is the workaround on the line with the comment about question # 5 a reliable one?
    The renderer worker is supposed to detect when the canvas is resized and resize the renderer. The following code doesn’t work because the canvas (the one added when creating the renderer) has the width specified both as attribute and in the style attribute, something like this:
<canvas data-engine="three.js r138" width="1221" height="800" style="display: block; width=800px; height=300px;"></canvas>
# this renderer worker only works if width is removed from the style (see above)
def render(renderer, *a):
    size = THREE.Vector3()
    renderer.getSize(size)
    old_width = size.x

    canvas = anvil.js.get_dom_node(self.column_panel_1)
    new_width = canvas.clientWidth

    if old_width != new_width:
        renderer.setSize(new_width, canvas.clientHeight, False)
        aspect = new_width / canvas.clientHeight
        camera.aspect = aspect
        camera.updateProjectionMatrix()

    renderer.render(scene, camera)
    anvil.js.window.requestAnimationFrame(partial(render, renderer))
  1. Why do I need that ['default'] when I get OrbitControls and FontLoader?

This is a very similar question to Why do I need datetime?

import datetime
datetime.datetime.now()

datetime is the module and datetime.datetime is the class.

Similarly:

OrbitControlsMod = anvil.js.import_from("https://cdn.skypack.dev/three-orbit-controls")
OrbitContorls = OrbitControlsMod.default

You’re importing a module and then you’re getting attributes from the module.

JavaScript has different semantics for modules compared to Python.
Every attribute must be exported explicitly and there is also a semantic for providing a default export.

I guess you can think of it as being a semantic similar to Python using the __all__ attribute when doing from x import *


  1. Why the font_loaded and the font_load_error are not called?

I think you are using the API wrong - it should be:


        font_loader = FontLoader(['Trebuchet MS:n4', 'Arial:n4'], {
            "fontLoaded": font_loaded, 
            "complete": font_complete
        })

  1. Why removing the time.sleep(0.001) the font is not loaded (well, it’s loaded later)?

Javascript is single threaded.
When you use sleep in skulpt it doesn’t block the thread.
Whereas just doing a while True with a print statement blocks the thread and then there’s no opportunity for JavasScript to do other work.

Some details in this talk from @meredydd


  1. Threejs either uses the canvas provided as argument if one is provided

I’m pretty sure it’s because the anvil canvas element is setup as a 2D context, whereas Three wants it to have a different context.

  1. Is the workaround on the line with the comment about question # 5 a reliable one?

Is that code from somewhere in particular?
I’d probably want to play around with a clone.

Thank you for the detailed answers!

Now I know that time.sleep(0.001) is the equivalent of DoEvents in VBA / VB6.

I am going to learn more and get the app working, then (in 2-3 weeks) I will post a clone link of a simplified version with working orbit controls, window resize, text rendering and other things required for a production app.

Then you will clone it, play with it and give me the list of things I got wrong :slight_smile:

3 Likes

Hi, did you get to a demo of this we can see, I have a thing I’m thinking of using 3d for! looking for examples to learn from!

I did a short tutorial on 3d force graph a while back, which should cover some of the principles you are after: 3D Network Graph Tutorial

We also published a worked example using Three.js in Anvil a couple of years back, as part of our advent calendar:

Found that wth a better search thanks!

Is there a way to get a move event from the tree panel in this so I can drag the camera orientation around? Should I use a different host control?