{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Webgui - programming and internal features"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1",
   "metadata": {},
   "source": [
    "The webgui is used in nearly all tutorials, see there for some basic usage. This section provides information about advanced features and customizations.\n",
    "\n",
    "### Controls\n",
    "- left click: rotate\n",
    "- right click: move\n",
    "- mouse wheel: zoom\n",
    "- ctrl + right click: Move the clipping plane\n",
    "- double click: Move center of rotation\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "### Check webgui_jupyter_widgets version\n",
    "You need to have webgui_jupyter_widgets >= 0.2.18 installed, the cell below verifies your version."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    import webgui_jupyter_widgets\n",
    "    from packaging.version import parse\n",
    "    assert parse(webgui_jupyter_widgets.__version__) >= parse(\"0.2.18\")\n",
    "    print('Everything good!')\n",
    "except:\n",
    "    print(\"\\x1b[31mYou need to update webgui_jupyter_widgets by running: \\x1b[0m\\npython3 -m pip install --upgrade webgui_jupyter_widgets\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## The Draw function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from netgen.csg import unit_cube\n",
    "from ngsolve import *\n",
    "from ngsolve.webgui import Draw\n",
    "m = Mesh(unit_cube.GenerateMesh(maxh=0.2))\n",
    "\n",
    "help(Draw)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "### objects\n",
    "\n",
    "The `objects` argument allows to pass additional visualization data, like points, lines and text labels."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {},
   "outputs": [],
   "source": [
    "lines = { \"type\": \"lines\", \"position\": [0,0,0, 1,1,1, 1,0,0, 0,0,1], \"name\": \"my lines\", \"color\": \"red\"}\n",
    "points = { \"type\": \"points\", \"position\": [0.5, 0.5, 0.5, 1.1,1,1], \"size\":20, \"color\": \"blue\", \"name\": \"my points\"}\n",
    "text = { \"type\": \"text\", \"name\": \"my text\", \"text\": \"hello!\", \"position\": [1.3,0,0]}\n",
    "Draw(m, objects=[lines,points, text], settings={\"Objects\": {\"Edges\": False, \"Surface\": False}})\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "### eval_function\n",
    "\n",
    "`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.\n",
    "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).\n",
    "\n",
    "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 )\n",
    "```c\n",
    "#ifdef USER_FUNCTION\n",
    "vec3 userFunction( vec3 value, vec3 p, vec3 normal )\n",
    "{\n",
    "  return vec3(USER_FUNCTION);\n",
    "}\n",
    "#endif // USER_FUNCTION\n",
    "```\n",
    "\n",
    "Errors in the expression are shown in the JS console (see below)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {},
   "outputs": [],
   "source": [
    "eval_function = \"p.x*p.x+p.y*p.y+p.z*p.z < 0.5 ? 1. : sin(100.0*p.x)\"\n",
    "Draw(CF(1), m, \"func\", eval_function=eval_function, min=-1, max=1, settings={\"camera\": {\"transformations\": [{\"type\": \"rotateY\", \"angle\": 45}]}})\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "## Behind the scenes\n",
    "\n",
    "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).\n",
    "The `render_data` member contains the data that was generated by the python kernel and sent to the browser.\n",
    "Some interesting fields are\n",
    "\n",
    "- render_data: data that was sent from the python kernel\n",
    "- controls: handling mouse events and camera handling\n",
    "- render_objects: objects that can be rendered (and turned on/off in the \"Objects\" tab of the gui)\n",
    "- gui: the gui object managing the menu on the top right corner\n",
    "\n",
    "![js_console.png](js_console.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11",
   "metadata": {},
   "source": [
    "### Pass javascript code to frontend"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "`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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {},
   "outputs": [],
   "source": [
    "js_code = \"\"\"\n",
    "  // Example: Rotate the view around the y axis for the first 3 seconds\n",
    "  // print message in javascript console of the browser (open with F12 for Chrome and Firefox)\n",
    "  console.log(\"hello from Javascript!\", \"Scene\", scene, \"render_data\", render_data)\n",
    "  \n",
    "  // hide geometry edges (see the 'Objects' menu in the GUI for entry names)\n",
    "  scene.gui.settings.Objects['Edges'] = false\n",
    "\n",
    "  // Track time since first draw\n",
    "  let t = 0;\n",
    "  const speed = 90*Math.PI/180;\n",
    "\n",
    "  // Register a callback function to the scene, which is called after a frame is rendered\n",
    "  scene.on(\"afterrender\", (scene, dt) => {\n",
    "    t += dt;\n",
    "    if(t<3) {\n",
    "      console.log(`time since last frame: ${dt} seconds`, \"total time: \", t, \"seconds\")\n",
    "\n",
    "      // rotate around y axis\n",
    "      scene.controls.rotateObject(new modules.THREE.Vector3(0,1,0), dt*speed)\n",
    "\n",
    "      // recalculate transformation matrices, also triggers rerendering of scene\n",
    "      scene.controls.update();\n",
    "    }\n",
    "  })\n",
    "\"\"\"\n",
    "from IPython.display import display, Markdown\n",
    "display(Markdown(f\"```javascript\\n{js_code}```\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14",
   "metadata": {},
   "outputs": [],
   "source": [
    "Draw(m, js_code=js_code);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15",
   "metadata": {},
   "source": [
    "You can also add stuff to the gui. The following example adds a checkbox and moves the clipping plane, when it is set.\n",
    "`scene.gui` is a dat.GUI object, see [here](https://github.com/dataarts/dat.gui/blob/master/API.md) for more information."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16",
   "metadata": {},
   "outputs": [],
   "source": [
    "js_code = \"\"\"  \n",
    "  scene.gui.settings.panclipping = false;\n",
    "  scene.gui.add(scene.gui.settings, \"panclipping\").onChange(()=>scene.animate())\n",
    "    const clipping = scene.gui.settings.Clipping;\n",
    "    clipping.x = -1;\n",
    "    clipping.z = -1;\n",
    "    \n",
    "    clipping.enable = true;\n",
    "       \n",
    "   scene.on(\"afterrender\", (scene, dt) => {\n",
    "    if(scene.gui.settings.panclipping) {\n",
    "    clipping.dist += 0.5*dt;\n",
    "      if(clipping.dist >= 1)\n",
    "       clipping.dist = -1;\n",
    "      scene.controls.update();\n",
    "    }\n",
    "  })\n",
    "\"\"\"\n",
    "Draw(m, js_code=js_code);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create a THREE.js object and add it to the scene\n",
    "# Note that some features (clipping plane, double click) are note working correctly for \"foreign\" render objects\n",
    "js_code = \"\"\"  \n",
    "    const geometry = new modules.THREE.BoxGeometry( 1, 1, 1 );\n",
    "    const material = new modules.THREE.MeshBasicMaterial( { color: 0x0000ff } );\n",
    "    const cube = new modules.THREE.Mesh( geometry, material );\n",
    "    const render_object = new modules.render_object.RenderObject()\n",
    "    cube.matrixWorldAutoUpdate = false;\n",
    "    render_object.name = \"My Render Object\"\n",
    "    render_object.three_object = cube\n",
    "    scene.addRenderObject(render_object)\n",
    "\"\"\"\n",
    "Draw(m, js_code=js_code);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18",
   "metadata": {},
   "source": [
    "Todo\n",
    "- Example: Show center of rotation while rotating\n",
    "- Advanced: How to modify webgui (recompile/install)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19",
   "metadata": {},
   "source": [
    "### Communication Python -> Javascript\n",
    "\n",
    "- Use `scene.widget.send` on the Python side to send messages (scene is the return value of `Draw()`)\n",
    "- Use `scene.widget.model.on('msg:custom', callback)` on the JS side"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20",
   "metadata": {},
   "outputs": [],
   "source": [
    "# change the colormap max setting from python in a loop\n",
    "js_code = \"\"\"  \n",
    "scene.widget.model.on('msg:custom', (message)=> {\n",
    "    console.log(\"received message\", message)\n",
    "    scene.gui.settings.Colormap.max = message.colormap_max\n",
    "    scene.animate()\n",
    "})\n",
    "\"\"\"\n",
    "s = Draw(x, m, \"x\", js_code=js_code);\n",
    "\n",
    "import time\n",
    "for i in range(10):\n",
    "    time.sleep(1)\n",
    "    s.widget.send({\"colormap_max\": .9-.1*i})\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21",
   "metadata": {},
   "source": [
    "### Communication Javascript -> Python\n",
    "\n",
    "- Use `scene.widget.send(message)` in JS\n",
    "- Use `scene.widget.on_msg(callback)` in Python"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22",
   "metadata": {},
   "outputs": [],
   "source": [
    "# print adjacent faces of selected edge in python\n",
    "\n",
    "js_code = \"\"\"  \n",
    "scene.on(\"select\", ({dim, index}) => {\n",
    "    console.log(\"selected\", dim, index);\n",
    "    scene.widget.send({type: 'select', dim, index})\n",
    "})\n",
    "\"\"\"\n",
    "s = Draw(m, js_code=js_code);\n",
    "def onMessage(widget, content, buffers):\n",
    "    dim = content['dim']\n",
    "    index = content['index']\n",
    "    if dim == 1:\n",
    "        # find adjacent faces to selected edge\n",
    "        r = m.Region(BBND)\n",
    "        r.Mask().Clear()\n",
    "        r.Mask().Set(index)\n",
    "        nb = r.Neighbours(BND)\n",
    "        boundaries = m.GetBoundaries()\n",
    "        faces = [ (i, boundaries[i]) for i, val in enumerate(nb.Mask()) if val ]\n",
    "        print(\"faces\", faces)\n",
    "s.widget.on_msg(onMessage)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "23",
   "metadata": {},
   "source": [
    "### Combine both directions\n",
    "When selecting an edge -> find adjacent faces in python -> send result back and append tooltip text"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24",
   "metadata": {},
   "outputs": [],
   "source": [
    "js_code = \"\"\"\n",
    "scene.on(\"select\", ({dim, index}) => {\n",
    "    console.log(\"selected\", dim, index);\n",
    "    scene.widget.send({type: 'select', dim, index})\n",
    "})\n",
    "\n",
    "scene.widget.model.on('msg:custom', (faces)=> {\n",
    "    console.log(\"received faces\", faces)\n",
    "    scene.tooltip.textContent += \", Faces: \";\n",
    "\n",
    "    for ( let i =0; i < faces.length; i++)\n",
    "        scene.tooltip.textContent += faces[i][0] + \" \" + faces[i][1] + \", \"\n",
    "\n",
    "    // extend tooltip width\n",
    "    scene.tooltip.style.width = \"300px\"\n",
    "})\n",
    "\"\"\"\n",
    "s = Draw(m, js_code=js_code);\n",
    "def onMessage(widget, content, buffers):\n",
    "    dim = content['dim']\n",
    "    index = content['index']\n",
    "    if dim == 1:\n",
    "        # find adjacent faces to selected edge\n",
    "        r = m.Region(BBND)\n",
    "        r.Mask().Clear()\n",
    "        r.Mask().Set(index)\n",
    "        nb = r.Neighbours(BND)\n",
    "        faces = []\n",
    "        boundaries = m.GetBoundaries()\n",
    "        faces = [ (i, boundaries[i]) for i, val in enumerate(nb.Mask()) if val ]\n",
    "        s.widget.send(faces)\n",
    "s.widget.on_msg(onMessage)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25",
   "metadata": {},
   "source": [
    "## JS classes\n",
    "\n",
    "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.\n",
    "- GUI settings: https://github.com/CERBSim/webgui/blob/main/src/gui.ts#L60\n",
    "- Scene: https://github.com/CERBSim/webgui/blob/main/src/scene.ts#L71\n",
    "- Camera controls: https://github.com/CERBSim/webgui/blob/main/src/camera.ts#L16"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
