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
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 ofDraw()
)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 JSUse
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
[ ]: