This page was generated from appendix-webgui/webgui-internal.ipynb.

Webgui - programming and internal features

The webgui is used in nearly all tutorials, see there for some basic usage. This section provides information about advanced features and customizations.

Controls

  • left click: rotate

  • right click: move

  • mouse wheel: zoom

  • ctrl + right click: Move the clipping plane

  • double click: Move center of rotation

Check webgui_jupyter_widgets version

You need to have webgui_jupyter_widgets >= 0.2.18 installed, the cell below verifies your version.

[1]:
try:
    import webgui_jupyter_widgets
    from packaging.version import parse
    assert parse(webgui_jupyter_widgets.__version__) >= parse("0.2.18")
    print('Everything good!')
except:
    print("\x1b[31mYou need to update webgui_jupyter_widgets by running: \x1b[0m\npython3 -m pip install --upgrade webgui_jupyter_widgets")
Everything good!

The Draw function

[2]:
from netgen.csg import unit_cube
from ngsolve import *
from ngsolve.webgui import Draw
m = Mesh(unit_cube.GenerateMesh(maxh=0.2))

help(Draw)
Help on function _DrawDocu in module netgen.webgui:

_DrawDocu(obj, *args, **kwargs)

objects

The objects argument allows to pass additional visualization data, like points, lines and text labels.

[3]:
lines = { "type": "lines", "position": [0,0,0, 1,1,1, 1,0,0, 0,0,1], "name": "my lines", "color": "red"}
points = { "type": "points", "position": [0.5, 0.5, 0.5, 1.1,1,1], "size":20, "color": "blue", "name": "my points"}
text = { "type": "text", "name": "my text", "text": "hello!", "position": [1.3,0,0]}
Draw(m, objects=[lines,points, text], settings={"Objects": {"Edges": False, "Surface": False}})

[3]:
BaseWebGuiScene

eval_function

eval_function is passed directly to the WebGL fragment shader code and can be used to alter the output function values. Any GLSL expression is allowed, which is convertible to a vec3. It can depend on the position, value and normal vector. Note that GLSL is very strict about typing/automatic conversion, so always use float literals for expresions (10 * p.x won’t compile, 10. * p.x will).

That’s the part of the GLSL shader code, where eval_function is injected as USER_FUNCTION (see https://github.com/CERBSim/webgui/blob/main/src/shader/function.frag#LL10C1-L15C24 )

#ifdef USER_FUNCTION
vec3 userFunction( vec3 value, vec3 p, vec3 normal )
{
  return vec3(USER_FUNCTION);
}
#endif // USER_FUNCTION

Errors in the expression are shown in the JS console (see below).

[4]:
eval_function = "p.x*p.x+p.y*p.y+p.z*p.z < 0.5 ? 1. : sin(100.0*p.x)"
Draw(CF(1), m, "func", eval_function=eval_function, min=-1, max=1, settings={"camera": {"transformations": [{"type": "rotateY", "angle": 45}]}})

[4]:
BaseWebGuiScene

Behind the scenes

To see what’s going on on the client side (the browser), you can have a look at the JS console (F12 for Chrome and Firefox). When you open the Webgui - initRenderData tab, you can have a look at the JS data structures (most importantly the scene object). The render_data member contains the data that was generated by the python kernel and sent to the browser. Some interesting fields are

  • render_data: data that was sent from the python kernel

  • controls: handling mouse events and camera handling

  • render_objects: objects that can be rendered (and turned on/off in the "Objects" tab of the gui)

  • gui: the gui object managing the menu on the top right corner

js_console.png

Pass javascript code to frontend

ngsolve.webgui.Draw allows to inject javascript code using the js_code argument. The passed code is executed after the scene is initialized. Below is an example, which rotates the camera for 3 seconds. The console.log output is visible in the JS console.

[5]:
js_code = """
  // Example: Rotate the view around the y axis for the first 3 seconds
  // print message in javascript console of the browser (open with F12 for Chrome and Firefox)
  console.log("hello from Javascript!", "Scene", scene, "render_data", render_data)

  // hide geometry edges (see the 'Objects' menu in the GUI for entry names)
  scene.gui.settings.Objects['Edges'] = false

  // Track time since first draw
  let t = 0;
  const speed = 90*Math.PI/180;

  // Register a callback function to the scene, which is called after a frame is rendered
  scene.on("afterrender", (scene, dt) => {
    t += dt;
    if(t<3) {
      console.log(`time since last frame: ${dt} seconds`, "total time: ", t, "seconds")

      // rotate around y axis
      scene.controls.rotateObject(new modules.THREE.Vector3(0,1,0), dt*speed)

      // recalculate transformation matrices, also triggers rerendering of scene
      scene.controls.update();
    }
  })
"""
from IPython.display import display, Markdown
display(Markdown(f"```javascript\n{js_code}```"))
// Example: Rotate the view around the y axis for the first 3 seconds
// print message in javascript console of the browser (open with F12 for Chrome and Firefox)
console.log("hello from Javascript!", "Scene", scene, "render_data", render_data)

// hide geometry edges (see the 'Objects' menu in the GUI for entry names)
scene.gui.settings.Objects['Edges'] = false

// Track time since first draw
let t = 0;
const speed = 90*Math.PI/180;

// Register a callback function to the scene, which is called after a frame is rendered
scene.on("afterrender", (scene, dt) => {
  t += dt;
  if(t<3) {
    console.log(`time since last frame: ${dt} seconds`, "total time: ", t, "seconds")

    // rotate around y axis
    scene.controls.rotateObject(new modules.THREE.Vector3(0,1,0), dt*speed)

    // recalculate transformation matrices, also triggers rerendering of scene
    scene.controls.update();
  }
})
[6]:
Draw(m, js_code=js_code);

You can also add stuff to the gui. The following example adds a checkbox and moves the clipping plane, when it is set. scene.gui is a dat.GUI object, see here for more information.

[7]:
js_code = """
  scene.gui.settings.panclipping = false;
  scene.gui.add(scene.gui.settings, "panclipping").onChange(()=>scene.animate())
    const clipping = scene.gui.settings.Clipping;
    clipping.x = -1;
    clipping.z = -1;

    clipping.enable = true;

   scene.on("afterrender", (scene, dt) => {
    if(scene.gui.settings.panclipping) {
    clipping.dist += 0.5*dt;
      if(clipping.dist >= 1)
       clipping.dist = -1;
      scene.controls.update();
    }
  })
"""
Draw(m, js_code=js_code);
[8]:
# Create a THREE.js object and add it to the scene
# Note that some features (clipping plane, double click) are note working correctly for "foreign" render objects
js_code = """
    const geometry = new modules.THREE.BoxGeometry( 1, 1, 1 );
    const material = new modules.THREE.MeshBasicMaterial( { color: 0x0000ff } );
    const cube = new modules.THREE.Mesh( geometry, material );
    const render_object = new modules.render_object.RenderObject()
    cube.matrixWorldAutoUpdate = false;
    render_object.name = "My Render Object"
    render_object.three_object = cube
    scene.addRenderObject(render_object)
"""
Draw(m, js_code=js_code);

Todo - Example: Show center of rotation while rotating - Advanced: How to modify webgui (recompile/install)

Communication Python -> Javascript

  • Use scene.widget.send on the Python side to send messages (scene is the return value of Draw())

  • Use scene.widget.model.on('msg:custom', callback) on the JS side

[9]:
# change the colormap max setting from python in a loop
js_code = """
scene.widget.model.on('msg:custom', (message)=> {
    console.log("received message", message)
    scene.gui.settings.Colormap.max = message.colormap_max
    scene.animate()
})
"""
s = Draw(x, m, "x", js_code=js_code);

import time
for i in range(10):
    time.sleep(1)
    s.widget.send({"colormap_max": .9-.1*i})

Communication Javascript -> Python

  • Use scene.widget.send(message) in JS

  • Use scene.widget.on_msg(callback) in Python

[10]:
# print adjacent faces of selected edge in python

js_code = """
scene.on("select", ({dim, index}) => {
    console.log("selected", dim, index);
    scene.widget.send({type: 'select', dim, index})
})
"""
s = Draw(m, js_code=js_code);
def onMessage(widget, content, buffers):
    dim = content['dim']
    index = content['index']
    if dim == 1:
        # find adjacent faces to selected edge
        r = m.Region(BBND)
        r.Mask().Clear()
        r.Mask().Set(index)
        nb = r.Neighbours(BND)
        boundaries = m.GetBoundaries()
        faces = [ (i, boundaries[i]) for i, val in enumerate(nb.Mask()) if val ]
        print("faces", faces)
s.widget.on_msg(onMessage)

Combine both directions

When selecting an edge -> find adjacent faces in python -> send result back and append tooltip text

[11]:
js_code = """
scene.on("select", ({dim, index}) => {
    console.log("selected", dim, index);
    scene.widget.send({type: 'select', dim, index})
})

scene.widget.model.on('msg:custom', (faces)=> {
    console.log("received faces", faces)
    scene.tooltip.textContent += ", Faces: ";

    for ( let i =0; i < faces.length; i++)
        scene.tooltip.textContent += faces[i][0] + " " + faces[i][1] + ", "

    // extend tooltip width
    scene.tooltip.style.width = "300px"
})
"""
s = Draw(m, js_code=js_code);
def onMessage(widget, content, buffers):
    dim = content['dim']
    index = content['index']
    if dim == 1:
        # find adjacent faces to selected edge
        r = m.Region(BBND)
        r.Mask().Clear()
        r.Mask().Set(index)
        nb = r.Neighbours(BND)
        faces = []
        boundaries = m.GetBoundaries()
        faces = [ (i, boundaries[i]) for i, val in enumerate(nb.Mask()) if val ]
        s.widget.send(faces)
s.widget.on_msg(onMessage)

JS classes

The examples here don’t cover all the options (for istance GUI settings) available. To get more insight, have a look at the source code. - GUI settings: https://github.com/CERBSim/webgui/blob/main/src/gui.ts#L60 - Scene: https://github.com/CERBSim/webgui/blob/main/src/scene.ts#L71 - Camera controls: https://github.com/CERBSim/webgui/blob/main/src/camera.ts#L16

[ ]: